azul_layout/managers/
text_edit.rs1use azul_core::{
13 dom::{DomId, DomNodeId, NodeId},
14 selection::{MultiCursorState, Selection, TextCursor},
15 styled_dom::NodeHierarchyItemId,
16 task::Instant,
17};
18
19
20pub const CURSOR_BLINK_INTERVAL_MS: u64 = 530;
22
23#[derive(Debug, Clone)]
28#[derive(Default)]
29pub struct BlinkState {
30 pub is_visible: bool,
32 pub last_input_time: Option<Instant>,
35 pub blink_timer_active: bool,
37}
38
39
40impl BlinkState {
41 #[must_use] pub fn new() -> Self { Self::default() }
42
43 pub fn reset_blink_on_input(&mut self, now: Instant) {
45 self.is_visible = true;
46 self.last_input_time = Some(now);
47 }
48
49 pub const fn toggle_visibility(&mut self) -> bool {
51 self.is_visible = !self.is_visible;
52 self.is_visible
53 }
54
55 pub const fn set_visibility(&mut self, visible: bool) {
56 self.is_visible = visible;
57 }
58
59 pub const fn set_blink_timer_active(&mut self, active: bool) {
60 self.blink_timer_active = active;
61 }
62
63 #[must_use] pub const fn is_blink_timer_active(&self) -> bool {
64 self.blink_timer_active
65 }
66
67 #[must_use] pub fn should_blink(&self, now: &Instant) -> bool {
69 use azul_core::task::{Duration, SystemTimeDiff};
70 self.last_input_time.as_ref().is_none_or(|last_input| {
71 let elapsed = now.duration_since(last_input);
72 let blink_interval = Duration::System(SystemTimeDiff::from_millis(CURSOR_BLINK_INTERVAL_MS));
73 elapsed.greater_than(&blink_interval)
74 })
75 }
76
77 pub fn clear(&mut self) {
79 self.is_visible = false;
80 self.last_input_time = None;
81 self.blink_timer_active = false;
82 }
83}
84
85#[derive(Debug, Clone)]
91pub struct TextEditManager {
92 pub multi_cursor: Option<MultiCursorState>,
96 pub blink: BlinkState,
98 pub preedit_text: Option<String>,
101 pub preedit_cursor_begin: i32,
104 pub preedit_cursor_end: i32,
107 pub display_list_dirty: bool,
109}
110
111impl Default for TextEditManager {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl PartialEq for TextEditManager {
121 fn eq(&self, other: &Self) -> bool {
122 self.multi_cursor == other.multi_cursor
123 }
124}
125
126impl TextEditManager {
127 #[must_use] pub fn new() -> Self {
129 Self {
130 multi_cursor: None,
131 blink: BlinkState::new(),
132 preedit_text: None,
133 preedit_cursor_begin: -1,
134 preedit_cursor_end: -1,
135 display_list_dirty: false,
136 }
137 }
138
139 pub const fn mark_dirty(&mut self) {
143 self.display_list_dirty = true;
144 }
145
146 #[must_use] pub const fn has_active_editing(&self) -> bool {
150 self.multi_cursor.is_some()
151 }
152
153 #[must_use] pub fn get_editing_dom_id(&self) -> Option<DomId> {
155 self.multi_cursor.as_ref().map(|mc| mc.node_id.dom)
156 }
157
158 #[must_use] pub fn get_editing_node_id(&self) -> Option<NodeId> {
160 self.multi_cursor.as_ref()
161 .and_then(|mc| mc.node_id.node.into_crate_internal())
162 }
163
164 #[must_use] pub fn get_primary_cursor(&self) -> Option<TextCursor> {
166 self.multi_cursor.as_ref().and_then(MultiCursorState::get_primary_cursor)
167 }
168
169 #[must_use] pub const fn should_draw_cursor(&self) -> bool {
171 self.has_active_editing() && self.blink.is_visible
172 }
173
174 pub fn initialize_editing(
179 &mut self,
180 cursor: TextCursor,
181 dom_id: DomId,
182 node_id: NodeId,
183 contenteditable_key: u64,
184 ) {
185 let dom_node_id = DomNodeId {
186 dom: dom_id,
187 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
188 };
189 self.multi_cursor = Some(MultiCursorState::new_with_cursor(
190 cursor,
191 dom_node_id,
192 contenteditable_key,
193 ));
194 self.blink.is_visible = true;
195 self.blink.last_input_time = None;
196 self.clear_preedit();
197 self.mark_dirty();
198 }
199
200 pub fn clear_editing(&mut self) {
202 self.multi_cursor = None;
203 self.blink.clear();
204 self.clear_preedit();
205 self.mark_dirty();
206 }
207
208 pub fn set_preedit(&mut self, text: String, cursor_begin: i32, cursor_end: i32) {
212 self.preedit_text = if text.is_empty() { None } else { Some(text) };
213 self.preedit_cursor_begin = cursor_begin;
214 self.preedit_cursor_end = cursor_end;
215 self.mark_dirty();
216 }
217
218 pub fn clear_preedit(&mut self) {
220 self.preedit_text = None;
221 self.preedit_cursor_begin = -1;
222 self.preedit_cursor_end = -1;
223 self.mark_dirty();
224 }
225
226 #[must_use] pub fn build_cursor_locations(&self) -> Vec<(DomId, NodeId, TextCursor)> {
232 let Some(ref mc) = self.multi_cursor else {
233 return Vec::new();
234 };
235 let Some(node_id) = mc.node_id.node.into_crate_internal() else {
236 return Vec::new();
237 };
238 mc.selections.iter().map(|s| {
239 let cursor = match &s.selection {
240 Selection::Cursor(c) => *c,
241 Selection::Range(r) => r.end,
242 };
243 (mc.node_id.dom, node_id, cursor)
244 }).collect()
245 }
246
247 #[must_use] pub fn build_text_selections_map(&self) -> std::collections::BTreeMap<DomId, azul_core::selection::TextSelection> {
255 use azul_core::selection::{TextSelection, SelectionAnchor, SelectionFocus};
256 use azul_core::geom::LogicalRect;
257
258 let mut map = std::collections::BTreeMap::new();
259 let Some(ref mc) = self.multi_cursor else {
260 return map;
261 };
262 let Some(node_id) = mc.node_id.node.into_crate_internal() else {
263 return map;
264 };
265
266 let mut affected_nodes = std::collections::BTreeMap::new();
267 let mut first_range: Option<azul_core::selection::SelectionRange> = None;
268 for sel in &mc.selections {
269 if let Selection::Range(range) = &sel.selection {
270 affected_nodes.insert(node_id, *range);
271 if first_range.is_none() {
272 first_range = Some(*range);
273 }
274 }
275 }
276
277 if let Some(range) = first_range {
278 map.insert(mc.node_id.dom, TextSelection {
279 dom_id: mc.node_id.dom,
280 anchor: SelectionAnchor {
281 ifc_root_node_id: node_id,
282 cursor: range.start,
283 char_bounds: LogicalRect::zero(),
284 mouse_position: azul_core::geom::LogicalPosition::zero(),
285 },
286 focus: SelectionFocus {
287 ifc_root_node_id: node_id,
288 cursor: range.end,
289 mouse_position: azul_core::geom::LogicalPosition::zero(),
290 },
291 affected_nodes,
292 is_forward: true,
293 });
294 }
295
296 map
297 }
298}
299
300impl crate::managers::NodeIdRemap for TextEditManager {
301 fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
307 let Some(ref mut mc) = self.multi_cursor else {
308 return;
309 };
310 if mc.node_id.dom != dom {
311 return;
312 }
313 let unmounted = mc
314 .node_id
315 .node
316 .into_crate_internal()
317 .is_none_or(|old| map.resolve(old).is_none());
318 if unmounted {
319 self.multi_cursor = None;
320 self.preedit_text = None;
321 self.preedit_cursor_begin = -1;
322 self.preedit_cursor_end = -1;
323 self.display_list_dirty = true;
324 return;
325 }
326 mc.remap_node_ids(dom, map.as_btree_map());
327 }
328}