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