Add a simple context menu into terminal2 (#3343)

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2023-11-16 16:41:27 +02:00 committed by GitHub
commit 2aa7c6f2b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 98 deletions

1
Cargo.lock generated
View File

@ -10132,6 +10132,7 @@ dependencies = [
"chrono",
"gpui2",
"itertools 0.11.0",
"menu2",
"rand 0.8.5",
"serde",
"settings2",

View File

@ -9,7 +9,6 @@ path = "src/terminal_view.rs"
doctest = false
[dependencies]
# context_menu = { package = "context_menu2", path = "../context_menu2" }
editor = { package = "editor2", path = "../editor2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }

View File

@ -1,8 +1,8 @@
// use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
// use gpui::{
// AnyElement, AppContext, Bounds, Component, Element, HighlightStyle, Hsla, LayoutId, Line,
// ModelContext, MouseButton, Pixels, Point, TextStyle, Underline, ViewContext, WeakModel,
// WindowContext,
// point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
// FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
// Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
// };
// use itertools::Itertools;
// use language::CursorShape;
@ -23,6 +23,7 @@
// TerminalSize,
// };
// use theme::ThemeSettings;
// use workspace::ElementId;
// use std::mem;
// use std::{fmt::Debug, ops::RangeInclusive};
@ -130,23 +131,24 @@
// cx: &mut ViewContext<TerminalView>,
// ) {
// let position = {
// let point = self.point;
// vec2f(
// (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
// origin.y() + point.line as f32 * layout.size.line_height,
// let alac_point = self.point;
// point(
// (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
// origin.y + alac_point.line as f32 * layout.size.line_height,
// )
// };
// let size = vec2f(
// let size = point(
// (layout.size.cell_width * self.num_of_cells as f32).ceil(),
// layout.size.line_height,
// );
// )
// .into();
// cx.paint_quad(
// Bounds::new(position, size),
// Default::default(),
// self.color,
// Default::default(),
// Default::default(),
// transparent_black(),
// );
// }
// }
@ -281,9 +283,9 @@
// cursor_point: DisplayCursor,
// size: TerminalSize,
// text_fragment: &Line,
// ) -> Option<(Vector2F, f32)> {
// ) -> Option<(Point<Pixels>, Pixels)> {
// if cursor_point.line() < size.total_lines() as i32 {
// let cursor_width = if text_fragment.width == 0. {
// let cursor_width = if text_fragment.width == Pixels::ZERO {
// size.cell_width()
// } else {
// text_fragment.width
@ -292,7 +294,7 @@
// //Cursor should always surround as much of the text as possible,
// //hence when on pixel boundaries round the origin down and the width up
// Some((
// vec2f(
// point(
// (cursor_point.col() as f32 * size.cell_width()).floor(),
// (cursor_point.line() as f32 * size.line_height()).floor(),
// ),
@ -332,15 +334,15 @@
// let mut properties = Properties::new();
// if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
// properties = *properties.weight(Weight::BOLD);
// properties = *properties.weight(FontWeight::BOLD);
// }
// if indexed.flags.intersects(Flags::ITALIC) {
// properties = *properties.style(Italic);
// properties = *properties.style(FontStyle::Italic);
// }
// let font_id = font_cache
// .select_font(text_style.font_family_id, &properties)
// .unwrap_or(8text_style.font_id);
// .select_font(text_style.font_family, &properties)
// .unwrap_or(text_style.font_id);
// let mut result = RunStyle {
// color: fg,
@ -366,7 +368,7 @@
// fn generic_button_handler<E>(
// connection: WeakModel<Terminal>,
// origin: Point<Pixels>,
// f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
// f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
// ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
// move |event, _: &mut TerminalView, cx| {
// cx.focus_parent();
@ -522,9 +524,9 @@
// fn layout(
// &mut self,
// view_state: &mut TerminalView,
// element_state: &mut Self::ElementState,
// element_state: Option<Self::ElementState>,
// cx: &mut ViewContext<TerminalView>,
// ) -> LayoutId {
// ) -> (LayoutId, Self::ElementState) {
// let settings = ThemeSettings::get_global(cx);
// let terminal_settings = TerminalSettings::get_global(cx);
@ -569,7 +571,7 @@
// let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
// gutter = cell_width;
// let size = constraint.max - vec2f(gutter, 0.);
// let size = constraint.max - point(gutter, 0.);
// TerminalSize::new(line_height, cell_width, size)
// };
@ -607,11 +609,11 @@
// cx,
// ),
// )
// .with_position_mode(gpui::elements::OverlayPositionMode::Local)
// .with_position_mode(gpui::OverlayPositionMode::Local)
// .into_any();
// tooltip.layout(
// SizeConstraint::new(Vector2F::zero(), cx.window_size()),
// SizeConstraint::new(Point::zero(), cx.window_size()),
// view_state,
// cx,
// );
@ -735,7 +737,7 @@
// let clip_bounds = Some(visible_bounds);
// cx.paint_layer(clip_bounds, |cx| {
// let origin = bounds.origin() + vec2f(element_state.gutter, 0.);
// let origin = bounds.origin + point(element_state.gutter, 0.);
// // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
// self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
@ -808,7 +810,7 @@
// });
// }
// fn id(&self) -> Option<gpui::ElementId> {
// fn element_id(&self) -> Option<ElementId> {
// todo!()
// }
@ -842,12 +844,12 @@
// // ) -> Option<Bounds<Pixels>> {
// // // Use the same origin that's passed to `Cursor::paint` in the paint
// // // method bove.
// // let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
// // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
// // // TODO - Why is it necessary to move downward one line to get correct
// // // positioning? I would think that we'd want the same rect that is
// // // painted for the cursor.
// // origin += vec2f(0., layout.size.line_height);
// // origin += point(0., layout.size.line_height);
// // Some(layout.cursor.as_ref()?.bounding_rect(origin))
// // }
@ -886,7 +888,7 @@
// range: &RangeInclusive<AlacPoint>,
// layout: &LayoutState,
// origin: Point<Pixels>,
// ) -> Option<(f32, Vec<HighlightedRangeLine>)> {
// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
// // Step 1. Normalize the points to be viewport relative.
// // When display_offset = 1, here's how the grid is arranged:
// //-2,0 -2,1...
@ -937,8 +939,8 @@
// }
// highlighted_range_lines.push(HighlightedRangeLine {
// start_x: origin.x() + line_start as f32 * layout.size.cell_width,
// end_x: origin.x() + line_end as f32 * layout.size.cell_width,
// start_x: origin.x + line_start as f32 * layout.size.cell_width,
// end_x: origin.x + line_end as f32 * layout.size.cell_width,
// });
// }

View File

@ -7,27 +7,17 @@ pub mod terminal_panel;
// todo!()
// use crate::terminal_element::TerminalElement;
use anyhow::Context;
use dirs::home_dir;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, ParentComponent, Pixels,
Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
WeakView,
};
use language::Bias;
use persistence::TERMINAL_DB;
use project::{search::SearchQuery, LocalWorktree, Project};
use serde::Deserialize;
use settings::Settings;
use smol::Timer;
use std::{
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use terminal::{
alacritty_terminal::{
index::Point,
@ -42,7 +32,21 @@ use workspace::{
notifications::NotifyResultExt,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem},
NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
ui::{ContextMenu, ContextMenuItem, Label},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
use anyhow::Context;
use dirs::home_dir;
use serde::Deserialize;
use settings::Settings;
use smol::Timer;
use std::{
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@ -62,6 +66,7 @@ pub struct SendKeystroke(String);
actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
pub fn init(cx: &mut AppContext) {
workspace::ui::init(cx);
terminal_panel::init(cx);
terminal::init(cx);
@ -82,7 +87,7 @@ pub struct TerminalView {
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
// context_menu: View<ContextMenu>,
context_menu: Option<ContextMenu>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
@ -265,8 +270,7 @@ impl TerminalView {
has_new_content: true,
has_bell: false,
focus_handle: cx.focus_handle(),
// todo!()
// context_menu: cx.build_view(|cx| ContextMenu::new(view_id, cx)),
context_menu: None,
blink_state: true,
blinking_on: false,
blinking_paused: false,
@ -293,18 +297,20 @@ impl TerminalView {
cx.emit(Event::Wakeup);
}
pub fn deploy_context_menu(&mut self, _position: Point<Pixels>, _cx: &mut ViewContext<Self>) {
//todo!(context_menu)
// let menu_entries = vec![
// ContextMenuItem::action("Clear", Clear),
// ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
// ];
// self.context_menu.update(cx, |menu, cx| {
// menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
// });
// cx.notify();
pub fn deploy_context_menu(
&mut self,
position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
self.context_menu = Some(ContextMenu::new(vec![
ContextMenuItem::entry(Label::new("Clear"), Clear),
ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }),
]));
dbg!(&position);
// todo!()
// self.context_menu
// .show(position, AnchorCorner::TopLeft, menu_entries, cx);
// cx.notify();
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
@ -541,28 +547,41 @@ impl Render for TerminalView {
let focused = self.focus_handle.is_focused(cx);
div()
.relative()
.child(
div()
.z_index(0)
.absolute()
.on_key_down(Self::key_down)
.on_action(TerminalView::send_text)
.on_action(TerminalView::send_keystroke)
.on_action(TerminalView::copy)
.on_action(TerminalView::paste)
.on_action(TerminalView::clear)
.on_action(TerminalView::show_character_palette)
.on_action(TerminalView::select_all)
// todo!()
.child(
"TERMINAL HERE", // TerminalElement::new(
// terminal_handle,
// focused,
// self.should_show_cursor(focused, cx),
// self.can_navigate_to_selected_word,
// )
)
.on_mouse_down(MouseButton::Right, |this, event, cx| {
this.deploy_context_menu(event.position, cx);
cx.notify();
}),
)
.children(
self.context_menu
.clone()
.map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
)
.track_focus(&self.focus_handle)
.on_focus_in(Self::focus_in)
.on_focus_out(Self::focus_out)
.on_key_down(Self::key_down)
.on_action(TerminalView::send_text)
.on_action(TerminalView::send_keystroke)
.on_action(TerminalView::copy)
.on_action(TerminalView::paste)
.on_action(TerminalView::clear)
.on_action(TerminalView::show_character_palette)
.on_action(TerminalView::select_all)
// todo!()
.child(
"TERMINAL HERE", // TerminalElement::new(
// terminal_handle,
// focused,
// self.should_show_cursor(focused, cx),
// self.can_navigate_to_selected_word,
// )
)
// todo!()
// .child(ChildView::new(&self.context_menu, cx))
}
}

View File

@ -9,6 +9,7 @@ anyhow.workspace = true
chrono = "0.4"
gpui = { package = "gpui2", path = "../gpui2" }
itertools = { version = "0.11.0", optional = true }
menu = { package = "menu2", path = "../menu2"}
serde.workspace = true
settings2 = { path = "../settings2" }
smallvec.workspace = true

View File

@ -3,17 +3,29 @@ use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHea
pub enum ContextMenuItem {
Header(SharedString),
Entry(Label),
Entry(Label, Box<dyn gpui::Action>),
Separator,
}
impl Clone for ContextMenuItem {
fn clone(&self) -> Self {
match self {
ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()),
ContextMenuItem::Entry(label, action) => {
ContextMenuItem::Entry(label.clone(), action.boxed_clone())
}
ContextMenuItem::Separator => ContextMenuItem::Separator,
}
}
}
impl ContextMenuItem {
fn to_list_item<V: 'static>(self) -> ListItem {
match self {
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
ContextMenuItem::Entry(label) => {
ListEntry::new(label).variant(ListItemVariant::Inset).into()
}
ContextMenuItem::Entry(label, action) => ListEntry::new(label)
.variant(ListItemVariant::Inset)
.on_click(action)
.into(),
ContextMenuItem::Separator => ListSeparator::new().into(),
}
}
@ -26,12 +38,12 @@ impl ContextMenuItem {
Self::Separator
}
pub fn entry(label: Label) -> Self {
Self::Entry(label)
pub fn entry(label: Label, action: impl Action) -> Self {
Self::Entry(label, Box::new(action))
}
}
#[derive(Component)]
#[derive(Component, Clone)]
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
}
@ -42,7 +54,12 @@ impl ContextMenu {
items: items.into_iter().collect(),
}
}
// todo!()
// cx.add_action(ContextMenu::select_first);
// cx.add_action(ContextMenu::select_last);
// cx.add_action(ContextMenu::select_next);
// cx.add_action(ContextMenu::select_prev);
// cx.add_action(ContextMenu::confirm);
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
v_stack()
.flex()
@ -55,9 +72,11 @@ impl ContextMenu {
.map(ContextMenuItem::to_list_item::<V>)
.collect(),
))
.on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
}
}
use gpui::Action;
#[cfg(feature = "stories")]
pub use stories::*;
@ -65,7 +84,7 @@ pub use stories::*;
mod stories {
use super::*;
use crate::story::Story;
use gpui::{Div, Render};
use gpui::{action, Div, Render};
pub struct ContextMenuStory;
@ -73,14 +92,22 @@ mod stories {
type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
#[action]
struct PrintCurrentDate {}
Story::container(cx)
.child(Story::title_for::<_, ContextMenu>(cx))
.child(Story::label(cx, "Default"))
.child(ContextMenu::new([
ContextMenuItem::header("Section header"),
ContextMenuItem::Separator,
ContextMenuItem::entry(Label::new("Some entry")),
ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
]))
.on_action(|_, _: &PrintCurrentDate, _| {
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
println!("Current Unix time is {:?}", unix_time.as_secs());
}
})
}
}
}

View File

@ -60,7 +60,7 @@ pub enum LineHeightStyle {
UILabel,
}
#[derive(Component)]
#[derive(Clone, Component)]
pub struct Label {
label: SharedString,
size: LabelSize,

View File

@ -1,11 +1,10 @@
use gpui::div;
use gpui::{div, Action};
use crate::prelude::*;
use crate::settings::user_settings;
use crate::{
disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
TextColor, Toggle,
disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
};
use crate::{prelude::*, GraphicSlot};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
@ -232,6 +231,7 @@ pub struct ListEntry {
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
on_click: Option<Box<dyn Action>>,
}
impl ListEntry {
@ -245,9 +245,15 @@ impl ListEntry {
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
}
}
pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
self.on_click = Some(action.into());
self
}
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
@ -303,9 +309,21 @@ impl ListEntry {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
div()
.relative()
.hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into());
style
})
.on_mouse_down(gpui::MouseButton::Left, {
let action = self.on_click.map(|action| action.boxed_clone());
move |entry: &mut V, event, cx| {
if let Some(action) = action.as_ref() {
cx.dispatch_action(action.boxed_clone());
}
}
})
.group("")
.bg(cx.theme().colors().surface_background)
// TODO: Add focus state
@ -401,7 +419,7 @@ impl List {
v_stack()
.w_full()
.py_1()
.children(self.header)
.children(self.header.map(|header| header))
.child(list_content)
}
}

View File

@ -27,7 +27,6 @@ collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
collections = { path = "../collections" }
command_palette = { package="command_palette2", path = "../command_palette2" }
# component_test = { path = "../component_test" }
# context_menu = { path = "../context_menu" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }

View File

@ -141,7 +141,6 @@ fn main() {
cx.set_global(client.clone());
theme::init(cx);
// context_menu::init(cx);
project::Project::init(&client, cx);
client::init(&client, cx);
command_palette::init(cx);