1use alloc::collections::BTreeMap;
7use alloc::vec::Vec;
8use core::time::Duration;
9
10use azul_core::{
11 dom::{DomId, DomNodeId, NodeId},
12 events::SelectionManagerQuery,
13 geom::{LogicalPosition, LogicalRect},
14 selection::{
15 Selection, SelectionAnchor, SelectionFocus, SelectionRange, SelectionState, SelectionVec,
16 TextCursor, TextSelection,
17 },
18};
19use azul_css::{impl_option, impl_option_inner, AzString, OptionString};
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct ClickState {
24 pub last_node: Option<DomNodeId>,
26 pub last_position: LogicalPosition,
28 pub last_time_ms: u64,
30 pub click_count: u8,
32}
33
34impl Default for ClickState {
35 fn default() -> Self {
36 Self {
37 last_node: None,
38 last_position: LogicalPosition { x: 0.0, y: 0.0 },
39 last_time_ms: 0,
40 click_count: 0,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
50pub struct SelectionManager {
51 pub selections: BTreeMap<DomId, SelectionState>,
55
56 pub text_selections: BTreeMap<DomId, TextSelection>,
59
60 pub click_state: ClickState,
62}
63
64impl Default for SelectionManager {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl SelectionManager {
71 pub const MULTI_CLICK_TIMEOUT_MS: u64 = 500;
73 pub const MULTI_CLICK_DISTANCE_PX: f32 = 5.0;
75
76 pub fn new() -> Self {
78 Self {
79 selections: BTreeMap::new(),
80 text_selections: BTreeMap::new(),
81 click_state: ClickState::default(),
82 }
83 }
84
85 pub fn update_click_count(
88 &mut self,
89 node_id: DomNodeId,
90 position: LogicalPosition,
91 current_time_ms: u64,
92 ) -> u8 {
93 let should_increment = if let Some(last_node) = self.click_state.last_node {
95 if last_node != node_id {
96 return self.reset_click_count(node_id, position, current_time_ms);
97 }
98
99 let time_delta = current_time_ms.saturating_sub(self.click_state.last_time_ms);
100 if time_delta >= Self::MULTI_CLICK_TIMEOUT_MS {
101 return self.reset_click_count(node_id, position, current_time_ms);
102 }
103
104 let dx = position.x - self.click_state.last_position.x;
105 let dy = position.y - self.click_state.last_position.y;
106 let distance = (dx * dx + dy * dy).sqrt();
107 if distance >= Self::MULTI_CLICK_DISTANCE_PX {
108 return self.reset_click_count(node_id, position, current_time_ms);
109 }
110
111 true
112 } else {
113 false
114 };
115
116 let click_count = if should_increment {
117 let new_count = self.click_state.click_count + 1;
119 if new_count > 3 {
120 1
121 } else {
122 new_count
123 }
124 } else {
125 1
126 };
127
128 self.click_state = ClickState {
129 last_node: Some(node_id),
130 last_position: position,
131 last_time_ms: current_time_ms,
132 click_count,
133 };
134
135 click_count
136 }
137
138 fn reset_click_count(
140 &mut self,
141 node_id: DomNodeId,
142 position: LogicalPosition,
143 current_time_ms: u64,
144 ) -> u8 {
145 self.click_state = ClickState {
146 last_node: Some(node_id),
147 last_position: position,
148 last_time_ms: current_time_ms,
149 click_count: 1,
150 };
151 1
152 }
153
154 pub fn get_selection(&self, dom_id: &DomId) -> Option<&SelectionState> {
156 self.selections.get(dom_id)
157 }
158
159 pub fn get_selection_mut(&mut self, dom_id: &DomId) -> Option<&mut SelectionState> {
161 self.selections.get_mut(dom_id)
162 }
163
164 pub fn set_selection(&mut self, dom_id: DomId, selection: SelectionState) {
166 self.selections.insert(dom_id, selection);
167 }
168
169 pub fn set_cursor(&mut self, dom_id: DomId, node_id: DomNodeId, cursor: TextCursor) {
171 let state = SelectionState {
172 selections: vec![Selection::Cursor(cursor)].into(),
173 node_id,
174 };
175 self.selections.insert(dom_id, state);
176 }
177
178 pub fn set_range(&mut self, dom_id: DomId, node_id: DomNodeId, range: SelectionRange) {
180 let state = SelectionState {
181 selections: vec![Selection::Range(range)].into(),
182 node_id,
183 };
184 self.selections.insert(dom_id, state);
185 }
186
187 pub fn add_selection(&mut self, dom_id: DomId, node_id: DomNodeId, selection: Selection) {
189 self.selections
190 .entry(dom_id)
191 .or_insert_with(|| SelectionState {
192 selections: SelectionVec::from_const_slice(&[]),
193 node_id,
194 })
195 .add(selection);
196 }
197
198 pub fn clear_selection(&mut self, dom_id: &DomId) {
200 self.selections.remove(dom_id);
201 }
202
203 pub fn clear_all(&mut self) {
205 self.selections.clear();
206 }
207
208 pub fn get_all_selections(&self) -> &BTreeMap<DomId, SelectionState> {
210 &self.selections
211 }
212
213 pub fn has_any_selection(&self) -> bool {
215 !self.selections.is_empty()
216 }
217
218 pub fn has_selection(&self, dom_id: &DomId) -> bool {
220 self.selections.contains_key(dom_id)
221 }
222
223 pub fn get_primary_cursor(&self, dom_id: &DomId) -> Option<TextCursor> {
225 self.selections
226 .get(dom_id)?
227 .selections
228 .as_slice()
229 .first()
230 .and_then(|s| match s {
231 Selection::Cursor(c) => Some(c.clone()),
232 Selection::Range(r) => Some(r.end.clone()),
234 })
235 }
236
237 pub fn get_ranges(&self, dom_id: &DomId) -> alloc::vec::Vec<SelectionRange> {
239 self.selections
240 .get(dom_id)
241 .map(|state| {
242 state
243 .selections
244 .as_slice()
245 .iter()
246 .filter_map(|s| match s {
247 Selection::Range(r) => Some(r.clone()),
248 Selection::Cursor(_) => None,
249 })
250 .collect()
251 })
252 .unwrap_or_default()
253 }
254
255 pub fn analyze_click_for_selection(
267 &self,
268 node_id: DomNodeId,
269 position: LogicalPosition,
270 current_time_ms: u64,
271 ) -> Option<u8> {
272 let click_state = &self.click_state;
273
274 if let Some(last_node) = click_state.last_node {
276 if last_node != node_id {
277 return Some(1); }
279
280 let time_delta = current_time_ms.saturating_sub(click_state.last_time_ms);
281 if time_delta >= Self::MULTI_CLICK_TIMEOUT_MS {
282 return Some(1); }
284
285 let dx = position.x - click_state.last_position.x;
286 let dy = position.y - click_state.last_position.y;
287 let distance = (dx * dx + dy * dy).sqrt();
288 if distance >= Self::MULTI_CLICK_DISTANCE_PX {
289 return Some(1); }
291 } else {
292 return Some(1); }
294
295 let next_count = click_state.click_count + 1;
297 if next_count > 3 {
298 Some(1) } else {
300 Some(next_count)
301 }
302 }
303
304 pub fn start_selection(
320 &mut self,
321 dom_id: DomId,
322 ifc_root_node_id: NodeId,
323 cursor: TextCursor,
324 char_bounds: LogicalRect,
325 mouse_position: LogicalPosition,
326 ) {
327 let selection = TextSelection::new_collapsed(
328 dom_id,
329 ifc_root_node_id,
330 cursor,
331 char_bounds,
332 mouse_position,
333 );
334 self.text_selections.insert(dom_id, selection);
335 }
336
337 pub fn update_selection_focus(
354 &mut self,
355 dom_id: &DomId,
356 ifc_root_node_id: NodeId,
357 cursor: TextCursor,
358 mouse_position: LogicalPosition,
359 affected_nodes: BTreeMap<NodeId, SelectionRange>,
360 is_forward: bool,
361 ) -> bool {
362 if let Some(selection) = self.text_selections.get_mut(dom_id) {
363 selection.focus = SelectionFocus {
364 ifc_root_node_id,
365 cursor,
366 mouse_position,
367 };
368 selection.affected_nodes = affected_nodes;
369 selection.is_forward = is_forward;
370 true
371 } else {
372 false
373 }
374 }
375
376 pub fn get_text_selection(&self, dom_id: &DomId) -> Option<&TextSelection> {
378 self.text_selections.get(dom_id)
379 }
380
381 pub fn get_text_selection_mut(&mut self, dom_id: &DomId) -> Option<&mut TextSelection> {
383 self.text_selections.get_mut(dom_id)
384 }
385
386 pub fn has_text_selection(&self, dom_id: &DomId) -> bool {
388 self.text_selections.contains_key(dom_id)
389 }
390
391 pub fn get_range_for_ifc_root(
404 &self,
405 dom_id: &DomId,
406 ifc_root_node_id: &NodeId,
407 ) -> Option<&SelectionRange> {
408 self.text_selections
409 .get(dom_id)?
410 .get_range_for_node(ifc_root_node_id)
411 }
412
413 pub fn clear_text_selection(&mut self, dom_id: &DomId) {
415 self.text_selections.remove(dom_id);
416 }
417
418 pub fn clear_all_text_selections(&mut self) {
420 self.text_selections.clear();
421 }
422
423 pub fn get_all_text_selections(&self) -> &BTreeMap<DomId, TextSelection> {
425 &self.text_selections
426 }
427}
428
429#[derive(Debug, Clone, PartialEq)]
433#[repr(C)]
434pub struct StyledTextRun {
435 pub text: AzString,
437 pub font_family: OptionString,
439 pub font_size_px: f32,
441 pub color: azul_css::props::basic::ColorU,
443 pub is_bold: bool,
445 pub is_italic: bool,
447}
448
449azul_css::impl_vec!(
450 StyledTextRun,
451 StyledTextRunVec,
452 StyledTextRunVecDestructor,
453 StyledTextRunVecDestructorType
454);
455azul_css::impl_vec_debug!(StyledTextRun, StyledTextRunVec);
456azul_css::impl_vec_clone!(StyledTextRun, StyledTextRunVec, StyledTextRunVecDestructor);
457azul_css::impl_vec_partialeq!(StyledTextRun, StyledTextRunVec);
458
459#[derive(Debug, Clone, PartialEq)]
461#[repr(C)]
462pub struct ClipboardContent {
463 pub plain_text: AzString,
465 pub styled_runs: StyledTextRunVec,
467}
468
469impl_option!(
470 ClipboardContent,
471 OptionClipboardContent,
472 copy = false,
473 [Debug, Clone, PartialEq]
474);
475
476impl ClipboardContent {
477 pub fn to_html(&self) -> String {
479 let mut html = String::from("<div>");
480
481 for run in self.styled_runs.as_slice() {
482 html.push_str("<span style=\"");
483
484 if let Some(font_family) = run.font_family.as_ref() {
485 html.push_str(&format!("font-family: {}; ", font_family.as_str()));
486 }
487 html.push_str(&format!("font-size: {}px; ", run.font_size_px));
488 html.push_str(&format!(
489 "color: rgba({}, {}, {}, {}); ",
490 run.color.r,
491 run.color.g,
492 run.color.b,
493 run.color.a as f32 / 255.0
494 ));
495 if run.is_bold {
496 html.push_str("font-weight: bold; ");
497 }
498 if run.is_italic {
499 html.push_str("font-style: italic; ");
500 }
501
502 html.push_str("\">");
503 let escaped = run
505 .text
506 .as_str()
507 .replace('&', "&")
508 .replace('<', "<")
509 .replace('>', ">");
510 html.push_str(&escaped);
511 html.push_str("</span>");
512 }
513
514 html.push_str("</div>");
515 html
516 }
517}
518
519impl SelectionManagerQuery for SelectionManager {
522 fn get_click_count(&self) -> u8 {
523 self.click_state.click_count
524 }
525
526 fn get_drag_start_position(&self) -> Option<LogicalPosition> {
527 if self.click_state.click_count > 0 {
530 Some(self.click_state.last_position)
531 } else {
532 None
533 }
534 }
535
536 fn has_selection(&self) -> bool {
537 if self.click_state.click_count > 0 {
544 return true;
545 }
546
547 for (_dom_id, selection_state) in &self.selections {
549 if !selection_state.selections.is_empty() {
550 return true;
551 }
552 }
553
554 false
555 }
556}
557
558impl SelectionManager {
559 pub fn remap_node_ids(
564 &mut self,
565 dom_id: DomId,
566 node_id_map: &std::collections::BTreeMap<azul_core::dom::NodeId, azul_core::dom::NodeId>,
567 ) {
568 use azul_core::styled_dom::NodeHierarchyItemId;
569
570 if let Some(selection_state) = self.selections.get_mut(&dom_id) {
572 if let Some(old_node_id) = selection_state.node_id.node.into_crate_internal() {
573 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
574 selection_state.node_id.node = NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
575 } else {
576 self.selections.remove(&dom_id);
578 return;
579 }
580 }
581 }
582
583 if let Some(text_selection) = self.text_selections.get_mut(&dom_id) {
585 let old_anchor_id = text_selection.anchor.ifc_root_node_id;
587 if let Some(&new_node_id) = node_id_map.get(&old_anchor_id) {
588 text_selection.anchor.ifc_root_node_id = new_node_id;
589 } else {
590 self.text_selections.remove(&dom_id);
592 return;
593 }
594
595 let old_focus_id = text_selection.focus.ifc_root_node_id;
597 if let Some(&new_node_id) = node_id_map.get(&old_focus_id) {
598 text_selection.focus.ifc_root_node_id = new_node_id;
599 } else {
600 self.text_selections.remove(&dom_id);
602 return;
603 }
604
605 let old_affected: Vec<_> = text_selection.affected_nodes.keys().cloned().collect();
607 let mut new_affected = std::collections::BTreeMap::new();
608 for old_node_id in old_affected {
609 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
610 if let Some(range) = text_selection.affected_nodes.remove(&old_node_id) {
611 new_affected.insert(new_node_id, range);
612 }
613 }
614 }
615 text_selection.affected_nodes = new_affected;
616 }
617
618 if let Some(last_node) = &mut self.click_state.last_node {
620 if last_node.dom == dom_id {
621 if let Some(old_node_id) = last_node.node.into_crate_internal() {
622 if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
623 last_node.node = NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
624 } else {
625 self.click_state = ClickState::default();
627 }
628 }
629 }
630 }
631 }
632}