Skip to content

Commit 8a99f79

Browse files
committed
feat(record): support mouse click to select
1 parent 93bb129 commit 8a99f79

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

scm-record/src/render.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ pub(crate) struct RectSize {
2121
pub height: usize,
2222
}
2323

24+
impl RectSize {
25+
pub fn area(self) -> usize {
26+
self.width * self.height
27+
}
28+
}
29+
2430
impl From<tui::layout::Rect> for RectSize {
2531
fn from(rect: tui::layout::Rect) -> Self {
2632
Rect::from(rect).into()
@@ -67,6 +73,14 @@ impl From<tui::layout::Rect> for Rect {
6773
}
6874

6975
impl Rect {
76+
/// The size of the `Rect`. (To get the area as a scalar, call `.size().area()`.)
77+
pub fn size(self) -> RectSize {
78+
RectSize {
79+
width: self.width,
80+
height: self.height,
81+
}
82+
}
83+
7084
/// The (x, y) coordinate of the top-left corner of this `Rect`.
7185
fn top_left(self) -> (isize, isize) {
7286
(self.x, self.y)
@@ -80,6 +94,13 @@ impl Rect {
8094
)
8195
}
8296

97+
/// Whether or not this `Rect` contains the given point.
98+
pub fn contains_point(self, x: isize, y: isize) -> bool {
99+
let (x1, y1) = self.top_left();
100+
let (x2, y2) = self.bottom_right();
101+
x1 <= x && x < x2 && y1 <= y && y < y2
102+
}
103+
83104
/// Whether this `Rect` has zero area.
84105
pub fn is_empty(self) -> bool {
85106
self.width == 0 || self.height == 0

scm-record/src/ui.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::{fs, io, panic};
1414

1515
use crossterm::event::{
1616
DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
17-
MouseEvent, MouseEventKind,
17+
MouseButton, MouseEvent, MouseEventKind,
1818
};
1919
use crossterm::terminal::{
2020
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, EnterAlternateScreen,
@@ -68,6 +68,12 @@ enum SelectionKey {
6868
Line(LineKey),
6969
}
7070

71+
impl Default for SelectionKey {
72+
fn default() -> Self {
73+
Self::None
74+
}
75+
}
76+
7177
/// A copy of the contents of the screen at a certain point in time.
7278
#[derive(Clone, Debug, Default, Eq, PartialEq)]
7379
pub struct TestingScreenshot {
@@ -116,6 +122,7 @@ pub enum Event {
116122
FocusOuter,
117123
ToggleItem,
118124
ToggleItemAndAdvance,
125+
Click { row: usize, column: usize },
119126
}
120127

121128
impl From<crossterm::event::Event> for Event {
@@ -252,6 +259,16 @@ impl From<crossterm::event::Event> for Event {
252259
state: _,
253260
}) => Self::ToggleItemAndAdvance,
254261

262+
Event::Mouse(MouseEvent {
263+
kind: MouseEventKind::Down(MouseButton::Left),
264+
column,
265+
row,
266+
modifiers: _,
267+
}) => Self::Click {
268+
row: row.into(),
269+
column: column.into(),
270+
},
271+
255272
_event => Self::None,
256273
}
257274
}
@@ -763,6 +780,11 @@ impl<'a> Recorder<'a> {
763780
let advanced_key = self.advance_to_next_of_kind();
764781
StateUpdate::ToggleItemAndAdvance(self.selection_key, advanced_key)
765782
}
783+
784+
(_, Event::Click { row, column }) => {
785+
let component_id = self.find_component_at(drawn_rects, row, column);
786+
self.click_component(component_id)
787+
}
766788
};
767789
Ok(state_update)
768790
}
@@ -994,6 +1016,37 @@ impl<'a> Recorder<'a> {
9941016
}
9951017
}
9961018

1019+
fn find_component_at(
1020+
&self,
1021+
drawn_rects: &HashMap<ComponentId, Rect>,
1022+
row: usize,
1023+
column: usize,
1024+
) -> ComponentId {
1025+
let x = column.unwrap_isize();
1026+
let y = row.unwrap_isize() + self.scroll_offset_y;
1027+
drawn_rects
1028+
.iter()
1029+
.filter(|(_id, rect)| rect.contains_point(x, y))
1030+
.min_by(|(_, lhs), (_, rhs)| lhs.size().area().cmp(&rhs.size().area()))
1031+
.map(|(id, _rect)| id.clone())
1032+
.unwrap_or(ComponentId::App)
1033+
}
1034+
1035+
fn click_component(&self, component_id: ComponentId) -> StateUpdate {
1036+
match component_id {
1037+
ComponentId::App | ComponentId::QuitDialog => StateUpdate::None,
1038+
ComponentId::SelectableItem(selection_key) => StateUpdate::SelectItem(selection_key),
1039+
ComponentId::TristateBox => {
1040+
// TODO: implement toggling the checkbox
1041+
StateUpdate::None
1042+
}
1043+
ComponentId::QuitDialogButton(QuitDialogButtonId::GoBack) => {
1044+
StateUpdate::SetQuitDialog(None)
1045+
}
1046+
ComponentId::QuitDialogButton(QuitDialogButtonId::Quit) => StateUpdate::QuitCancel,
1047+
}
1048+
}
1049+
9971050
fn toggle_item(&mut self, selection: SelectionKey) -> Result<(), RecordError> {
9981051
match selection {
9991052
SelectionKey::None => {}

scm-record/tests/test_scm_record.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,3 +789,53 @@ fn test_state_binary_selected_contents() -> eyre::Result<()> {
789789

790790
Ok(())
791791
}
792+
793+
#[test]
794+
fn test_mouse_support() -> eyre::Result<()> {
795+
let state = example_contents();
796+
797+
let initial = TestingScreenshot::default();
798+
let first_click = TestingScreenshot::default();
799+
let click_scrolled_item = TestingScreenshot::default();
800+
let event_source = EventSource::testing(
801+
80,
802+
6,
803+
[
804+
initial.event(),
805+
Event::Click { row: 5, column: 8 },
806+
first_click.event(),
807+
Event::Click { row: 5, column: 8 },
808+
click_scrolled_item.event(),
809+
Event::QuitAccept,
810+
],
811+
);
812+
let recorder = Recorder::new(state, event_source);
813+
recorder.run()?;
814+
815+
insta::assert_display_snapshot!(initial, @r###"
816+
"(~) foo/bar "
817+
" ⋮ "
818+
" 18 this is some text "
819+
" 19 this is some text "
820+
" 20 this is some text "
821+
" [~] Section 1/1 "
822+
"###);
823+
insta::assert_display_snapshot!(first_click, @r###"
824+
" 20 this is some text "
825+
" (~) Section 1/1 "
826+
" [×] - before text 1 "
827+
" [×] - before text 2 "
828+
" [×] + after text 1 "
829+
" [ ] + after text 2 "
830+
"###);
831+
insta::assert_display_snapshot!(click_scrolled_item, @r###"
832+
" 20 this is some text "
833+
" [~] Section 1/1 "
834+
" [×] - before text 1 "
835+
" [×] - before text 2 "
836+
" [×] + after text 1 "
837+
" ( ) + after text 2 "
838+
"###);
839+
840+
Ok(())
841+
}

0 commit comments

Comments
 (0)