1use std::collections::BTreeMap;
2use std::fmt;
3use std::ops::Range;
4
5use crate::core::rect::Rect;
6use crate::sanitize::char_display_width;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct WidgetId(pub String);
10
11impl WidgetId {
12 pub fn new(id: impl Into<String>) -> Self {
13 Self(id.into())
14 }
15}
16
17impl AsRef<str> for WidgetId {
18 fn as_ref(&self) -> &str {
19 &self.0
20 }
21}
22
23impl fmt::Display for WidgetId {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(&self.0)
26 }
27}
28
29impl From<&str> for WidgetId {
30 fn from(value: &str) -> Self {
31 Self::new(value)
32 }
33}
34
35impl From<String> for WidgetId {
36 fn from(value: String) -> Self {
37 Self::new(value)
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct SelectionId(pub String);
43
44impl SelectionId {
45 pub fn new(id: impl Into<String>) -> Self {
46 Self(id.into())
47 }
48}
49
50impl AsRef<str> for SelectionId {
51 fn as_ref(&self) -> &str {
52 &self.0
53 }
54}
55
56impl From<&str> for SelectionId {
57 fn from(value: &str) -> Self {
58 Self::new(value)
59 }
60}
61
62impl From<String> for SelectionId {
63 fn from(value: String) -> Self {
64 Self::new(value)
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub struct SelectionGroup(pub String);
70
71impl SelectionGroup {
72 pub fn new(id: impl Into<String>) -> Self {
73 Self(id.into())
74 }
75}
76
77impl AsRef<str> for SelectionGroup {
78 fn as_ref(&self) -> &str {
79 &self.0
80 }
81}
82
83impl From<&str> for SelectionGroup {
84 fn from(value: &str) -> Self {
85 Self::new(value)
86 }
87}
88
89impl From<String> for SelectionGroup {
90 fn from(value: String) -> Self {
91 Self::new(value)
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub struct Position {
97 pub x: u16,
98 pub y: u16,
99}
100
101impl Position {
102 pub const fn new(x: u16, y: u16) -> Self {
103 Self { x, y }
104 }
105
106 pub fn local_to(self, area: Rect) -> Self {
107 Self {
108 x: self.x.saturating_sub(area.x),
109 y: self.y.saturating_sub(area.y),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub enum MouseButton {
116 Left,
117 Right,
118 Middle,
119 WheelUp,
120 WheelDown,
121 Other(u8),
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum MouseCursor {
126 Default,
127 Pointer,
128 Text,
129 Grab,
130 Grabbing,
131 ResizeHorizontal,
132 ResizeVertical,
133 Crosshair,
134 NotAllowed,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138pub enum WidgetAction {
139 Activate,
140 Focus,
141 Toggle,
142 Open,
143 Copy,
144 CopyCode,
145 Select,
146 Scroll,
147 Drag,
148 Custom(String),
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
152pub enum WidgetRole {
153 Unknown,
154 Button,
155 ListItem,
156 Tab,
157 TextSpan,
158 CodeBlock,
159 CodeLine,
160 TodoItem,
161 StatusIndicator,
162 ScrollbarThumb,
163 Link,
164 Panel,
165 Pane,
166 Region,
167 Scrollbar,
168 Status,
169 Text,
170 Toggle,
171 Tooltip,
172 Transcript,
173 TranscriptRow,
174 BoardColumn,
175 BoardCard,
176 Effect,
177 CommandPaletteEntry,
178 ModelRow,
179 RuntimeDiagnostic,
180 Custom(String),
181}
182
183impl Default for WidgetRole {
184 fn default() -> Self {
185 Self::Unknown
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
190pub struct WidgetState {
191 pub focused: bool,
192 pub hovered: bool,
193 pub selected: bool,
194 pub disabled: bool,
195 pub checked: bool,
196 pub active: bool,
197 pub expanded: bool,
198}
199
200impl WidgetState {
201 pub const fn focused(mut self, focused: bool) -> Self {
202 self.focused = focused;
203 self
204 }
205
206 pub const fn hovered(mut self, hovered: bool) -> Self {
207 self.hovered = hovered;
208 self
209 }
210
211 pub const fn selected(mut self, selected: bool) -> Self {
212 self.selected = selected;
213 self
214 }
215
216 pub const fn checked(mut self, checked: bool) -> Self {
217 self.checked = checked;
218 self
219 }
220
221 pub const fn active(mut self, active: bool) -> Self {
222 self.active = active;
223 self
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228pub enum WidgetValue {
229 Text(String),
230 Count(usize),
231 Percent(u16),
232 Status(String),
233 Language(String),
234 LineNumber(usize),
235 Custom(String),
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct HitRegion {
240 pub id: WidgetId,
241 pub area: Rect,
242 pub role: WidgetRole,
243 pub label: String,
244 pub tooltip: Option<String>,
245 pub action: Option<WidgetAction>,
246 pub cursor: Option<MouseCursor>,
247 pub z_index: i16,
248 pub row: Option<usize>,
249 pub column: Option<usize>,
250 pub selection_group: Option<SelectionGroup>,
251 pub description: Option<String>,
252 pub shortcut: Option<String>,
253 pub state: WidgetState,
254 pub value: Option<WidgetValue>,
255}
256
257impl HitRegion {
258 pub fn new(id: impl Into<WidgetId>, area: Rect) -> Self {
259 Self {
260 id: id.into(),
261 area,
262 role: WidgetRole::Unknown,
263 label: String::new(),
264 tooltip: None,
265 action: None,
266 cursor: None,
267 z_index: 0,
268 row: None,
269 column: None,
270 selection_group: None,
271 description: None,
272 shortcut: None,
273 state: WidgetState::default(),
274 value: None,
275 }
276 }
277
278 pub fn with_role(mut self, role: WidgetRole) -> Self {
279 self.role = role;
280 self
281 }
282
283 pub fn with_label(mut self, label: impl Into<String>) -> Self {
284 self.label = label.into();
285 self
286 }
287
288 pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
289 self.tooltip = Some(tooltip.into());
290 self
291 }
292
293 pub fn with_action(mut self, action: WidgetAction) -> Self {
294 self.action = Some(action);
295 self
296 }
297
298 pub fn with_cursor(mut self, cursor: MouseCursor) -> Self {
299 self.cursor = Some(cursor);
300 self
301 }
302
303 pub fn with_z_index(mut self, z_index: i16) -> Self {
304 self.z_index = z_index;
305 self
306 }
307
308 pub fn with_row(mut self, row: usize) -> Self {
309 self.row = Some(row);
310 self
311 }
312
313 pub fn with_column(mut self, column: usize) -> Self {
314 self.column = Some(column);
315 self
316 }
317
318 pub fn with_selection_group(mut self, group: impl Into<SelectionGroup>) -> Self {
319 self.selection_group = Some(group.into());
320 self
321 }
322
323 pub fn with_description(mut self, description: impl Into<String>) -> Self {
324 self.description = Some(description.into());
325 self
326 }
327
328 pub fn with_shortcut(mut self, shortcut: impl Into<String>) -> Self {
329 self.shortcut = Some(shortcut.into());
330 self
331 }
332
333 pub fn with_state(mut self, state: WidgetState) -> Self {
334 self.state = state;
335 self
336 }
337
338 pub fn with_value(mut self, value: WidgetValue) -> Self {
339 self.value = Some(value);
340 self
341 }
342
343 pub fn contains(&self, x: u16, y: u16) -> bool {
344 rect_contains(self.area, x, y)
345 }
346
347 pub fn local_position(&self, x: u16, y: u16) -> Position {
348 Position::new(x, y).local_to(self.area)
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Default)]
353pub struct WidgetMetadata {
354 pub id: WidgetId,
355 pub role: WidgetRole,
356 pub label: String,
357 pub tooltip: Option<String>,
358 pub action: Option<WidgetAction>,
359 pub cursor: Option<MouseCursor>,
360 pub z_index: i16,
361 pub row: Option<usize>,
362 pub column: Option<usize>,
363 pub selection_group: Option<SelectionGroup>,
364 pub description: Option<String>,
365 pub shortcut: Option<String>,
366 pub state: WidgetState,
367 pub value: Option<WidgetValue>,
368}
369
370impl WidgetMetadata {
371 pub fn new(id: impl Into<WidgetId>) -> Self {
372 Self {
373 id: id.into(),
374 ..Self::default()
375 }
376 }
377
378 pub fn with_role(mut self, role: WidgetRole) -> Self {
379 self.role = role;
380 self
381 }
382
383 pub fn with_label(mut self, label: impl Into<String>) -> Self {
384 self.label = label.into();
385 self
386 }
387
388 pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
389 self.tooltip = Some(tooltip.into());
390 self
391 }
392
393 pub fn with_action(mut self, action: WidgetAction) -> Self {
394 self.action = Some(action);
395 self
396 }
397
398 pub fn with_cursor(mut self, cursor: MouseCursor) -> Self {
399 self.cursor = Some(cursor);
400 self
401 }
402
403 pub fn with_z_index(mut self, z_index: i16) -> Self {
404 self.z_index = z_index;
405 self
406 }
407
408 pub fn with_row(mut self, row: usize) -> Self {
409 self.row = Some(row);
410 self
411 }
412
413 pub fn with_column(mut self, column: usize) -> Self {
414 self.column = Some(column);
415 self
416 }
417
418 pub fn with_selection_group(mut self, group: impl Into<SelectionGroup>) -> Self {
419 self.selection_group = Some(group.into());
420 self
421 }
422
423 pub fn with_description(mut self, description: impl Into<String>) -> Self {
424 self.description = Some(description.into());
425 self
426 }
427
428 pub fn with_shortcut(mut self, shortcut: impl Into<String>) -> Self {
429 self.shortcut = Some(shortcut.into());
430 self
431 }
432
433 pub fn with_state(mut self, state: WidgetState) -> Self {
434 self.state = state;
435 self
436 }
437
438 pub fn with_value(mut self, value: WidgetValue) -> Self {
439 self.value = Some(value);
440 self
441 }
442
443 pub fn into_region(self, area: Rect) -> HitRegion {
444 HitRegion {
445 id: self.id,
446 area,
447 role: self.role,
448 label: self.label,
449 tooltip: self.tooltip,
450 action: self.action,
451 cursor: self.cursor,
452 z_index: self.z_index,
453 row: self.row,
454 column: self.column,
455 selection_group: self.selection_group,
456 description: self.description,
457 shortcut: self.shortcut,
458 state: self.state,
459 value: self.value,
460 }
461 }
462}
463
464impl Default for WidgetId {
465 fn default() -> Self {
466 Self(String::new())
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
471pub struct TextRange {
472 pub line: usize,
473 pub start_col: usize,
474 pub end_col: usize,
475}
476
477impl TextRange {
478 pub const fn new(line: usize, start_col: usize, end_col: usize) -> Self {
479 Self {
480 line,
481 start_col,
482 end_col,
483 }
484 }
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Hash)]
488pub enum CopyTransform {
489 PlainText,
490 MarkdownSource,
491 CodeOnly,
492 Custom(String),
493}
494
495#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct SelectableSpan {
497 pub id: SelectionId,
498 pub text: String,
499 pub source_range: Range<usize>,
500 pub screen_area: Rect,
501 pub group: Option<SelectionGroup>,
502 pub source_id: WidgetId,
503 pub logical_range: TextRange,
504 pub copyable: bool,
505 pub copy_transform: Option<CopyTransform>,
506}
507
508impl SelectableSpan {
509 pub fn new(
510 id: impl Into<SelectionId>,
511 text: impl Into<String>,
512 source_range: Range<usize>,
513 screen_area: Rect,
514 ) -> Self {
515 Self {
516 id: id.into(),
517 text: text.into(),
518 source_range,
519 screen_area,
520 group: None,
521 source_id: WidgetId::default(),
522 logical_range: TextRange::new(0, 0, 0),
523 copyable: true,
524 copy_transform: None,
525 }
526 }
527
528 pub fn from_logical(
529 id: impl Into<SelectionId>,
530 source_id: impl Into<WidgetId>,
531 screen_area: Rect,
532 logical_range: TextRange,
533 text: impl Into<String>,
534 ) -> Self {
535 let text = text.into();
536 let end = text.len();
537 Self {
538 id: id.into(),
539 text,
540 source_range: 0..end,
541 screen_area,
542 group: None,
543 source_id: source_id.into(),
544 logical_range,
545 copyable: true,
546 copy_transform: None,
547 }
548 }
549
550 pub fn with_group(mut self, group: impl Into<SelectionGroup>) -> Self {
551 self.group = Some(group.into());
552 self
553 }
554
555 pub fn with_source_id(mut self, source_id: impl Into<WidgetId>) -> Self {
556 self.source_id = source_id.into();
557 self
558 }
559
560 pub fn with_logical_range(mut self, range: TextRange) -> Self {
561 self.logical_range = range;
562 self
563 }
564
565 pub fn with_copyable(mut self, copyable: bool) -> Self {
566 self.copyable = copyable;
567 self
568 }
569
570 pub fn with_copy_transform(mut self, transform: CopyTransform) -> Self {
571 self.copy_transform = Some(transform);
572 self
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
577pub struct ScrollRowHit {
578 pub row_id: WidgetId,
579 pub logical_row: usize,
580 pub source_line: Option<usize>,
581 pub span_id: Option<SelectionId>,
582 pub item_id: Option<WidgetId>,
583 pub wrapped_continuation: bool,
584}
585
586impl ScrollRowHit {
587 pub fn new(row_id: impl Into<WidgetId>, logical_row: usize) -> Self {
588 Self {
589 row_id: row_id.into(),
590 logical_row,
591 source_line: None,
592 span_id: None,
593 item_id: None,
594 wrapped_continuation: false,
595 }
596 }
597
598 pub fn with_source_line(mut self, source_line: usize) -> Self {
599 self.source_line = Some(source_line);
600 self
601 }
602
603 pub fn with_span_id(mut self, span_id: impl Into<SelectionId>) -> Self {
604 self.span_id = Some(span_id.into());
605 self
606 }
607
608 pub fn with_item_id(mut self, item_id: impl Into<WidgetId>) -> Self {
609 self.item_id = Some(item_id.into());
610 self
611 }
612
613 pub fn with_wrapped_continuation(mut self, continuation: bool) -> Self {
614 self.wrapped_continuation = continuation;
615 self
616 }
617}
618
619#[derive(Debug, Clone, PartialEq, Eq)]
620pub struct ScrollHitRegion {
621 pub id: WidgetId,
622 pub viewport: Rect,
623 pub scroll_offset: usize,
624 pub rows: Vec<ScrollRowHit>,
625}
626
627#[derive(Debug, Clone, PartialEq, Eq)]
628pub struct LogicalHit {
629 pub region_id: WidgetId,
630 pub row_id: WidgetId,
631 pub logical_row: usize,
632 pub source_line: Option<usize>,
633 pub span_id: Option<SelectionId>,
634 pub item_id: Option<WidgetId>,
635 pub wrapped_continuation: bool,
636 pub screen_row: usize,
637 pub local: Position,
638 pub global: Position,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Default)]
642pub struct InteractionLayer {
643 pub regions: Vec<HitRegion>,
644 pub selectable_spans: Vec<SelectableSpan>,
645 pub scroll_regions: Vec<ScrollHitRegion>,
646}
647
648impl InteractionLayer {
649 pub fn new() -> Self {
650 Self::default()
651 }
652
653 pub fn clear(&mut self) {
654 self.regions.clear();
655 self.selectable_spans.clear();
656 self.scroll_regions.clear();
657 }
658
659 pub fn push_region(&mut self, region: HitRegion) -> &HitRegion {
660 self.regions.push(region);
661 self.regions.last().expect("region was just pushed")
662 }
663
664 pub fn push_metadata_region(&mut self, area: Rect, metadata: WidgetMetadata) -> &HitRegion {
665 self.push_region(metadata.into_region(area))
666 }
667
668 pub fn push_cell(&mut self, x: u16, y: u16, metadata: WidgetMetadata) -> &HitRegion {
669 self.push_metadata_region(Rect::new(x, y, 1, 1), metadata)
670 }
671
672 pub fn push_selectable_span(&mut self, span: SelectableSpan) -> &SelectableSpan {
673 self.selectable_spans.push(span);
674 self.selectable_spans
675 .last()
676 .expect("selectable span was just pushed")
677 }
678
679 pub fn push_scroll_region(
680 &mut self,
681 id: impl Into<WidgetId>,
682 viewport: Rect,
683 scroll_offset: usize,
684 rows: Vec<ScrollRowHit>,
685 ) -> &ScrollHitRegion {
686 self.scroll_regions.push(ScrollHitRegion {
687 id: id.into(),
688 viewport,
689 scroll_offset,
690 rows,
691 });
692 self.scroll_regions
693 .last()
694 .expect("scroll region was just pushed")
695 }
696
697 pub fn hit_test(&self, x: u16, y: u16) -> Option<&HitRegion> {
698 self.regions
699 .iter()
700 .enumerate()
701 .filter(|(_, region)| region.contains(x, y))
702 .max_by_key(|(index, region)| (region.z_index, *index))
703 .map(|(_, region)| region)
704 }
705
706 pub fn hit_test_position(&self, position: Position) -> Option<&HitRegion> {
707 self.hit_test(position.x, position.y)
708 }
709
710 pub fn region_by_id(&self, id: &WidgetId) -> Option<&HitRegion> {
711 self.regions.iter().rev().find(|region| ®ion.id == id)
712 }
713
714 pub fn regions_for_role(&self, role: WidgetRole) -> impl Iterator<Item = &HitRegion> {
715 self.regions
716 .iter()
717 .filter(move |region| region.role == role)
718 }
719
720 pub fn dirty_regions_for_ids<'a>(
721 &'a self,
722 ids: impl IntoIterator<Item = &'a WidgetId>,
723 ) -> Vec<Rect> {
724 ids.into_iter()
725 .filter_map(|id| self.region_by_id(id).map(|region| region.area))
726 .collect()
727 }
728
729 pub fn selectable_at(&self, x: u16, y: u16) -> Option<(&SelectableSpan, SelectionPoint)> {
730 self.selectable_spans
731 .iter()
732 .enumerate()
733 .filter(|(_, span)| span.copyable && rect_contains(span.screen_area, x, y))
734 .max_by_key(|(index, _)| *index)
735 .map(|(_, span)| {
736 let local_col = x.saturating_sub(span.screen_area.x) as usize;
737 let col = span
738 .logical_range
739 .start_col
740 .saturating_add(local_col)
741 .min(span.logical_range.end_col);
742 let source_offset = span.source_range.start.saturating_add(local_col);
743 (
744 span,
745 SelectionPoint {
746 source_id: span.source_id.clone(),
747 group: span.group.clone(),
748 line: span.logical_range.line,
749 column: col,
750 source_offset,
751 },
752 )
753 })
754 }
755
756 pub fn scroll_hit_test(&self, x: u16, y: u16) -> Option<LogicalHit> {
757 self.scroll_regions.iter().rev().find_map(|region| {
758 if !rect_contains(region.viewport, x, y) {
759 return None;
760 }
761 let screen_row = y.saturating_sub(region.viewport.y) as usize;
762 region.rows.get(screen_row).map(|row| LogicalHit {
763 region_id: region.id.clone(),
764 row_id: row.row_id.clone(),
765 logical_row: row.logical_row,
766 source_line: row.source_line,
767 span_id: row.span_id.clone(),
768 item_id: row.item_id.clone(),
769 wrapped_continuation: row.wrapped_continuation,
770 screen_row,
771 local: Position::new(x, y).local_to(region.viewport),
772 global: Position::new(x, y),
773 })
774 })
775 }
776
777 pub fn plain_text_for_source(&self, source_id: &WidgetId) -> String {
778 self.collect_plain_text(|span| &span.source_id == source_id)
779 }
780
781 pub fn plain_text_for_group(&self, group: &SelectionGroup) -> String {
782 self.collect_plain_text(|span| span.group.as_ref() == Some(group))
783 }
784
785 pub fn selected_plain_text(&self, range: &SelectionRange) -> String {
786 let mut lines: BTreeMap<usize, String> = BTreeMap::new();
787 for span in self.selectable_spans.iter().filter(|span| {
788 span.copyable && span.source_id == range.source_id && span.group == range.group
789 }) {
790 if span.logical_range.line < range.start.line
791 || span.logical_range.line > range.end.line
792 {
793 continue;
794 }
795 let start_col = if span.logical_range.line == range.start.line {
796 range.start.column
797 } else {
798 0
799 };
800 let end_col = if span.logical_range.line == range.end.line {
801 range.end.column
802 } else {
803 usize::MAX
804 };
805 let clipped_start = span.logical_range.start_col.max(start_col);
806 let clipped_end = span.logical_range.end_col.min(end_col);
807 if clipped_start >= clipped_end {
808 continue;
809 }
810 let local_start = clipped_start.saturating_sub(span.logical_range.start_col);
811 let local_end = clipped_end.saturating_sub(span.logical_range.start_col);
812 lines
813 .entry(span.logical_range.line)
814 .or_default()
815 .push_str(&slice_display_cols(&span.text, local_start, local_end));
816 }
817 lines.into_values().collect::<Vec<_>>().join("\n")
818 }
819
820 fn collect_plain_text(&self, predicate: impl Fn(&SelectableSpan) -> bool) -> String {
821 let mut spans: Vec<&SelectableSpan> = self
822 .selectable_spans
823 .iter()
824 .filter(|span| span.copyable && predicate(span))
825 .collect();
826 spans.sort_by_key(|span| {
827 (
828 span.logical_range.line,
829 span.logical_range.start_col,
830 span.source_range.start,
831 )
832 });
833
834 let mut lines: BTreeMap<usize, String> = BTreeMap::new();
835 for span in spans {
836 lines
837 .entry(span.logical_range.line)
838 .or_default()
839 .push_str(&span.text);
840 }
841 lines.into_values().collect::<Vec<_>>().join("\n")
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Hash)]
846pub struct SelectionPoint {
847 pub source_id: WidgetId,
848 pub group: Option<SelectionGroup>,
849 pub line: usize,
850 pub column: usize,
851 pub source_offset: usize,
852}
853
854impl SelectionPoint {
855 pub fn new(source_id: impl Into<WidgetId>, line: usize, column: usize) -> Self {
856 Self {
857 source_id: source_id.into(),
858 group: None,
859 line,
860 column,
861 source_offset: 0,
862 }
863 }
864
865 pub fn with_group(mut self, group: impl Into<SelectionGroup>) -> Self {
866 self.group = Some(group.into());
867 self
868 }
869
870 pub fn with_source_offset(mut self, source_offset: usize) -> Self {
871 self.source_offset = source_offset;
872 self
873 }
874}
875
876#[derive(Debug, Clone, PartialEq, Eq)]
877pub struct SelectionRange {
878 pub source_id: WidgetId,
879 pub group: Option<SelectionGroup>,
880 pub start: SelectionPoint,
881 pub end: SelectionPoint,
882}
883
884#[derive(Debug, Clone, PartialEq, Eq, Default)]
885pub struct SelectionModel {
886 pub anchor: Option<SelectionPoint>,
887 pub focus: Option<SelectionPoint>,
888 pub focused_span: Option<SelectionId>,
889}
890
891impl SelectionModel {
892 pub fn new() -> Self {
893 Self::default()
894 }
895
896 pub fn focus_row(&mut self, span_id: impl Into<SelectionId>) {
897 self.focused_span = Some(span_id.into());
898 }
899
900 pub fn start(&mut self, point: SelectionPoint) {
901 self.anchor = Some(point.clone());
902 self.focus = Some(point);
903 }
904
905 pub fn start_at(&mut self, layer: &InteractionLayer, x: u16, y: u16) -> bool {
906 if let Some((span, point)) = layer.selectable_at(x, y) {
907 self.focused_span = Some(span.id.clone());
908 self.start(point);
909 true
910 } else {
911 false
912 }
913 }
914
915 pub fn extend_to(&mut self, point: SelectionPoint) {
916 if self.anchor.is_none() {
917 self.anchor = Some(point.clone());
918 }
919 self.focus = Some(point);
920 }
921
922 pub fn shift_click_at(&mut self, layer: &InteractionLayer, x: u16, y: u16) -> bool {
923 if let Some((span, point)) = layer.selectable_at(x, y) {
924 self.focused_span = Some(span.id.clone());
925 self.extend_to(point);
926 true
927 } else {
928 false
929 }
930 }
931
932 pub fn update_drag(&mut self, point: SelectionPoint) {
933 self.focus = Some(point);
934 }
935
936 pub fn update_drag_at(&mut self, layer: &InteractionLayer, x: u16, y: u16) -> bool {
937 if let Some((span, point)) = layer.selectable_at(x, y) {
938 self.focused_span = Some(span.id.clone());
939 self.update_drag(point);
940 true
941 } else {
942 false
943 }
944 }
945
946 pub fn clear(&mut self) {
947 self.anchor = None;
948 self.focus = None;
949 self.focused_span = None;
950 }
951
952 pub fn is_active(&self) -> bool {
953 self.anchor.is_some() && self.focus.is_some()
954 }
955
956 pub fn normalized(&self) -> Option<SelectionRange> {
957 let anchor = self.anchor.as_ref()?;
958 let focus = self.focus.as_ref()?;
959 if anchor.source_id != focus.source_id || anchor.group != focus.group {
960 return None;
961 }
962 let (start, end) = if (anchor.line, anchor.column, anchor.source_offset)
963 <= (focus.line, focus.column, focus.source_offset)
964 {
965 (anchor.clone(), focus.clone())
966 } else {
967 (focus.clone(), anchor.clone())
968 };
969 Some(SelectionRange {
970 source_id: start.source_id.clone(),
971 group: start.group.clone(),
972 start,
973 end,
974 })
975 }
976
977 pub fn state(&self) -> SelectionState {
978 SelectionState {
979 active: self.is_active(),
980 anchor: self.anchor.clone(),
981 focus: self.focus.clone(),
982 focused_span: self.focused_span.clone(),
983 }
984 }
985
986 pub fn copied_plain_text(&self, layer: &InteractionLayer) -> String {
987 self.normalized()
988 .map(|range| layer.selected_plain_text(&range))
989 .unwrap_or_default()
990 }
991}
992
993#[derive(Debug, Clone, PartialEq, Eq, Default)]
994pub struct SelectionState {
995 pub active: bool,
996 pub anchor: Option<SelectionPoint>,
997 pub focus: Option<SelectionPoint>,
998 pub focused_span: Option<SelectionId>,
999}
1000
1001#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1002pub enum HoverEventKind {
1003 Enter,
1004 Leave,
1005 Move,
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq)]
1009pub struct HoverEvent {
1010 pub kind: HoverEventKind,
1011 pub id: Option<WidgetId>,
1012 pub local: Option<Position>,
1013 pub area: Option<Rect>,
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Eq, Default)]
1017pub struct HoverTransition {
1018 pub events: Vec<HoverEvent>,
1019 pub entered: Option<WidgetId>,
1020 pub left: Option<WidgetId>,
1021 pub dirty: Vec<Rect>,
1022}
1023
1024#[derive(Debug, Clone, PartialEq, Eq, Default)]
1025pub struct HoverTracker {
1026 hovered: Option<WidgetId>,
1027}
1028
1029impl HoverTracker {
1030 pub fn new() -> Self {
1031 Self::default()
1032 }
1033
1034 pub fn hovered(&self) -> Option<&WidgetId> {
1035 self.hovered.as_ref()
1036 }
1037
1038 pub fn update(&mut self, layer: &InteractionLayer, x: u16, y: u16) -> HoverTransition {
1039 let next = layer.hit_test(x, y).map(|region| region.id.clone());
1040 let previous = self.hovered.clone();
1041 if previous == next {
1042 if let Some(id) = next {
1043 let region = layer.region_by_id(&id);
1044 return HoverTransition {
1045 events: vec![HoverEvent {
1046 kind: HoverEventKind::Move,
1047 id: Some(id),
1048 local: region.map(|region| region.local_position(x, y)),
1049 area: region.map(|region| region.area),
1050 }],
1051 ..HoverTransition::default()
1052 };
1053 }
1054 return HoverTransition::default();
1055 }
1056
1057 self.hovered = next.clone();
1058 let mut transition = HoverTransition::default();
1059 if let Some(id) = previous {
1060 let region = layer.region_by_id(&id);
1061 if let Some(region) = region {
1062 transition.dirty.push(region.area);
1063 }
1064 transition.left = Some(id.clone());
1065 transition.events.push(HoverEvent {
1066 kind: HoverEventKind::Leave,
1067 id: Some(id),
1068 local: None,
1069 area: region.map(|region| region.area),
1070 });
1071 }
1072 if let Some(id) = next {
1073 let region = layer.region_by_id(&id);
1074 if let Some(region) = region {
1075 transition.dirty.push(region.area);
1076 }
1077 transition.entered = Some(id.clone());
1078 transition.events.push(HoverEvent {
1079 kind: HoverEventKind::Enter,
1080 id: Some(id),
1081 local: region.map(|region| region.local_position(x, y)),
1082 area: region.map(|region| region.area),
1083 });
1084 }
1085 transition
1086 }
1087}
1088
1089#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1090pub enum PointerEventKind {
1091 Move,
1092 Down(MouseButton),
1093 Drag(MouseButton),
1094 Up(MouseButton),
1095}
1096
1097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1098pub struct PointerEvent {
1099 pub kind: PointerEventKind,
1100 pub position: Position,
1101}
1102
1103impl PointerEvent {
1104 pub const fn new(kind: PointerEventKind, x: u16, y: u16) -> Self {
1105 Self {
1106 kind,
1107 position: Position::new(x, y),
1108 }
1109 }
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq)]
1113pub enum UiEvent {
1114 HoverEnter(WidgetId),
1115 HoverMove {
1116 id: WidgetId,
1117 local: Position,
1118 },
1119 HoverLeave(WidgetId),
1120 MouseDown {
1121 id: WidgetId,
1122 button: MouseButton,
1123 local: Position,
1124 },
1125 MouseUp {
1126 id: WidgetId,
1127 button: MouseButton,
1128 local: Position,
1129 },
1130 Click {
1131 id: WidgetId,
1132 button: MouseButton,
1133 local: Position,
1134 },
1135 DragStart {
1136 id: WidgetId,
1137 local: Position,
1138 },
1139 DragMove {
1140 id: WidgetId,
1141 local: Position,
1142 },
1143 DragEnd {
1144 id: WidgetId,
1145 local: Position,
1146 },
1147 SelectionChanged(SelectionState),
1148 Ignored,
1149}
1150
1151#[derive(Debug, Clone, PartialEq, Eq, Default)]
1152pub struct UiEventBatch {
1153 pub events: Vec<UiEvent>,
1154 pub dirty: Vec<Rect>,
1155}
1156
1157#[derive(Debug, Clone, PartialEq, Eq, Default)]
1158pub struct UiEventMapper {
1159 hover: HoverTracker,
1160 pressed: Option<(WidgetId, MouseButton)>,
1161 dragging: bool,
1162}
1163
1164impl UiEventMapper {
1165 pub fn new() -> Self {
1166 Self::default()
1167 }
1168
1169 pub fn hover(&self) -> &HoverTracker {
1170 &self.hover
1171 }
1172
1173 pub fn handle_pointer_event(
1174 &mut self,
1175 layer: &InteractionLayer,
1176 event: PointerEvent,
1177 ) -> UiEventBatch {
1178 let x = event.position.x;
1179 let y = event.position.y;
1180 let mut batch = UiEventBatch::default();
1181
1182 if matches!(event.kind, PointerEventKind::Move) {
1183 let transition = self.hover.update(layer, x, y);
1184 batch.dirty.extend(transition.dirty);
1185 for event in transition.events {
1186 match event.kind {
1187 HoverEventKind::Enter => {
1188 if let Some(id) = event.id {
1189 batch.events.push(UiEvent::HoverEnter(id));
1190 }
1191 }
1192 HoverEventKind::Leave => {
1193 if let Some(id) = event.id {
1194 batch.events.push(UiEvent::HoverLeave(id));
1195 }
1196 }
1197 HoverEventKind::Move => {
1198 if let (Some(id), Some(local)) = (event.id, event.local) {
1199 batch.events.push(UiEvent::HoverMove { id, local });
1200 }
1201 }
1202 }
1203 }
1204 return batch;
1205 }
1206
1207 let hit = layer.hit_test(x, y);
1208 match event.kind {
1209 PointerEventKind::Move => unreachable!(),
1210 PointerEventKind::Down(button) => {
1211 if let Some(region) = hit {
1212 let id = region.id.clone();
1213 let local = region.local_position(x, y);
1214 self.pressed = Some((id.clone(), button));
1215 self.dragging = false;
1216 batch.dirty.push(region.area);
1217 batch.events.push(UiEvent::MouseDown { id, button, local });
1218 } else {
1219 batch.events.push(UiEvent::Ignored);
1220 }
1221 }
1222 PointerEventKind::Drag(button) => {
1223 if let Some((id, _)) = self.pressed.clone() {
1224 if let Some(region) = layer.region_by_id(&id) {
1225 let local = region.local_position(x, y);
1226 if !self.dragging {
1227 self.dragging = true;
1228 batch.events.push(UiEvent::DragStart {
1229 id: id.clone(),
1230 local,
1231 });
1232 }
1233 batch.dirty.push(region.area);
1234 batch.events.push(UiEvent::DragMove { id, local });
1235 }
1236 } else if let Some(region) = hit {
1237 batch.events.push(UiEvent::DragMove {
1238 id: region.id.clone(),
1239 local: region.local_position(x, y),
1240 });
1241 if button == MouseButton::WheelUp || button == MouseButton::WheelDown {
1242 batch.dirty.push(region.area);
1243 }
1244 } else {
1245 batch.events.push(UiEvent::Ignored);
1246 }
1247 }
1248 PointerEventKind::Up(button) => {
1249 if let Some((pressed_id, pressed_button)) = self.pressed.take() {
1250 if let Some(region) = layer.region_by_id(&pressed_id) {
1251 let local = region.local_position(x, y);
1252 batch.events.push(UiEvent::MouseUp {
1253 id: pressed_id.clone(),
1254 button,
1255 local,
1256 });
1257 if self.dragging {
1258 batch.events.push(UiEvent::DragEnd {
1259 id: pressed_id.clone(),
1260 local,
1261 });
1262 } else if pressed_button == button && region.contains(x, y) {
1263 batch.events.push(UiEvent::Click {
1264 id: pressed_id.clone(),
1265 button,
1266 local,
1267 });
1268 }
1269 batch.dirty.push(region.area);
1270 }
1271 self.dragging = false;
1272 } else if let Some(region) = hit {
1273 batch.events.push(UiEvent::MouseUp {
1274 id: region.id.clone(),
1275 button,
1276 local: region.local_position(x, y),
1277 });
1278 } else {
1279 batch.events.push(UiEvent::Ignored);
1280 }
1281 }
1282 }
1283
1284 batch
1285 }
1286}
1287
1288fn rect_contains(rect: Rect, x: u16, y: u16) -> bool {
1289 x >= rect.x && x < rect.right() && y >= rect.y && y < rect.bottom()
1290}
1291
1292fn slice_display_cols(text: &str, start: usize, end: usize) -> String {
1293 let mut out = String::new();
1294 let mut col = 0usize;
1295 for ch in text.chars() {
1296 let width = char_display_width(ch).max(1);
1297 let next = col.saturating_add(width);
1298 if next <= start {
1299 col = next;
1300 continue;
1301 }
1302 if col >= end {
1303 break;
1304 }
1305 out.push(ch);
1306 col = next;
1307 }
1308 out
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313 use super::*;
1314
1315 #[test]
1316 fn hit_test_prefers_highest_z_then_latest_region() {
1317 let mut layer = InteractionLayer::new();
1318 layer.push_region(
1319 HitRegion::new("bottom", Rect::new(0, 0, 4, 1)).with_role(WidgetRole::Panel),
1320 );
1321 layer.push_region(
1322 HitRegion::new("top", Rect::new(1, 0, 1, 1))
1323 .with_role(WidgetRole::Button)
1324 .with_z_index(2),
1325 );
1326
1327 assert_eq!(layer.hit_test(1, 0).unwrap().id.as_ref(), "top");
1328 assert_eq!(layer.hit_test(3, 0).unwrap().id.as_ref(), "bottom");
1329 }
1330
1331 #[test]
1332 fn hover_tracker_emits_enter_leave_and_dirty_rects() {
1333 let mut layer = InteractionLayer::new();
1334 layer.push_region(HitRegion::new("a", Rect::new(0, 0, 2, 1)));
1335 layer.push_region(HitRegion::new("b", Rect::new(3, 0, 2, 1)));
1336 let mut hover = HoverTracker::new();
1337
1338 let first = hover.update(&layer, 0, 0);
1339 assert_eq!(first.entered.as_ref().map(WidgetId::as_ref), Some("a"));
1340 assert_eq!(first.dirty, vec![Rect::new(0, 0, 2, 1)]);
1341
1342 let second = hover.update(&layer, 3, 0);
1343 assert_eq!(second.left.as_ref().map(WidgetId::as_ref), Some("a"));
1344 assert_eq!(second.entered.as_ref().map(WidgetId::as_ref), Some("b"));
1345 assert_eq!(second.dirty.len(), 2);
1346 }
1347
1348 #[test]
1349 fn selection_model_extracts_plain_text() {
1350 let mut layer = InteractionLayer::new();
1351 layer.push_selectable_span(
1352 SelectableSpan::from_logical(
1353 "line0",
1354 "doc",
1355 Rect::new(0, 0, 5, 1),
1356 TextRange::new(0, 0, 5),
1357 "hello",
1358 )
1359 .with_group("doc:body"),
1360 );
1361 layer.push_selectable_span(
1362 SelectableSpan::from_logical(
1363 "line1",
1364 "doc",
1365 Rect::new(0, 1, 5, 1),
1366 TextRange::new(1, 0, 5),
1367 "world",
1368 )
1369 .with_group("doc:body"),
1370 );
1371
1372 let mut selection = SelectionModel::new();
1373 selection.start(SelectionPoint::new("doc", 0, 1).with_group("doc:body"));
1374 selection.update_drag(SelectionPoint::new("doc", 1, 3).with_group("doc:body"));
1375
1376 assert_eq!(selection.copied_plain_text(&layer), "ello\nwor");
1377 }
1378
1379 #[test]
1380 fn scroll_hit_maps_screen_row_to_logical_row() {
1381 let mut layer = InteractionLayer::new();
1382 layer.push_scroll_region(
1383 "transcript",
1384 Rect::new(0, 2, 10, 2),
1385 5,
1386 vec![
1387 ScrollRowHit::new("r5", 5),
1388 ScrollRowHit::new("r6", 6).with_source_line(12),
1389 ],
1390 );
1391
1392 let hit = layer.scroll_hit_test(3, 3).unwrap();
1393 assert_eq!(hit.row_id.as_ref(), "r6");
1394 assert_eq!(hit.logical_row, 6);
1395 assert_eq!(hit.source_line, Some(12));
1396 assert_eq!(hit.screen_row, 1);
1397 assert_eq!(hit.local, Position::new(3, 1));
1398 }
1399
1400 #[test]
1401 fn ui_event_mapper_outputs_local_click_coordinates() {
1402 let mut layer = InteractionLayer::new();
1403 layer.push_region(HitRegion::new("button", Rect::new(5, 2, 4, 2)));
1404 let mut mapper = UiEventMapper::new();
1405
1406 let down = mapper.handle_pointer_event(
1407 &layer,
1408 PointerEvent::new(PointerEventKind::Down(MouseButton::Left), 6, 3),
1409 );
1410 assert!(matches!(down.events[0], UiEvent::MouseDown { .. }));
1411
1412 let up = mapper.handle_pointer_event(
1413 &layer,
1414 PointerEvent::new(PointerEventKind::Up(MouseButton::Left), 6, 3),
1415 );
1416 assert!(up.events.iter().any(|event| matches!(
1417 event,
1418 UiEvent::Click { id, local, .. } if id.as_ref() == "button" && *local == Position::new(1, 1)
1419 )));
1420 }
1421}