1use blockworx_canvas2d::css_color;
9use blockworx_geom::{Align, WorldPx};
10use blockworx_kernel::Event;
11use blockworx_paint::edit::{EDITOR_BORDER, EDITOR_PAD, EDITOR_ROUNDING};
12use blockworx_paint::{EditField, FontChoice, TextEvent, Vantage};
13use dioxus::prelude::*;
14use dioxus::web::WebEventExt as _;
15use wasm_bindgen::JsCast as _;
16use web_sys::{HtmlInputElement, HtmlTextAreaElement};
17
18use crate::shell::Shell;
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24enum Ending {
25 Open,
26 Said,
27}
28
29#[component]
32pub fn InPlaceEditor(
33 shell: Shell,
34 field: EditField,
35 vantage: Vantage,
36 typeface: FontChoice,
37) -> Element {
38 let shell = use_hook(|| CopyValue::new(shell));
39 let mut draft = use_signal(|| field.text.clone());
40 let mut ending = use_signal(|| Ending::Open);
41 let id = field.id;
42 let tab_cycle = field.tab_cycle;
43 let multiline = field.multiline;
44 let selecting = Selecting::of(&field);
45
46 let mut ends = move |event: TextEvent| {
47 if *ending.peek() == Ending::Open {
48 ending.set(Ending::Said);
49 let shell = shell.read();
50 shell.say(Event::Text(event));
51 shell.takes_keyboard();
52 }
53 };
54 let typed = move |event: KeyboardEvent| {
55 event.stop_propagation();
58 match event.key() {
59 Key::Escape => ends(TextEvent::Cancelled { id }),
60 Key::Enter if !multiline => ends(TextEvent::Committed {
61 id,
62 text: draft.peek().clone(),
63 }),
64 Key::Tab if tab_cycle => {
65 event.prevent_default();
66 ends(TextEvent::TabPressed {
67 id,
68 text: draft.peek().clone(),
69 });
70 }
71 _ => {}
72 }
73 };
74 let left = move |_| {
75 ends(TextEvent::Committed {
76 id,
77 text: draft.peek().clone(),
78 });
79 };
80 let opened = move |event: MountedEvent| {
81 if let Some(element) = event.try_as_web_event() {
82 focus(&element, selecting);
83 }
84 };
85 let style = style_of(&field, vantage, typeface);
86 let hint = field
87 .hint
88 .as_ref()
89 .map_or_else(String::new, |it| it.to_string());
90 let limit = field.char_limit.map(|limit| limit.to_string());
91
92 rsx! {
93 if multiline {
94 textarea {
95 class: "bw-field",
96 style: "{style}",
97 maxlength: limit,
98 placeholder: "{hint}",
99 value: "{draft}",
100 onmounted: opened,
101 oninput: move |event| draft.set(event.value()),
102 onkeydown: typed,
103 onfocusout: left,
104 }
105 } else {
106 input {
107 class: "bw-field",
108 style: "{style}",
109 r#type: "text",
110 maxlength: limit,
111 placeholder: "{hint}",
112 value: "{draft}",
113 onmounted: opened,
114 oninput: move |event| draft.set(event.value()),
115 onkeydown: typed,
116 onfocusout: left,
117 }
118 }
119 }
120}
121
122#[derive(Clone, Copy, PartialEq, Eq, Debug)]
124enum Selecting {
125 All,
126 Nothing,
127}
128
129impl Selecting {
130 fn of(field: &EditField) -> Self {
134 if field.select_all_on_focus || field.tab_cycle {
135 Self::All
136 } else {
137 Self::Nothing
138 }
139 }
140}
141
142fn focus(element: &web_sys::Element, selecting: Selecting) {
143 if let Some(field) = element.dyn_ref::<HtmlTextAreaElement>() {
144 let _ = field.focus();
145 if selecting == Selecting::All {
146 field.select();
147 }
148 } else if let Some(field) = element.dyn_ref::<HtmlInputElement>() {
149 let _ = field.focus();
150 if selecting == Selecting::All {
151 field.select();
152 }
153 }
154}
155
156fn style_of(field: &EditField, vantage: Vantage, typeface: FontChoice) -> String {
161 let rect = field.rect;
162 let font = vantage.remap_font(&field.font);
163 let pad = (
164 vantage.remap_len(WorldPx::new(EDITOR_PAD.x)),
165 vantage.remap_len(WorldPx::new(EDITOR_PAD.y)),
166 );
167 let colors = field.colors;
168 [
169 format!("left:{}px", rect.min.x),
170 format!("top:{}px", rect.min.y),
171 format!("width:{}px", rect.width()),
172 if field.multiline {
174 format!("min-height:{}px", rect.height())
175 } else {
176 format!("height:{}px", rect.height())
177 },
178 format!("transform:rotate({}deg)", field.angle.degrees()),
179 format!("font-family:{}", typeface.family_name()),
182 format!("font-size:{}px", font.size),
183 format!("text-align:{}", aligned(field.align.x())),
184 format!("color:{}", css_color(colors.text)),
185 format!("background:{}", css_color(colors.background)),
186 format!("caret-color:{}", css_color(colors.caret)),
187 format!("--bw-selection:{}", css_color(colors.selection)),
188 format!(
189 "border:{}px solid {}",
190 vantage.remap_len(EDITOR_BORDER),
191 css_color(colors.border),
192 ),
193 format!("border-radius:{}px", vantage.remap_len(EDITOR_ROUNDING)),
194 format!("padding:{}px {}px", pad.1, pad.0),
195 ]
196 .join(";")
197}
198
199fn aligned(align: Align) -> &'static str {
200 match align {
201 Align::Min => "left",
202 Align::Center => "center",
203 Align::Max => "right",
204 }
205}
206
207pub fn register_faces() {
212 let Some(fonts) = crate::shell::window()
213 .and_then(|window| window.document())
214 .map(|document| document.fonts())
215 else {
216 return;
217 };
218 for typeface in FontChoice::ALL {
219 let Ok(face) =
220 web_sys::FontFace::new_with_u8_array(typeface.family_name(), typeface.bytes())
221 else {
222 tracing::error!("the browser refused the {} face", typeface.family_name());
223 continue;
224 };
225 let _ = face.load();
226 let _ = fonts.add(&face);
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
237 fn every_alignment_has_its_own_keyword() {
238 let mut named = [Align::Min, Align::Center, Align::Max].map(aligned);
239 named.sort_unstable();
240 let count = named.len();
241 let mut unique = named.to_vec();
242 unique.dedup();
243 assert_eq!(unique.len(), count);
244 }
245}