Skip to content

Commit 6128c23

Browse files
committed
Implement text selection
1 parent dc37c75 commit 6128c23

File tree

4 files changed

+191
-46
lines changed

4 files changed

+191
-46
lines changed

theterminal/src/terminal_screen.rs

Lines changed: 152 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
mod cell_coordinates;
12
mod color_scheme;
23
pub mod events;
34
mod keyboard;
45
mod run_calculator;
56

6-
use crate::actions::PasteAction;
7+
use crate::actions::{CopyAction, PasteAction};
8+
use crate::terminal_screen::cell_coordinates::CellCoordinates;
79
use crate::terminal_screen::color_scheme::ColorScheme;
810
use crate::terminal_screen::events::{TerminalScreenCloseEvent, TerminalScreenEvents};
911
use crate::terminal_screen::keyboard::Keyboard;
@@ -15,14 +17,16 @@ use contemporary::components::dialog_box::{StandardButton, dialog_box};
1517
use contemporary::platform_support::cx_platform_extensions::CxPlatformExtensions;
1618
use gpui::prelude::FluentBuilder;
1719
use gpui::{
18-
App, AppContext, AsyncApp, BorderStyle, Bounds, Context, Corners, CursorStyle, Entity,
19-
EntityInputHandler, FocusHandle, Focusable, Hitbox, HitboxBehavior, Hsla, InteractiveElement,
20-
IntoElement, KeyBinding, KeyDownEvent, ParentElement, Pixels, Point, Refineable, Render,
21-
ScrollDelta, ScrollWheelEvent, Style, StyleRefinement, Styled, TextAlign, UTF16Selection,
22-
Window, WrappedLine, actions, canvas, div, point, px, quad, rgb, size, transparent_black,
20+
App, AppContext, AsyncApp, BorderStyle, Bounds, ClipboardItem, Context, Corners, CursorStyle,
21+
DispatchPhase, Entity, EntityInputHandler, FocusHandle, Focusable, Hitbox, HitboxBehavior,
22+
Hsla, InteractiveElement, IntoElement, KeyBinding, KeyDownEvent, MouseDownEvent,
23+
MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Refineable, Render, ScrollDelta,
24+
ScrollWheelEvent, Style, StyleRefinement, Styled, TextAlign, UTF16Selection, Window,
25+
WrappedLine, actions, canvas, div, point, px, quad, rgb, size, transparent_black,
2326
};
2427
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
2528
use std::cell::RefCell;
29+
use std::cmp::Ordering;
2630
use std::io::{Read, Write};
2731
use std::ops::{Range, Rem};
2832
use std::rc::Rc;
@@ -57,12 +61,15 @@ pub struct TerminalScreen {
5761
timer: Instant,
5862
close_warning_dialog: Option<Vec<String>>,
5963
partial_scroll: f32,
64+
clicked_cell_coordinates: Option<CellCoordinates>,
65+
selected_cell_coordinates: Option<Range<CellCoordinates>>,
6066
}
6167

6268
pub struct TerminalScreenPrepaint {
6369
screen: Screen,
6470
screen_hitbox: Hitbox,
6571
screen_lines: Vec<Vec<WrappedLine>>,
72+
cell_hitboxes: Vec<(CellCoordinates, Hitbox)>,
6673
style: Style,
6774
background: Hsla,
6875
caret_rect: Bounds<Pixels>,
@@ -208,6 +215,8 @@ impl TerminalScreen {
208215
timer: Instant::now(),
209216
close_warning_dialog: None,
210217
partial_scroll: 0.,
218+
clicked_cell_coordinates: None,
219+
selected_cell_coordinates: None,
211220
}
212221
})
213222
}
@@ -218,6 +227,39 @@ impl TerminalScreen {
218227

219228
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {}
220229

230+
pub fn copy(&mut self, _: &CopyAction, window: &mut Window, cx: &mut Context<Self>) {
231+
let Some(selected_cell_coordinates) = self.selected_cell_coordinates.as_ref() else {
232+
return;
233+
};
234+
235+
let screen = self.screen.read(cx);
236+
let screen = screen.screen();
237+
let mut copied_string = String::new();
238+
for line in 0..screen.size().0 {
239+
for column in 0..screen.size().1 {
240+
let cell = screen.cell(line, column);
241+
let coordinates = CellCoordinates(line, column);
242+
if selected_cell_coordinates.contains(&coordinates) {
243+
if column == 0 {
244+
copied_string += "\n";
245+
}
246+
247+
copied_string += cell
248+
.map(|cell| {
249+
if cell.has_contents() {
250+
cell.contents()
251+
} else {
252+
" "
253+
}
254+
})
255+
.unwrap_or_default()
256+
}
257+
}
258+
}
259+
260+
cx.write_to_clipboard(ClipboardItem::new_string(copied_string.trim().to_string()));
261+
}
262+
221263
pub fn paste(&mut self, _: &PasteAction, window: &mut Window, cx: &mut Context<Self>) {
222264
if let Some(clipboard_contents) = cx
223265
.read_from_clipboard()
@@ -366,18 +408,15 @@ impl TerminalScreen {
366408

367409
impl Render for TerminalScreen {
368410
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
369-
let screen = self.screen.clone();
370-
let screen_size = self.screen_size.clone();
371-
let style = self.style.clone();
372-
let color_scheme = self.color_scheme;
373-
let timer = self.timer;
374-
375411
div()
376412
.h_full()
377413
.w_full()
378414
.key_context("TerminalScreen")
379415
.track_focus(&self.focus_handle(cx))
380416
.on_action(cx.listener(Self::delete))
417+
.when(self.selected_cell_coordinates.is_some(), |div| {
418+
div.on_action(cx.listener(Self::copy))
419+
})
381420
.on_action(cx.listener(Self::paste))
382421
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
383422
this.process_key_press(event, window, cx)
@@ -386,21 +425,14 @@ impl Render for TerminalScreen {
386425
this.process_scroll(event, window, cx)
387426
}))
388427
.child(
389-
canvas(
390-
move |bounds, window, cx| {
391-
prepaint_terminal_screen(
392-
screen,
393-
screen_size,
394-
style,
395-
color_scheme,
396-
timer,
397-
bounds,
398-
window,
399-
cx,
400-
)
401-
},
402-
paint_terminal_screen,
403-
)
428+
canvas(cx.processor(prepaint_terminal_screen), {
429+
let entity = cx.entity();
430+
move |bounds, prepaint_state, window, cx| {
431+
entity.update(cx, |_, cx| {
432+
paint_terminal_screen(bounds, prepaint_state, window, cx)
433+
})
434+
}
435+
})
404436
.w_full()
405437
.h_full(),
406438
)
@@ -536,15 +568,18 @@ impl Styled for TerminalScreen {
536568
}
537569

538570
fn prepaint_terminal_screen(
539-
parser_entity: Entity<Parser<TerminalScreenCallbacks>>,
540-
screen_size: Entity<ScreenSize>,
541-
style_refinement: StyleRefinement,
542-
color_scheme: ColorScheme,
543-
timer: Instant,
571+
terminal_screen: &mut TerminalScreen,
544572
bounds: Bounds<Pixels>,
545573
window: &mut Window,
546-
cx: &mut App,
574+
cx: &mut Context<TerminalScreen>,
547575
) -> TerminalScreenPrepaint {
576+
let parser_entity = terminal_screen.screen.clone();
577+
let screen_size = terminal_screen.screen_size.clone();
578+
let style_refinement = terminal_screen.style.clone();
579+
let color_scheme = terminal_screen.color_scheme;
580+
let timer = terminal_screen.timer;
581+
let selected_cell_coordinates = &terminal_screen.selected_cell_coordinates;
582+
548583
let screen_hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
549584

550585
let style = Style::default().refined(style_refinement);
@@ -554,7 +589,7 @@ fn prepaint_terminal_screen(
554589
.with_timer(timer)
555590
.reverse_when(screen.reverse_video());
556591

557-
let (screen_lines, caret_rect) =
592+
let (screen_lines, caret_rect, cell_hitboxes) =
558593
window.with_text_style(style.text_style().cloned(), |window| {
559594
let text_style = window.text_style();
560595
let line_height = text_style.line_height_in_pixels(window.rem_size());
@@ -578,7 +613,7 @@ fn prepaint_terminal_screen(
578613

579614
if *screen_size.read(cx) != new_screen_size {
580615
let screen_size = screen_size.clone();
581-
cx.spawn(async move |cx: &mut AsyncApp| {
616+
cx.spawn(async move |_, cx: &mut AsyncApp| {
582617
cx.update_entity(&screen_size, |screen_size, cx| {
583618
screen_size.columns = new_screen_size.columns.max(1);
584619
screen_size.lines = new_screen_size.lines.max(1);
@@ -590,6 +625,7 @@ fn prepaint_terminal_screen(
590625
}
591626

592627
let mut screen_lines = Vec::new();
628+
let mut cell_hitboxes = Vec::new();
593629
for line in 0..screen.size().0 {
594630
let mut run_calculator = RunCalculator::new(
595631
window.text_system().clone(),
@@ -600,7 +636,26 @@ fn prepaint_terminal_screen(
600636

601637
for column in 0..screen.size().1 {
602638
let cell = screen.cell(line, column).cloned();
603-
run_calculator.push_cell(cell);
639+
let cell_bounds = Bounds {
640+
origin: point(
641+
column as f32 * character_size.width,
642+
line as f32 * character_size.height,
643+
) + bounds.origin,
644+
size: character_size,
645+
};
646+
647+
let coordinates = CellCoordinates(line, column);
648+
649+
let hitbox = window.insert_hitbox(cell_bounds, HitboxBehavior::Normal);
650+
cell_hitboxes.push((coordinates, hitbox));
651+
run_calculator.push_cell(
652+
cell,
653+
selected_cell_coordinates
654+
.as_ref()
655+
.is_some_and(|selected_coordinates| {
656+
selected_coordinates.contains(&coordinates)
657+
}),
658+
);
604659
}
605660

606661
screen_lines.push(run_calculator.runs());
@@ -615,7 +670,7 @@ fn prepaint_terminal_screen(
615670
size: size(px(1.), character_size.height),
616671
};
617672

618-
(screen_lines, caret_rect)
673+
(screen_lines, caret_rect, cell_hitboxes)
619674
});
620675

621676
let caret_color = color_scheme.parse_color(screen.fgcolor(), color_scheme.foreground, true);
@@ -627,6 +682,7 @@ fn prepaint_terminal_screen(
627682
screen,
628683
screen_hitbox,
629684
style,
685+
cell_hitboxes,
630686
screen_lines,
631687
background: color_scheme.background,
632688
caret_rect,
@@ -638,7 +694,7 @@ fn paint_terminal_screen(
638694
bounds: Bounds<Pixels>,
639695
prepaint_state: TerminalScreenPrepaint,
640696
window: &mut Window,
641-
cx: &mut App,
697+
cx: &mut Context<TerminalScreen>,
642698
) {
643699
window.paint_quad(quad(
644700
bounds,
@@ -695,6 +751,64 @@ fn paint_terminal_screen(
695751
}
696752
});
697753

754+
let entity = cx.entity();
755+
let cell_hitboxes = prepaint_state.cell_hitboxes.clone();
756+
window.on_mouse_event(move |event: &MouseDownEvent, dispatch_phase, window, cx| {
757+
if dispatch_phase != DispatchPhase::Bubble {
758+
return;
759+
}
760+
761+
let cell_hitboxes = cell_hitboxes.clone();
762+
763+
entity.update(cx, move |terminal_screen, cx| {
764+
for (cell, hitbox) in cell_hitboxes {
765+
if hitbox.is_hovered(window) {
766+
terminal_screen.clicked_cell_coordinates = Some(cell);
767+
terminal_screen.selected_cell_coordinates = None;
768+
}
769+
}
770+
cx.notify()
771+
})
772+
});
773+
774+
let entity_2 = cx.entity();
775+
let cell_hitboxes_2 = prepaint_state.cell_hitboxes.clone();
776+
window.on_mouse_event(move |event: &MouseMoveEvent, dispatch_phase, window, cx| {
777+
if dispatch_phase != DispatchPhase::Bubble {
778+
return;
779+
}
780+
781+
let cell_hitboxes = cell_hitboxes_2.clone();
782+
783+
entity_2.update(cx, move |terminal_screen, cx| {
784+
if let Some(clicked_cell_coordinates) = terminal_screen.clicked_cell_coordinates {
785+
for (cell, hitbox) in cell_hitboxes {
786+
if hitbox.is_hovered(window) {
787+
if clicked_cell_coordinates < cell {
788+
terminal_screen.selected_cell_coordinates =
789+
Some(clicked_cell_coordinates..cell);
790+
} else {
791+
terminal_screen.selected_cell_coordinates =
792+
Some(cell..clicked_cell_coordinates);
793+
}
794+
}
795+
}
796+
cx.notify()
797+
}
798+
})
799+
});
800+
801+
let entity_3 = cx.entity();
802+
window.on_mouse_event(move |event: &MouseUpEvent, dispatch_phase, window, cx| {
803+
if dispatch_phase != DispatchPhase::Bubble {
804+
return;
805+
}
806+
807+
entity_3.update(cx, move |terminal_screen, cx| {
808+
terminal_screen.clicked_cell_coordinates = None;
809+
})
810+
});
811+
698812
window.paint_quad(quad(
699813
prepaint_state.caret_rect,
700814
Corners::all(px(0.)),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use std::cmp::Ordering;
2+
use std::cmp::Ordering::{Equal, Greater, Less};
3+
4+
/// Coordinates of a cell in the terminal screen.
5+
/// First argument is the line, second argument is the column
6+
#[derive(Copy, Clone)]
7+
pub struct CellCoordinates(pub u16, pub u16);
8+
9+
impl PartialEq<CellCoordinates> for CellCoordinates {
10+
fn eq(&self, other: &CellCoordinates) -> bool {
11+
self.0 == other.0 && self.1 == other.1
12+
}
13+
}
14+
15+
impl PartialOrd<CellCoordinates> for CellCoordinates {
16+
fn partial_cmp(&self, other: &CellCoordinates) -> Option<Ordering> {
17+
match self.0.partial_cmp(&other.0) {
18+
None => None,
19+
Some(Less) => Some(Less),
20+
Some(Equal) => self.1.partial_cmp(&other.1),
21+
Some(Greater) => Some(Greater),
22+
}
23+
}
24+
}

theterminal/src/terminal_screen/run_calculator.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ impl RunCalculator {
3636
}
3737
}
3838

39-
pub fn push_cell(&mut self, cell: Option<Cell>) {
39+
pub fn push_cell(&mut self, cell: Option<Cell>, selected: bool) {
4040
if let Some(cell) = cell {
41-
let attributes = cell.clone().into();
41+
let attributes = CellAttributes::from(cell.clone()).inverse_when(selected);
4242
if self.current_cell_attributes != attributes {
4343
self.finalise_run();
4444
self.current_string = String::new();
@@ -135,6 +135,13 @@ impl CellAttributes {
135135
..TextStyleRefinement::default()
136136
}
137137
}
138+
139+
pub fn inverse_when(mut self, condition: bool) -> Self {
140+
if condition {
141+
self.inverse = !self.inverse;
142+
}
143+
self
144+
}
138145
}
139146

140147
impl From<Cell> for CellAttributes {

0 commit comments

Comments
 (0)