azul_layout/managers/
text_edit.rs1use azul_core::{
12 dom::{DomId, DomNodeId, NodeId},
13 selection::{MultiCursorState, Selection, TextCursor},
14 styled_dom::NodeHierarchyItemId,
15 task::Instant,
16};
17
18
19pub const CURSOR_BLINK_INTERVAL_MS: u64 = 530;
21
22#[derive(Debug, Clone)]
27pub struct BlinkState {
28 pub is_visible: bool,
30 pub last_input_time: Option<Instant>,
33 pub blink_timer_active: bool,
35}
36
37impl Default for BlinkState {
38 fn default() -> Self {
39 Self {
40 is_visible: false,
41 last_input_time: None,
42 blink_timer_active: false,
43 }
44 }
45}
46
47impl BlinkState {
48 pub fn new() -> Self { Self::default() }
49
50 pub fn reset_blink_on_input(&mut self, now: Instant) {
52 self.is_visible = true;
53 self.last_input_time = Some(now);
54 }
55
56 pub fn toggle_visibility(&mut self) -> bool {
58 self.is_visible = !self.is_visible;
59 self.is_visible
60 }
61
62 pub fn set_visibility(&mut self, visible: bool) {
63 self.is_visible = visible;
64 }
65
66 pub fn set_blink_timer_active(&mut self, active: bool) {
67 self.blink_timer_active = active;
68 }
69
70 pub fn is_blink_timer_active(&self) -> bool {
71 self.blink_timer_active
72 }
73
74 pub fn should_blink(&self, now: &Instant) -> bool {
76 use azul_core::task::{Duration, SystemTimeDiff};
77 match &self.last_input_time {
78 Some(last_input) => {
79 let elapsed = now.duration_since(last_input);
80 let blink_interval = Duration::System(SystemTimeDiff::from_millis(CURSOR_BLINK_INTERVAL_MS));
81 elapsed.greater_than(&blink_interval)
82 }
83 None => true,
84 }
85 }
86
87 pub fn clear(&mut self) {
89 self.is_visible = false;
90 self.last_input_time = None;
91 self.blink_timer_active = false;
92 }
93}
94
95#[derive(Debug, Clone)]
101pub struct TextEditManager {
102 pub multi_cursor: Option<MultiCursorState>,
106 pub blink: BlinkState,
108 pub preedit_text: Option<String>,
111 pub preedit_cursor_begin: i32,
114 pub preedit_cursor_end: i32,
117 pub display_list_dirty: bool,
119}
120
121impl Default for TextEditManager {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl PartialEq for TextEditManager {
131 fn eq(&self, other: &Self) -> bool {
132 self.multi_cursor == other.multi_cursor
133 }
134}
135
136impl TextEditManager {
137 pub fn new() -> Self {
139 Self {
140 multi_cursor: None,
141 blink: BlinkState::new(),
142 preedit_text: None,
143 preedit_cursor_begin: -1,
144 preedit_cursor_end: -1,
145 display_list_dirty: false,
146 }
147 }
148
149 pub fn take_display_list_dirty(&mut self) -> bool {
153 let v = self.display_list_dirty;
154 self.display_list_dirty = false;
155 v
156 }
157
158 pub fn mark_dirty(&mut self) {
160 self.display_list_dirty = true;
161 }
162
163 pub fn has_active_editing(&self) -> bool {
167 self.multi_cursor.is_some()
168 }
169
170 pub fn get_editing_dom_id(&self) -> Option<DomId> {
172 self.multi_cursor.as_ref().map(|mc| mc.node_id.dom)
173 }
174
175 pub fn get_editing_node_id(&self) -> Option<NodeId> {
177 self.multi_cursor.as_ref()
178 .and_then(|mc| mc.node_id.node.into_crate_internal())
179 }
180
181 pub fn get_primary_cursor(&self) -> Option<TextCursor> {
183 self.multi_cursor.as_ref().and_then(|mc| mc.get_primary_cursor())
184 }
185
186 pub fn should_draw_cursor(&self) -> bool {
188 self.has_active_editing() && self.blink.is_visible
189 }
190
191 pub fn initialize_editing(
196 &mut self,
197 cursor: TextCursor,
198 dom_id: DomId,
199 node_id: NodeId,
200 contenteditable_key: u64,
201 ) {
202 let dom_node_id = DomNodeId {
203 dom: dom_id,
204 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
205 };
206 self.multi_cursor = Some(MultiCursorState::new_with_cursor(
207 cursor,
208 dom_node_id,
209 contenteditable_key,
210 ));
211 self.blink.is_visible = true;
212 self.blink.last_input_time = None;
213 self.clear_preedit();
214 self.mark_dirty();
215 }
216
217 pub fn clear_editing(&mut self) {
219 self.multi_cursor = None;
220 self.blink.clear();
221 self.clear_preedit();
222 self.mark_dirty();
223 }
224
225 pub fn set_preedit(&mut self, text: String, cursor_begin: i32, cursor_end: i32) {
229 self.preedit_text = if text.is_empty() { None } else { Some(text) };
230 self.preedit_cursor_begin = cursor_begin;
231 self.preedit_cursor_end = cursor_end;
232 self.mark_dirty();
233 }
234
235 pub fn clear_preedit(&mut self) {
237 self.preedit_text = None;
238 self.preedit_cursor_begin = -1;
239 self.preedit_cursor_end = -1;
240 self.mark_dirty();
241 }
242
243 pub fn build_cursor_locations(&self) -> Vec<(DomId, NodeId, TextCursor)> {
249 let Some(ref mc) = self.multi_cursor else {
250 return Vec::new();
251 };
252 let Some(node_id) = mc.node_id.node.into_crate_internal() else {
253 return Vec::new();
254 };
255 mc.selections.iter().map(|s| {
256 let cursor = match &s.selection {
257 Selection::Cursor(c) => *c,
258 Selection::Range(r) => r.end,
259 };
260 (mc.node_id.dom, node_id, cursor)
261 }).collect()
262 }
263
264 pub fn build_text_selections_map(&self) -> std::collections::BTreeMap<DomId, azul_core::selection::TextSelection> {
272 use azul_core::selection::{TextSelection, SelectionAnchor, SelectionFocus};
273 use azul_core::geom::LogicalRect;
274
275 let mut map = std::collections::BTreeMap::new();
276 let Some(ref mc) = self.multi_cursor else {
277 return map;
278 };
279 let Some(node_id) = mc.node_id.node.into_crate_internal() else {
280 return map;
281 };
282
283 let mut affected_nodes = std::collections::BTreeMap::new();
284 let mut first_range: Option<azul_core::selection::SelectionRange> = None;
285 for sel in &mc.selections {
286 if let Selection::Range(range) = &sel.selection {
287 affected_nodes.insert(node_id, *range);
288 if first_range.is_none() {
289 first_range = Some(*range);
290 }
291 }
292 }
293
294 if let Some(range) = first_range {
295 map.insert(mc.node_id.dom, TextSelection {
296 dom_id: mc.node_id.dom,
297 anchor: SelectionAnchor {
298 ifc_root_node_id: node_id,
299 cursor: range.start,
300 char_bounds: LogicalRect::zero(),
301 mouse_position: azul_core::geom::LogicalPosition::zero(),
302 },
303 focus: SelectionFocus {
304 ifc_root_node_id: node_id,
305 cursor: range.end,
306 mouse_position: azul_core::geom::LogicalPosition::zero(),
307 },
308 affected_nodes,
309 is_forward: true,
310 });
311 }
312
313 map
314 }
315}