diff --git a/Cargo.lock b/Cargo.lock index 197dc7cf95..f359702b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3797,6 +3797,7 @@ dependencies = [ "image", "itertools 0.10.5", "lazy_static", + "linkme", "log", "media", "metal", @@ -4815,6 +4816,26 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linkme" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "linux-raw-sys" version = "0.0.42" @@ -8831,6 +8852,17 @@ dependencies = [ "util", ] +[[package]] +name = "storybook3" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "settings2", + "theme2", + "ui2", +] + [[package]] name = "stringprep" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 8d2c420b55..792a3ada56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ members = [ "crates/sqlez_macros", "crates/rich_text", "crates/storybook2", + "crates/storybook3", "crates/sum_tree", "crates/terminal", "crates/terminal2", diff --git a/assets/settings/default.json b/assets/settings/default.json index 85f8a8fbc4..08d85dd723 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -35,6 +35,15 @@ // "custom": 2 // }, "buffer_line_height": "comfortable", + // The name of a font to use for rendering text in the UI + "ui_font_family": "Zed Mono", + // The OpenType features to enable for text in the UI + "ui_font_features": { + // Disable ligatures: + "calt": false + }, + // The default font size for text in the UI + "ui_font_size": 14, // The factor to grow the active pane by. Defaults to 1.0 // which gives the same size as all other panes. "active_pane_magnification": 1.0, diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index de6f3e92a1..090a32d4ca 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -224,7 +224,7 @@ impl TestServer { }); cx.update(|cx| { - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); Project::init(&client, cx); client::init(&client, cx); language::init(cx); diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6fb3f03f60..20e77d7023 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -684,6 +684,7 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + //todo!(collapsed_channels) // panel.collapsed_channels = serialized_panel // .collapsed_channels // .unwrap_or_else(|| Vec::new()); diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c9d16c7a5d..ed010cc500 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,9 +31,9 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render, - Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext, - WeakView, WindowBounds, + div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, + Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, + VisualContext, WeakView, WindowBounds, }; use project::Project; use theme::ActiveTheme; @@ -88,12 +88,17 @@ impl Render for CollabTitlebarItem { h_stack() .id("titlebar") .justify_between() - .when( - !matches!(cx.window_bounds(), WindowBounds::Fullscreen), - |s| s.pl_20(), - ) .w_full() .h(rems(1.75)) + // Set a non-scaling min-height here to ensure the titlebar is + // always at least the height of the traffic lights. + .min_h(px(32.)) + .when( + !matches!(cx.window_bounds(), WindowBounds::Fullscreen), + // Use pixels here instead of a rem-based size because the macOS traffic + // lights are a static size, and don't scale with the rest of the UI. + |s| s.pl(px(68.)), + ) .bg(cx.theme().colors().title_bar_background) .on_click(|_, event, cx| { if event.up.click_count == 2 { @@ -102,6 +107,7 @@ impl Render for CollabTitlebarItem { }) .child( h_stack() + .gap_1() // TODO - Add player menu .child( div() @@ -130,14 +136,12 @@ impl Render for CollabTitlebarItem { .color(Some(TextColor::Muted)), ) .tooltip(move |_, cx| { - // todo!() Replace with real action. - #[gpui::action] - struct NoAction {} cx.build_view(|_| { Tooltip::new("Recent Branches") .key_binding(KeyBinding::new(gpui::KeyBinding::new( "cmd-b", - NoAction {}, + // todo!() Replace with real action. + gpui::NoAction, None, ))) .meta("Only local branches shown") diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 8138f025d3..9463cab68c 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,9 +1,8 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, - Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke, + ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -16,7 +15,7 @@ use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; use zed_actions::OpenZedURL; actions!(Toggle); @@ -47,7 +46,7 @@ impl CommandPalette { .available_actions() .into_iter() .filter_map(|action| { - let name = action.name(); + let name = gpui::remove_the_2(action.name()); let namespace = name.split("::").next().unwrap_or("malformed action name"); if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { return None; @@ -69,10 +68,9 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} -impl Modal for CommandPalette { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)); +impl ManagedView for CommandPalette { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } @@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } @@ -456,7 +454,7 @@ mod tests { fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { let app_state = AppState::test(cx); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); editor::init(cx); workspace::init(app_state.clone(), cx); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3b383c2ac9..92d430e3fb 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1051,17 +1051,15 @@ mod tests { ); // Ensure updates to the file are reflected in the LSP. - buffer_1 - .update(cx, |buffer, cx| { - buffer.file_updated( - Arc::new(File { - abs_path: "/root/child/buffer-1".into(), - path: Path::new("child/buffer-1").into(), - }), - cx, - ) - }) - .await; + buffer_1.update(cx, |buffer, cx| { + buffer.file_updated( + Arc::new(File { + abs_path: "/root/child/buffer-1".into(), + path: Path::new("child/buffer-1").into(), + }), + cx, + ) + }); assert_eq!( lsp.receive_notification::() .await, diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index e64d5e301c..533abcd871 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint}; use collections::{BTreeMap, HashMap, HashSet}; use fold_map::FoldMap; use gpui::{ - Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle, + Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine, + TextRun, UnderlineStyle, WrappedLine, }; use inlay_map::InlayMap; use language::{ @@ -561,7 +562,7 @@ impl DisplaySnapshot { }) } - pub fn lay_out_line_for_row( + pub fn layout_row( &self, display_row: u32, TextLayoutDetails { @@ -569,7 +570,7 @@ impl DisplaySnapshot { editor_style, rem_size, }: &TextLayoutDetails, - ) -> Line { + ) -> Arc { let mut runs = Vec::new(); let mut line = String::new(); @@ -598,29 +599,27 @@ impl DisplaySnapshot { let font_size = editor_style.text.font_size.to_pixels(*rem_size); text_system - .layout_text(&line, font_size, &runs, None) - .unwrap() - .pop() - .unwrap() + .layout_line(&line, font_size, &runs) + .expect("we expect the font to be loaded because it's rendered by the editor") } - pub fn x_for_point( + pub fn x_for_display_point( &self, display_point: DisplayPoint, text_layout_details: &TextLayoutDetails, ) -> Pixels { - let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details); - layout_line.x_for_index(display_point.column() as usize) + let line = self.layout_row(display_point.row(), text_layout_details); + line.x_for_index(display_point.column() as usize) } - pub fn column_for_x( + pub fn display_column_for_x( &self, display_row: u32, - x_coordinate: Pixels, - text_layout_details: &TextLayoutDetails, + x: Pixels, + details: &TextLayoutDetails, ) -> u32 { - let layout_line = self.lay_out_line_for_row(display_row, text_layout_details); - layout_line.closest_index_for_x(x_coordinate) as u32 + let layout_line = self.layout_row(display_row, details); + layout_line.closest_index_for_x(x) as u32 } pub fn chars_at( diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index a6bc6343f4..84fad96a48 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/crates/editor2/src/display_map/inlay_map.rs @@ -1891,6 +1891,6 @@ mod tests { fn init_test(cx: &mut AppContext) { let store = SettingsStore::test(cx); cx.set_global(store); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); } } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8e7bd5876f..b1dc76852d 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,7 +39,7 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement, + actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, @@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); // // .with_soft_wrap(true) // } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectNext { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectPrevious { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectAllMatches { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectToBeginningOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct MovePageUp { #[serde(default)] center_cursor: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct MovePageDown { #[serde(default)] center_cursor: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectToEndOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ToggleCodeActions { #[serde(default)] pub deployed_from_indicator: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ConfirmCompletion { #[serde(default)] pub item_ix: Option, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ConfirmCodeAction { #[serde(default)] pub item_ix: Option, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct FoldAt { pub buffer_row: u32, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct UnfoldAt { pub buffer_row: u32, } @@ -5445,7 +5445,9 @@ impl Editor { *head.column_mut() += 1; head = display_map.clip_point(head, Bias::Right); let goal = SelectionGoal::HorizontalPosition( - display_map.x_for_point(head, &text_layout_details).into(), + display_map + .x_for_display_point(head, &text_layout_details) + .into(), ); selection.collapse_to(head, goal); @@ -6391,8 +6393,8 @@ impl Editor { let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); let range = oldest_selection.display_range(&display_map).sorted(); - let start_x = display_map.x_for_point(range.start, &text_layout_details); - let end_x = display_map.x_for_point(range.end, &text_layout_details); + let start_x = display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = display_map.x_for_display_point(range.end, &text_layout_details); let positions = start_x.min(end_x)..start_x.max(end_x); selections.clear(); @@ -6431,15 +6433,16 @@ impl Editor { let range = selection.display_range(&display_map).sorted(); debug_assert_eq!(range.start.row(), range.end.row()); let mut row = range.start.row(); - let positions = if let SelectionGoal::HorizontalRange { start, end } = - selection.goal - { - px(start)..px(end) - } else { - let start_x = display_map.x_for_point(range.start, &text_layout_details); - let end_x = display_map.x_for_point(range.end, &text_layout_details); - start_x.min(end_x)..start_x.max(end_x) - }; + let positions = + if let SelectionGoal::HorizontalRange { start, end } = selection.goal { + px(start)..px(end) + } else { + let start_x = + display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = + display_map.x_for_display_point(range.end, &text_layout_details); + start_x.min(end_x)..start_x.max(end_x) + }; while row != end_row { if above { @@ -6992,7 +6995,7 @@ impl Editor { let display_point = point.to_display_point(display_snapshot); let goal = SelectionGoal::HorizontalPosition( display_snapshot - .x_for_point(display_point, &text_layout_details) + .x_for_display_point(display_point, &text_layout_details) .into(), ); (display_point, goal) @@ -9379,18 +9382,16 @@ impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let settings = ThemeSettings::get_global(cx); let text_style = match self.mode { - EditorMode::SingleLine => { - TextStyle { - color: cx.theme().colors().text, - font_family: settings.ui_font.family.clone(), // todo!() - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()), - underline: None, - } - } + EditorMode::SingleLine => TextStyle { + color: cx.theme().colors().text, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.).into(), + underline: None, + }, EditorMode::AutoHeight { max_lines } => todo!(), @@ -9761,7 +9762,8 @@ impl InputHandler for Editor { let scroll_left = scroll_position.x * em_width; let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width; + let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left + + self.gutter_width; let y = line_height * (start.row() as f32 - scroll_position.y); Some(Bounds { diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 63bc6179c2..bd69e7acdf 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -8277,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC cx.update(|cx| { let store = SettingsStore::test(cx); cx.set_global(store); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index aa54bed73a..3de5389b1f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line, + ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, - ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View, - ViewContext, WindowContext, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, + TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -476,7 +476,7 @@ impl EditorElement { Self::paint_diff_hunks(bounds, layout, cx); } - for (ix, line) in layout.line_number_layouts.iter().enumerate() { + for (ix, line) in layout.line_numbers.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin + point( @@ -775,21 +775,21 @@ impl EditorElement { .chars_at(cursor_position) .next() .and_then(|(character, _)| { - let text = character.to_string(); + let text = SharedString::from(character.to_string()); + let len = text.len(); cx.text_system() - .layout_text( - &text, + .shape_line( + text, cursor_row_layout.font_size, &[TextRun { - len: text.len(), + len, font: self.style.text.font(), color: self.style.background, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() + .log_err() }) } else { None @@ -1244,20 +1244,20 @@ impl EditorElement { let font_size = style.text.font_size.to_pixels(cx.rem_size()); let layout = cx .text_system() - .layout_text( - " ".repeat(column).as_str(), + .shape_line( + SharedString::from(" ".repeat(column)), font_size, &[TextRun { len: column, font: style.text.font(), color: Hsla::default(), + background_color: None, underline: None, }], - None, ) .unwrap(); - layout[0].width + layout.width } fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> Pixels { @@ -1338,7 +1338,7 @@ impl EditorElement { relative_rows } - fn layout_line_numbers( + fn shape_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -1347,12 +1347,12 @@ impl EditorElement { snapshot: &EditorSnapshot, cx: &ViewContext, ) -> ( - Vec>, + Vec>, Vec>, ) { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let include_line_numbers = snapshot.mode == EditorMode::Full; - let mut line_number_layouts = Vec::with_capacity(rows.len()); + let mut shaped_line_numbers = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); let is_relative = EditorSettings::get_global(cx).relative_line_numbers; @@ -1387,15 +1387,14 @@ impl EditorElement { len: line_number.len(), font: self.style.text.font(), color, + background_color: None, underline: None, }; - let layout = cx + let shaped_line = cx .text_system() - .layout_text(&line_number, font_size, &[run], None) - .unwrap() - .pop() + .shape_line(line_number.clone().into(), font_size, &[run]) .unwrap(); - line_number_layouts.push(Some(layout)); + shaped_line_numbers.push(Some(shaped_line)); fold_statuses.push( is_singleton .then(|| { @@ -1408,17 +1407,17 @@ impl EditorElement { } } else { fold_statuses.push(None); - line_number_layouts.push(None); + shaped_line_numbers.push(None); } } - (line_number_layouts, fold_statuses) + (shaped_line_numbers, fold_statuses) } fn layout_lines( &mut self, rows: Range, - line_number_layouts: &[Option], + line_number_layouts: &[Option], snapshot: &EditorSnapshot, cx: &ViewContext, ) -> Vec { @@ -1439,18 +1438,17 @@ impl EditorElement { .chain(iter::repeat("")) .take(rows.len()); placeholder_lines - .map(|line| { + .filter_map(move |line| { let run = TextRun { len: line.len(), font: self.style.text.font(), color: placeholder_color, + background_color: None, underline: Default::default(), }; cx.text_system() - .layout_text(line, font_size, &[run], None) - .unwrap() - .pop() - .unwrap() + .shape_line(line.to_string().into(), font_size, &[run]) + .log_err() }) .map(|line| LineWithInvisibles { line, @@ -1726,7 +1724,7 @@ impl EditorElement { .head }); - let (line_number_layouts, fold_statuses) = self.layout_line_numbers( + let (line_numbers, fold_statuses) = self.shape_line_numbers( start_row..end_row, &active_rows, head_for_relative, @@ -1740,8 +1738,7 @@ impl EditorElement { let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); let mut max_visible_line_width = Pixels::ZERO; - let line_layouts = - self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx); + let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx); for line_with_invisibles in &line_layouts { if line_with_invisibles.line.width > max_visible_line_width { max_visible_line_width = line_with_invisibles.line.width; @@ -1879,35 +1876,31 @@ impl EditorElement { let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx .text_system() - .layout_text( - "→", + .shape_line( + "→".into(), invisible_symbol_font_size, &[TextRun { len: "→".len(), font: self.style.text.font(), color: cx.theme().colors().editor_invisible, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() .unwrap(); let space_invisible = cx .text_system() - .layout_text( - "•", + .shape_line( + "•".into(), invisible_symbol_font_size, &[TextRun { len: "•".len(), font: self.style.text.font(), color: cx.theme().colors().editor_invisible, + background_color: None, underline: None, }], - None, ) - .unwrap() - .pop() .unwrap(); LayoutState { @@ -1939,7 +1932,7 @@ impl EditorElement { active_rows, highlighted_rows, highlighted_ranges, - line_number_layouts, + line_numbers, display_hunks, blocks, selections, @@ -2201,7 +2194,7 @@ impl EditorElement { #[derive(Debug)] pub struct LineWithInvisibles { - pub line: Line, + pub line: ShapedLine, invisibles: Vec, } @@ -2211,7 +2204,7 @@ impl LineWithInvisibles { text_style: &TextStyle, max_line_len: usize, max_line_count: usize, - line_number_layouts: &[Option], + line_number_layouts: &[Option], editor_mode: EditorMode, cx: &WindowContext, ) -> Vec { @@ -2231,11 +2224,12 @@ impl LineWithInvisibles { }]) { for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() { if ix > 0 { - let layout = cx + let shaped_line = cx .text_system() - .layout_text(&line, font_size, &styles, None); + .shape_line(line.clone().into(), font_size, &styles) + .unwrap(); layouts.push(Self { - line: layout.unwrap().pop().unwrap(), + line: shaped_line, invisibles: invisibles.drain(..).collect(), }); @@ -2269,6 +2263,7 @@ impl LineWithInvisibles { len: line_chunk.len(), font: text_style.font(), color: text_style.color, + background_color: None, underline: text_style.underline, }); @@ -3089,7 +3084,7 @@ pub struct LayoutState { visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, - line_number_layouts: Vec>, + line_numbers: Vec>, display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, @@ -3102,8 +3097,8 @@ pub struct LayoutState { code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, fold_indicators: Vec>>, - tab_invisible: Line, - space_invisible: Line, + tab_invisible: ShapedLine, + space_invisible: ShapedLine, } struct CodeActionsIndicator { @@ -3203,7 +3198,7 @@ fn layout_line( snapshot: &EditorSnapshot, style: &EditorStyle, cx: &WindowContext, -) -> Result { +) -> Result { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -3215,21 +3210,17 @@ fn layout_line( line.truncate(len); } - Ok(cx - .text_system() - .layout_text( - &line, - style.text.font_size.to_pixels(cx.rem_size()), - &[TextRun { - len: snapshot.line_len(row) as usize, - font: style.text.font(), - color: Hsla::default(), - underline: None, - }], - None, - )? - .pop() - .unwrap()) + cx.text_system().shape_line( + line.into(), + style.text.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len: snapshot.line_len(row) as usize, + font: style.text.font(), + color: Hsla::default(), + background_color: None, + underline: None, + }], + ) } #[derive(Debug)] @@ -3239,7 +3230,7 @@ pub struct Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, } impl Cursor { @@ -3249,7 +3240,7 @@ impl Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, ) -> Cursor { Cursor { origin, diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index af9febf376..eba49ccbf7 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); client::init_settings(cx); language::init(cx); Project::init_settings(cx); diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 03e1f9dfa9..d2b426f4ae 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -797,7 +797,7 @@ impl Item for Editor { fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { let workspace_id = workspace.database_id(); - let item_id = cx.view().entity_id().as_u64() as ItemId; + let item_id = cx.view().item_id().as_u64() as ItemId; self.workspace = Some((workspace.weak_handle(), workspace.database_id())); fn serialize( @@ -828,7 +828,7 @@ impl Item for Editor { serialize( buffer, *workspace_id, - cx.view().entity_id().as_u64() as ItemId, + cx.view().item_id().as_u64() as ItemId, cx, ); } diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index b28af681e0..1414ae702d 100644 --- a/crates/editor2/src/movement.rs +++ b/crates/editor2/src/movement.rs @@ -98,7 +98,7 @@ pub fn up_by_rows( SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.") SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), - _ => map.x_for_point(start, text_layout_details), + _ => map.x_for_display_point(start, text_layout_details), }; let prev_row = start.row().saturating_sub(row_count); @@ -107,7 +107,7 @@ pub fn up_by_rows( Bias::Left, ); if point.row() < start.row() { - *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_start { return (start, goal); } else { @@ -137,18 +137,18 @@ pub fn down_by_rows( SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), - _ => map.x_for_point(start, text_layout_details), + _ => map.x_for_display_point(start, text_layout_details), }; let new_row = start.row() + row_count; let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right); if point.row() > start.row() { - *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details) + *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details) } else if preserve_column_at_end { return (start, goal); } else { point = map.max_point(); - goal_x = map.x_for_point(point, text_layout_details) + goal_x = map.x_for_display_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 01e241c830..bcf41f135b 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -313,14 +313,14 @@ impl SelectionsCollection { let is_empty = positions.start == positions.end; let line_len = display_map.line_len(row); - let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); + let line = display_map.layout_row(row, &text_layout_details); dbg!("****START COL****"); - let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; - if start_col < line_len || (is_empty && positions.start == layed_out_line.width) { + let start_col = line.closest_index_for_x(positions.start) as u32; + if start_col < line_len || (is_empty && positions.start == line.width) { let start = DisplayPoint::new(row, start_col); dbg!("****END COL****"); - let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; + let end_col = line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); dbg!(start_col, end_col); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 236bc15244..0fee5102e6 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,9 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model, - ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent, + ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, + WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -19,7 +19,7 @@ use text::Point; use theme::ActiveTheme; use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -111,10 +111,9 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} -impl Modal for FileFinder { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)) +impl ManagedView for FileFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } impl Render for FileFinder { @@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) - .ok()?; + finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?; Some(()) }) @@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } @@ -1763,7 +1760,7 @@ mod tests { fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { let state = AppState::test(cx); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); super::init(cx); editor::init(cx); diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index ccd6b7ada2..565afb5e93 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,13 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString, - Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent, + Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -23,10 +23,9 @@ pub struct GoToLine { _subscriptions: Vec, } -impl EventEmitter for GoToLine {} -impl Modal for GoToLine { - fn focus(&self, cx: &mut WindowContext) { - self.line_editor.update(cx, |editor, cx| editor.focus(cx)) +impl ManagedView for GoToLine { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.line_editor.focus_handle(cx) } } @@ -88,7 +87,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::Event::Blurred => cx.emit(ModalEvent::Dismissed), + editor::Event::Blurred => cx.emit(Dismiss), editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -123,7 +122,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +139,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } } diff --git a/crates/gpui2/Cargo.toml b/crates/gpui2/Cargo.toml index df461af7b8..1bec9d43dc 100644 --- a/crates/gpui2/Cargo.toml +++ b/crates/gpui2/Cargo.toml @@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" } async-task = "4.0.3" backtrace = { version = "0.3", optional = true } ctor.workspace = true +linkme = "0.3" derive_more.workspace = true dhat = { version = "0.3", optional = true } env_logger = { version = "0.9", optional = true } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index a81bcfcdbc..958eaabdb8 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,10 +1,12 @@ use crate::SharedString; use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use lazy_static::lazy_static; -use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; -use serde::Deserialize; -use std::any::{type_name, Any, TypeId}; +pub use no_action::NoAction; +use serde_json::json; +use std::{ + any::{Any, TypeId}, + ops::Deref, +}; /// Actions are used to implement keyboard-driven UI. /// When you declare an action, you can bind keys to the action in the keymap and @@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId}; /// ```rust /// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline); /// ``` -/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro, -/// it will automatically +/// More complex data types can also be actions. If you annotate your type with the action derive macro +/// it will be implemented and registered automatically. /// ``` -/// #[action] +/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)] /// pub struct SelectNext { /// pub replace_newest: bool, /// } /// -/// Any type A that satisfies the following bounds is automatically an action: -/// -/// ``` -/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, -/// ``` -/// -/// The `#[action]` annotation will derive these implementations for your struct automatically. If you -/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only -/// generates the code needed to register your action before `main`. Then you'll need to implement all -/// the traits manually. +/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]` +/// macro, which only generates the code needed to register your action before `main`. /// /// ``` /// #[gpui::register_action] @@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId}; /// pub content: SharedString, /// } /// -/// impl std::default::Default for Paste { -/// fn default() -> Self { -/// Self { -/// content: SharedString::from("🍝"), -/// } -/// } +/// impl gpui::Action for Paste { +/// ///... /// } /// ``` -pub trait Action: std::fmt::Debug + 'static { - fn qualified_name() -> SharedString - where - Self: Sized; - fn build(value: Option) -> Result> - where - Self: Sized; - fn is_registered() -> bool - where - Self: Sized; - - fn partial_eq(&self, action: &dyn Action) -> bool; +pub trait Action: 'static { fn boxed_clone(&self) -> Box; fn as_any(&self) -> &dyn Any; + fn partial_eq(&self, action: &dyn Action) -> bool; + fn name(&self) -> &str; + + fn debug_name() -> &'static str + where + Self: Sized; + fn build(value: serde_json::Value) -> Result> + where + Self: Sized; } -// Types become actions by satisfying a list of trait bounds. -impl Action for A -where - A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static, -{ - fn qualified_name() -> SharedString { - let name = type_name::(); - let mut separator_matches = name.rmatch_indices("::"); - separator_matches.next().unwrap(); - let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2); - // todo!() remove the 2 replacement when migration is done - name[name_start_ix..].replace("2::", "::").into() - } - - fn build(params: Option) -> Result> - where - Self: Sized, - { - let action = if let Some(params) = params { - serde_json::from_value(params).context("failed to deserialize action")? - } else { - Self::default() - }; - Ok(Box::new(action)) - } - - fn is_registered() -> bool { - ACTION_REGISTRY - .read() - .names_by_type_id - .get(&TypeId::of::()) - .is_some() - } - - fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn as_any(&self) -> &dyn Any { - self +impl std::fmt::Debug for dyn Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("dyn Action") + .field("type_name", &self.name()) + .finish() } } @@ -119,69 +65,93 @@ impl dyn Action { pub fn type_id(&self) -> TypeId { self.as_any().type_id() } - - pub fn name(&self) -> SharedString { - ACTION_REGISTRY - .read() - .names_by_type_id - .get(&self.type_id()) - .expect("type is not a registered action") - .clone() - } } -type ActionBuilder = fn(json: Option) -> anyhow::Result>; +type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; -lazy_static! { - static ref ACTION_REGISTRY: RwLock = RwLock::default(); -} - -#[derive(Default)] -struct ActionRegistry { +pub(crate) struct ActionRegistry { builders_by_name: HashMap, names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } -/// Register an action type to allow it to be referenced in keymaps. -pub fn register_action() { - let name = A::qualified_name(); - let mut lock = ACTION_REGISTRY.write(); - lock.builders_by_name.insert(name.clone(), A::build); - lock.names_by_type_id - .insert(TypeId::of::(), name.clone()); - lock.all_names.push(name); +impl Default for ActionRegistry { + fn default() -> Self { + let mut this = ActionRegistry { + builders_by_name: Default::default(), + names_by_type_id: Default::default(), + all_names: Default::default(), + }; + + this.load_actions(); + + this + } } -/// Construct an action based on its name and optional JSON parameters sourced from the keymap. -pub fn build_action_from_type(type_id: &TypeId) -> Result> { - let lock = ACTION_REGISTRY.read(); - let name = lock - .names_by_type_id - .get(type_id) - .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? - .clone(); - drop(lock); +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub type MacroActionBuilder = fn() -> ActionData; - build_action(&name, None) +/// This type must be public so that our macros can build it in other crates. +/// But this is an implementation detail and should not be used directly. +#[doc(hidden)] +pub struct ActionData { + pub name: &'static str, + pub type_id: TypeId, + pub build: ActionBuilder, } -/// Construct an action based on its name and optional JSON parameters sourced from the keymap. -pub fn build_action(name: &str, params: Option) -> Result> { - let lock = ACTION_REGISTRY.read(); +/// This constant must be public to be accessible from other crates. +/// But it's existence is an implementation detail and should not be used directly. +#[doc(hidden)] +#[linkme::distributed_slice] +pub static __GPUI_ACTIONS: [MacroActionBuilder]; - let build_action = lock - .builders_by_name - .get(name) - .ok_or_else(|| anyhow!("no action type registered for {}", name))?; - (build_action)(params) -} +impl ActionRegistry { + /// Load all registered actions into the registry. + pub(crate) fn load_actions(&mut self) { + for builder in __GPUI_ACTIONS { + let action = builder(); + //todo(remove) + let name: SharedString = remove_the_2(action.name).into(); + self.builders_by_name.insert(name.clone(), action.build); + self.names_by_type_id.insert(action.type_id, name.clone()); + self.all_names.push(name); + } + } -pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> { - let lock = ACTION_REGISTRY.read(); - RwLockReadGuard::map(lock, |registry: &ActionRegistry| { - registry.all_names.as_slice() - }) + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action_type(&self, type_id: &TypeId) -> Result> { + let name = self + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + + self.build_action(&name, None) + } + + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. + pub fn build_action( + &self, + name: &str, + params: Option, + ) -> Result> { + //todo(remove) + let name = remove_the_2(name); + let build_action = self + .builders_by_name + .get(name.deref()) + .ok_or_else(|| anyhow!("no action type registered for {}", name))?; + (build_action)(params.unwrap_or_else(|| json!({}))) + .with_context(|| format!("Attempting to build action {}", name)) + } + + pub fn all_action_names(&self) -> &[SharedString] { + self.all_names.as_slice() + } } /// Defines unit structs that can be used as actions. @@ -191,7 +161,7 @@ macro_rules! actions { () => {}; ( $name:ident ) => { - #[gpui::action] + #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)] pub struct $name; }; @@ -200,3 +170,20 @@ macro_rules! actions { actions!($($rest)*); }; } + +//todo!(remove) +pub fn remove_the_2(action_name: &str) -> String { + let mut separator_matches = action_name.rmatch_indices("::"); + separator_matches.next().unwrap(); + let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2); + // todo!() remove the 2 replacement when migration is done + action_name[name_start_ix..] + .replace("2::", "::") + .to_string() +} + +mod no_action { + use crate as gpui; + + actions!(NoAction); +} diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index c76b62b510..b5083b97c2 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -14,12 +14,13 @@ use smallvec::SmallVec; pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle, - AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, - Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, - LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet, - Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, - Window, WindowContext, WindowHandle, WindowId, + current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView, + AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, + DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, + ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, + PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, + TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext, + WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; @@ -182,6 +183,7 @@ pub struct AppContext { text_system: Arc, flushing_effects: bool, pending_updates: usize, + pub(crate) actions: Rc, pub(crate) active_drag: Option, pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, @@ -240,6 +242,7 @@ impl AppContext { platform: platform.clone(), app_metadata, text_system, + actions: Rc::new(ActionRegistry::default()), flushing_effects: false, pending_updates: 0, active_drag: None, @@ -964,6 +967,18 @@ impl AppContext { pub fn propagate(&mut self) { self.propagate_event = true; } + + pub fn build_action( + &self, + name: &str, + data: Option, + ) -> Result> { + self.actions.build_action(name, data) + } + + pub fn all_action_names(&self) -> &[SharedString] { + self.actions.all_action_names() + } } impl Context for AppContext { diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 5b7f8ce590..83b3ccebe7 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -182,6 +182,10 @@ pub struct AsyncWindowContext { } impl AsyncWindowContext { + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self { Self { app, window } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 1269110bc6..940492573f 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -370,10 +370,19 @@ impl Model { }) }); - cx.executor().run_until_parked(); - rx.try_next() - .expect("no event received") - .expect("model was dropped") + // Run other tasks until the event is emitted. + loop { + match rx.try_next() { + Ok(Some(event)) => return event, + Ok(None) => panic!("model was dropped"), + Err(_) => { + if !cx.executor().tick() { + break; + } + } + } + } + panic!("no event received") } } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 221eb903fd..b4b1af630e 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -13,7 +13,7 @@ pub trait Element { fn layout( &mut self, view_state: &mut V, - previous_element_state: Option, + element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState); diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 31a8827109..f9560f2c53 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -237,11 +237,11 @@ pub trait InteractiveComponent: Sized + Element { // // if we are relying on this side-effect still, removing the debug_assert! // likely breaks the command_palette tests. - debug_assert!( - A::is_registered(), - "{:?} is not registered as an action", - A::qualified_name() - ); + // debug_assert!( + // A::is_registered(), + // "{:?} is not registered as an action", + // A::qualified_name() + // ); self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |view, action, phase, cx| { @@ -960,11 +960,11 @@ where cx.background_executor().timer(TOOLTIP_DELAY).await; view.update(&mut cx, move |view_state, cx| { active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), cursor_offset: cx.mouse_position(), }), + _task: None, }); cx.notify(); }) @@ -972,12 +972,17 @@ where } }); active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: Some(task), tooltip: None, + _task: Some(task), }); } }); + let active_tooltip = element_state.active_tooltip.clone(); + cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { + active_tooltip.borrow_mut().take(); + }); + if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() { if active_tooltip.tooltip.is_some() { cx.active_tooltip = active_tooltip.tooltip.clone() @@ -1207,9 +1212,8 @@ pub struct InteractiveElementState { } pub struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, tooltip: Option, + _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index a190337f04..14a8048d39 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,9 @@ use smallvec::SmallVec; +use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point, - Size, Style, + point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, + Point, Size, Style, }; pub struct OverlayState { @@ -14,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -25,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -35,6 +37,13 @@ impl Overlay { self } + /// Sets the position in window co-ordinates + /// (otherwise the location the overlay is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = OverlayFitMode::SnapToWindow; @@ -48,6 +57,12 @@ impl ParentComponent for Overlay { } } +impl Component for Overlay { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + impl Element for Overlay { type ElementState = OverlayState; @@ -66,7 +81,12 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + + let mut overlay_style = Style::default(); + overlay_style.position = Position::Absolute; + overlay_style.display = Display::Flex; + + let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); (layout_id, OverlayState { child_layout_ids }) } @@ -90,7 +110,7 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (child_max - child_min).into(); - let origin = bounds.origin; + let origin = self.anchor_position.unwrap_or(bounds.origin); let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { @@ -184,6 +204,15 @@ impl AnchorCorner { Bounds { origin, size } } + pub fn corner(&self, bounds: Bounds) -> Point { + match self { + Self::TopLeft => bounds.origin, + Self::TopRight => bounds.upper_right(), + Self::BottomLeft => bounds.lower_left(), + Self::BottomRight => bounds.lower_right(), + } + } + fn switch_axis(self, axis: Axis) -> Self { match axis { Axis::Vertical => match self { diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 1081154e7d..6849a89711 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,76 +1,39 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, - Size, TextRun, ViewContext, + AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, + SharedString, Size, TextRun, ViewContext, WrappedLine, }; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{marker::PhantomData, sync::Arc}; +use std::{cell::Cell, rc::Rc, sync::Arc}; use util::ResultExt; -impl Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - state_type: PhantomData, - } - .render() - } -} - -impl Component for &'static str { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -// TODO: Figure out how to pass `String` to `child` without this. -// This impl doesn't exist in the `gpui2` crate. -impl Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -pub struct Text { +pub struct Text { text: SharedString, runs: Option>, - state_type: PhantomData, } -impl Text { - /// styled renders text that has different runs of different styles. - /// callers are responsible for setting the correct style for each run. - //// - /// For uniform text you can usually just pass a string as a child, and - /// cx.text_style() will be used automatically. +impl Text { + /// Renders text with runs of different styles. + /// + /// Callers are responsible for setting the correct style for each run. + /// For text with a uniform style, you can usually avoid calling this constructor + /// and just pass text directly. pub fn styled(text: SharedString, runs: Vec) -> Self { Text { text, runs: Some(runs), - state_type: Default::default(), } } } -impl Component for Text { +impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { - type ElementState = Arc>>; +impl Element for Text { + type ElementState = TextState; fn element_id(&self) -> Option { None @@ -103,7 +66,7 @@ impl Element for Text { let element_state = element_state.clone(); move |known_dimensions, _| { let Some(lines) = text_system - .layout_text( + .shape_text( &text, font_size, &runs[..], @@ -111,30 +74,23 @@ impl Element for Text { ) .log_err() else { - element_state.lock().replace(TextElementState { + element_state.lock().replace(TextStateInner { lines: Default::default(), line_height, }); return Size::default(); }; - let line_count = lines - .iter() - .map(|line| line.wrap_count() + 1) - .sum::(); - let size = Size { - width: lines - .iter() - .map(|line| line.layout.width) - .max() - .unwrap() - .ceil(), - height: line_height * line_count, - }; + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width); + } element_state .lock() - .replace(TextElementState { lines, line_height }); + .replace(TextStateInner { lines, line_height }); size } @@ -165,7 +121,104 @@ impl Element for Text { } } -pub struct TextElementState { - lines: SmallVec<[Line; 1]>, +#[derive(Default, Clone)] +pub struct TextState(Arc>>); + +impl TextState { + fn lock(&self) -> MutexGuard> { + self.0.lock() + } +} + +struct TextStateInner { + lines: SmallVec<[WrappedLine; 1]>, line_height: Pixels, } + +struct InteractiveText { + id: ElementId, + text: Text, +} + +struct InteractiveTextState { + text_state: TextState, + clicked_range_ixs: Rc>>, +} + +impl Element for InteractiveText { + type ElementState = InteractiveTextState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::ElementState) { + if let Some(InteractiveTextState { + text_state, + clicked_range_ixs, + }) = element_state + { + let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); + let element_state = InteractiveTextState { + text_state, + clicked_range_ixs, + }; + (layout_id, element_state) + } else { + let (layout_id, text_state) = self.text.layout(view_state, None, cx); + let element_state = InteractiveTextState { + text_state, + clicked_range_ixs: Rc::default(), + }; + (layout_id, element_state) + } + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.text + .paint(bounds, view_state, &mut element_state.text_state, cx) + } +} + +impl Component for SharedString { + fn render(self) -> AnyElement { + Text { + text: self, + runs: None, + } + .render() + } +} + +impl Component for &'static str { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} + +// TODO: Figure out how to pass `String` to `child` without this. +// This impl doesn't exist in the `gpui2` crate. +impl Component for String { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index bb9b5d0d79..cf138a90db 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -5,10 +5,11 @@ use std::{ fmt::Debug, marker::PhantomData, mem, + num::NonZeroUsize, pin::Pin, rc::Rc, sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, task::{Context, Poll}, @@ -71,30 +72,57 @@ impl Future for Task { } } } + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct TaskLabel(NonZeroUsize); + +impl TaskLabel { + pub fn new() -> Self { + static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); + Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap()) + } +} + type AnyLocalFuture = Pin>>; + type AnyFuture = Pin>>; + impl BackgroundExecutor { pub fn new(dispatcher: Arc) -> Self { Self { dispatcher } } - /// Enqueues the given closure to be run on any thread. The closure returns - /// a future which will be run to completion on any available thread. + /// Enqueues the given future to be run to completion on a background thread. pub fn spawn(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { + self.spawn_internal::(Box::pin(future), None) + } + + /// Enqueues the given future to be run to completion on a background thread. + /// The given label can be used to control the priority of the task in tests. + pub fn spawn_labeled( + &self, + label: TaskLabel, + future: impl Future + Send + 'static, + ) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), Some(label)) + } + + fn spawn_internal( + &self, + future: AnyFuture, + label: Option, + ) -> Task { let dispatcher = self.dispatcher.clone(); - fn inner( - dispatcher: Arc, - future: AnyFuture, - ) -> Task { - let (runnable, task) = - async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable)); - runnable.schedule(); - Task::Spawned(task) - } - inner::(dispatcher, Box::pin(future)) + let (runnable, task) = + async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label)); + runnable.schedule(); + Task::Spawned(task) } #[cfg(any(test, feature = "test-support"))] @@ -130,7 +158,7 @@ impl BackgroundExecutor { match future.as_mut().poll(&mut cx) { Poll::Ready(result) => return result, Poll::Pending => { - if !self.dispatcher.poll(background_only) { + if !self.dispatcher.tick(background_only) { if awoken.swap(false, SeqCst) { continue; } @@ -216,11 +244,21 @@ impl BackgroundExecutor { self.dispatcher.as_test().unwrap().simulate_random_delay() } + #[cfg(any(test, feature = "test-support"))] + pub fn deprioritize(&self, task_label: TaskLabel) { + self.dispatcher.as_test().unwrap().deprioritize(task_label) + } + #[cfg(any(test, feature = "test-support"))] pub fn advance_clock(&self, duration: Duration) { self.dispatcher.as_test().unwrap().advance_clock(duration) } + #[cfg(any(test, feature = "test-support"))] + pub fn tick(&self) -> bool { + self.dispatcher.as_test().unwrap().tick(false) + } + #[cfg(any(test, feature = "test-support"))] pub fn run_until_parked(&self) { self.dispatcher.as_test().unwrap().run_until_parked() diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index a1898bfd6d..e1f039e309 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -343,7 +343,7 @@ where impl Bounds where - T: Clone + Debug + PartialOrd + Add + Sub + Default, + T: Clone + Debug + PartialOrd + Add + Sub + Default + Half, { pub fn intersects(&self, other: &Bounds) -> bool { let my_lower_right = self.lower_right(); @@ -362,6 +362,13 @@ where self.size.width = self.size.width.clone() + double_amount.clone(); self.size.height = self.size.height.clone() + double_amount; } + + pub fn center(&self) -> Point { + Point { + x: self.origin.x.clone() + self.size.width.clone().half(), + y: self.origin.y.clone() + self.size.height.clone().half(), + } + } } impl + Sub> Bounds { @@ -1211,6 +1218,46 @@ impl From<()> for Length { } } +pub trait Half { + fn half(&self) -> Self; +} + +impl Half for f32 { + fn half(&self) -> Self { + self / 2. + } +} + +impl Half for DevicePixels { + fn half(&self) -> Self { + Self(self.0 / 2) + } +} + +impl Half for ScaledPixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Pixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for Rems { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + +impl Half for GlobalPixels { + fn half(&self) -> Self { + Self(self.0 / 2.) + } +} + pub trait IsZero { fn is_zero(&self) -> bool; } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 3b98b846c4..88ecd52c03 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -49,11 +49,13 @@ pub use input::*; pub use interactive::*; pub use key_dispatch::*; pub use keymap::*; +pub use linkme; pub use platform::*; use private::Sealed; pub use refineable::*; pub use scene::*; pub use serde; +pub use serde_derive; pub use serde_json; pub use smallvec; pub use smol::Timer; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 962a030844..5fbf83bfba 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,6 +1,6 @@ use crate::{ - build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, - Keymap, Keystroke, KeystrokeMatcher, WindowContext, + Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, + Keystroke, KeystrokeMatcher, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -10,7 +10,6 @@ use std::{ rc::Rc, sync::Arc, }; -use util::ResultExt; #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct DispatchNodeId(usize); @@ -22,6 +21,7 @@ pub(crate) struct DispatchTree { focusable_node_ids: HashMap, keystroke_matchers: HashMap, KeystrokeMatcher>, keymap: Arc>, + action_registry: Rc, } #[derive(Default)] @@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener { } impl DispatchTree { - pub fn new(keymap: Arc>) -> Self { + pub fn new(keymap: Arc>, action_registry: Rc) -> Self { Self { node_stack: Vec::new(), context_stack: Vec::new(), @@ -49,6 +49,7 @@ impl DispatchTree { focusable_node_ids: HashMap::default(), keystroke_matchers: HashMap::default(), keymap, + action_registry, } } @@ -153,7 +154,9 @@ impl DispatchTree { for node_id in self.dispatch_path(*node) { let node = &self.nodes[node_id.0]; for DispatchActionListener { action_type, .. } in &node.action_listeners { - actions.extend(build_action_from_type(action_type).log_err()); + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + actions.extend(self.action_registry.build_action_type(action_type).ok()); } } } diff --git a/crates/gpui2/src/keymap/keymap.rs b/crates/gpui2/src/keymap/keymap.rs index 989ee7a8d5..8152693c07 100644 --- a/crates/gpui2/src/keymap/keymap.rs +++ b/crates/gpui2/src/keymap/keymap.rs @@ -1,7 +1,10 @@ -use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke}; +use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction}; use collections::HashSet; use smallvec::SmallVec; -use std::{any::TypeId, collections::HashMap}; +use std::{ + any::{Any, TypeId}, + collections::HashMap, +}; #[derive(Copy, Clone, Eq, PartialEq, Default)] pub struct KeymapVersion(usize); @@ -37,20 +40,19 @@ impl Keymap { } pub fn add_bindings>(&mut self, bindings: T) { - // todo!("no action") - // let no_action_id = (NoAction {}).id(); + let no_action_id = &(NoAction {}).type_id(); let mut new_bindings = Vec::new(); - let has_new_disabled_keystrokes = false; + let mut has_new_disabled_keystrokes = false; for binding in bindings { - // if binding.action().id() == no_action_id { - // has_new_disabled_keystrokes |= self - // .disabled_keystrokes - // .entry(binding.keystrokes) - // .or_default() - // .insert(binding.context_predicate); - // } else { - new_bindings.push(binding); - // } + if binding.action.type_id() == *no_action_id { + has_new_disabled_keystrokes |= self + .disabled_keystrokes + .entry(binding.keystrokes) + .or_default() + .insert(binding.context_predicate); + } else { + new_bindings.push(binding); + } } if has_new_disabled_keystrokes { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 00ce3340f8..3027c05fbd 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -8,7 +8,7 @@ use crate::{ point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, - SharedString, Size, + SharedString, Size, TaskLabel, }; use anyhow::{anyhow, bail}; use async_task::Runnable; @@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow { pub trait PlatformDispatcher: Send + Sync { fn is_main_thread(&self) -> bool; - fn dispatch(&self, runnable: Runnable); + fn dispatch(&self, runnable: Runnable, label: Option); fn dispatch_on_main_thread(&self, runnable: Runnable); fn dispatch_after(&self, duration: Duration, runnable: Runnable); - fn poll(&self, background_only: bool) -> bool; + fn tick(&self, background_only: bool) -> bool; fn park(&self); fn unparker(&self) -> Unparker; diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index 68c0e3b4f5..2fb0eef3e5 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/crates/gpui2/src/platform/mac/dispatcher.rs @@ -2,7 +2,7 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] -use crate::PlatformDispatcher; +use crate::{PlatformDispatcher, TaskLabel}; use async_task::Runnable; use objc::{ class, msg_send, @@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher { is_main_thread == YES } - fn dispatch(&self, runnable: Runnable) { + fn dispatch(&self, runnable: Runnable, _: Option) { unsafe { dispatch_async_f( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0), @@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher { } } - fn poll(&self, _background_only: bool) -> bool { + fn tick(&self, _background_only: bool) -> bool { false } diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index 155f3097fe..9ef0f321b6 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -343,10 +343,10 @@ impl MacTextSystemState { // Construct the attributed string, converting UTF8 ranges to UTF16 ranges. let mut string = CFMutableAttributedString::new(); { - string.replace_str(&CFString::new(text), CFRange::init(0, 0)); + string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0)); let utf16_line_len = string.char_len() as usize; - let mut ix_converter = StringIndexConverter::new(text); + let mut ix_converter = StringIndexConverter::new(text.as_ref()); for run in font_runs { let utf8_end = ix_converter.utf8_ix + run.len; let utf16_start = ix_converter.utf16_ix; @@ -390,7 +390,7 @@ impl MacTextSystemState { }; let font_id = self.id_for_native_font(font); - let mut ix_converter = StringIndexConverter::new(text); + let mut ix_converter = StringIndexConverter::new(text.as_ref()); let mut glyphs = SmallVec::new(); for ((glyph_id, position), glyph_utf16_ix) in run .glyphs() @@ -413,11 +413,11 @@ impl MacTextSystemState { let typographic_bounds = line.get_typographic_bounds(); LineLayout { + runs, + font_size, width: typographic_bounds.width.into(), ascent: typographic_bounds.ascent.into(), descent: typographic_bounds.descent.into(), - runs, - font_size, len: text.len(), } } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index d07df3d94b..03782d13a8 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) }; if let Some(mut event) = event { - let synthesized_second_event = match &mut event { + match &mut event { InputEvent::MouseDown( event @ MouseDownEvent { button: MouseButton::Left, @@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { .. }, ) => { + // On mac, a ctrl-left click should be handled as a right click. *event = MouseDownEvent { button: MouseButton::Right, modifiers: Modifiers { @@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { click_count: 1, ..*event }; - - Some(InputEvent::MouseDown(MouseDownEvent { - button: MouseButton::Right, - ..*event - })) } // Because we map a ctrl-left_down to a right_down -> right_up let's ignore // the ctrl-left_up to avoid having a mismatch in button down/up events if the // user is still holding ctrl when releasing the left mouse button - InputEvent::MouseUp(MouseUpEvent { - button: MouseButton::Left, - modifiers: Modifiers { control: true, .. }, - .. - }) => { - lock.synthetic_drag_counter += 1; - return; + InputEvent::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; } - _ => None, + _ => {} }; match &event { @@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { if let Some(mut callback) = lock.event_callback.take() { drop(lock); callback(event); - if let Some(event) = synthesized_second_event { - callback(event); - } window_state.lock().event_callback = Some(callback); } } diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 258c484063..e77c1c0529 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -1,7 +1,7 @@ -use crate::PlatformDispatcher; +use crate::{PlatformDispatcher, TaskLabel}; use async_task::Runnable; use backtrace::Backtrace; -use collections::{HashMap, VecDeque}; +use collections::{HashMap, HashSet, VecDeque}; use parking::{Parker, Unparker}; use parking_lot::Mutex; use rand::prelude::*; @@ -28,12 +28,14 @@ struct TestDispatcherState { random: StdRng, foreground: HashMap>, background: Vec, + deprioritized_background: Vec, delayed: Vec<(Duration, Runnable)>, time: Duration, is_main_thread: bool, next_id: TestDispatcherId, allow_parking: bool, waiting_backtrace: Option, + deprioritized_task_labels: HashSet, } impl TestDispatcher { @@ -43,12 +45,14 @@ impl TestDispatcher { random, foreground: HashMap::default(), background: Vec::new(), + deprioritized_background: Vec::new(), delayed: Vec::new(), time: Duration::ZERO, is_main_thread: true, next_id: TestDispatcherId(1), allow_parking: false, waiting_backtrace: None, + deprioritized_task_labels: Default::default(), }; TestDispatcher { @@ -101,8 +105,15 @@ impl TestDispatcher { } } + pub fn deprioritize(&self, task_label: TaskLabel) { + self.state + .lock() + .deprioritized_task_labels + .insert(task_label); + } + pub fn run_until_parked(&self) { - while self.poll(false) {} + while self.tick(false) {} } pub fn parking_allowed(&self) -> bool { @@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher { self.state.lock().is_main_thread } - fn dispatch(&self, runnable: Runnable) { - self.state.lock().background.push(runnable); + fn dispatch(&self, runnable: Runnable, label: Option) { + { + let mut state = self.state.lock(); + if label.map_or(false, |label| { + state.deprioritized_task_labels.contains(&label) + }) { + state.deprioritized_background.push(runnable); + } else { + state.background.push(runnable); + } + } self.unparker.unpark(); } @@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher { state.delayed.insert(ix, (next_time, runnable)); } - fn poll(&self, background_only: bool) -> bool { + fn tick(&self, background_only: bool) -> bool { let mut state = self.state.lock(); while let Some((deadline, _)) = state.delayed.first() { @@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher { }; let background_len = state.background.len(); + let runnable; + let main_thread; if foreground_len == 0 && background_len == 0 { - return false; - } - - let main_thread = state.random.gen_ratio( - foreground_len as u32, - (foreground_len + background_len) as u32, - ); - let was_main_thread = state.is_main_thread; - state.is_main_thread = main_thread; - - let runnable = if main_thread { - let state = &mut *state; - let runnables = state - .foreground - .values_mut() - .filter(|runnables| !runnables.is_empty()) - .choose(&mut state.random) - .unwrap(); - runnables.pop_front().unwrap() + let deprioritized_background_len = state.deprioritized_background.len(); + if deprioritized_background_len == 0 { + return false; + } + let ix = state.random.gen_range(0..deprioritized_background_len); + main_thread = false; + runnable = state.deprioritized_background.swap_remove(ix); } else { - let ix = state.random.gen_range(0..background_len); - state.background.swap_remove(ix) + main_thread = state.random.gen_ratio( + foreground_len as u32, + (foreground_len + background_len) as u32, + ); + if main_thread { + let state = &mut *state; + runnable = state + .foreground + .values_mut() + .filter(|runnables| !runnables.is_empty()) + .choose(&mut state.random) + .unwrap() + .pop_front() + .unwrap(); + } else { + let ix = state.random.gen_range(0..background_len); + runnable = state.background.swap_remove(ix); + }; }; + let was_main_thread = state.is_main_thread; + state.is_main_thread = main_thread; drop(state); runnable.run(); - self.state.lock().is_main_thread = was_main_thread; true diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 5d9dd5d804..1b0cabb401 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -203,6 +203,7 @@ impl TextStyle { style: self.font_style, }, color: self.color, + background_color: None, underline: self.underline.clone(), } } diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index c7031fcb4d..b3d7a96aff 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -3,20 +3,20 @@ mod line; mod line_layout; mod line_wrapper; -use anyhow::anyhow; pub use font_features::*; pub use line::*; pub use line_layout::*; pub use line_wrapper::*; -use smallvec::SmallVec; use crate::{ px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size, UnderlineStyle, }; +use anyhow::anyhow; use collections::HashMap; use core::fmt; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use smallvec::SmallVec; use std::{ cmp, fmt::{Debug, Display, Formatter}, @@ -151,13 +151,79 @@ impl TextSystem { } } - pub fn layout_text( + pub fn layout_line( &self, text: &str, font_size: Pixels, runs: &[TextRun], + ) -> Result> { + let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); + for run in runs.iter() { + let font_id = self.font_id(&run.font)?; + if let Some(last_run) = font_runs.last_mut() { + if last_run.font_id == font_id { + last_run.len += run.len; + continue; + } + } + font_runs.push(FontRun { + len: run.len, + font_id, + }); + } + + let layout = self + .line_layout_cache + .layout_line(&text, font_size, &font_runs); + + font_runs.clear(); + self.font_runs_pool.lock().push(font_runs); + + Ok(layout) + } + + pub fn shape_line( + &self, + text: SharedString, + font_size: Pixels, + runs: &[TextRun], + ) -> Result { + debug_assert!( + text.find('\n').is_none(), + "text argument should not contain newlines" + ); + + let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); + for run in runs { + if let Some(last_run) = decoration_runs.last_mut() { + if last_run.color == run.color && last_run.underline == run.underline { + last_run.len += run.len as u32; + continue; + } + } + decoration_runs.push(DecorationRun { + len: run.len as u32, + color: run.color, + underline: run.underline.clone(), + }); + } + + let layout = self.layout_line(text.as_ref(), font_size, runs)?; + + Ok(ShapedLine { + layout, + text, + decoration_runs, + }) + } + + pub fn shape_text( + &self, + text: &str, // todo!("pass a SharedString and preserve it when passed a single line?") + font_size: Pixels, + runs: &[TextRun], wrap_width: Option, - ) -> Result> { + ) -> Result> { let mut runs = runs.iter().cloned().peekable(); let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default(); @@ -210,10 +276,11 @@ impl TextSystem { let layout = self .line_layout_cache - .layout_line(&line_text, font_size, &font_runs, wrap_width); - lines.push(Line { + .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width); + lines.push(WrappedLine { layout, - decorations: decoration_runs, + decoration_runs, + text: SharedString::from(line_text), }); line_start = line_end + 1; // Skip `\n` character. @@ -384,6 +451,7 @@ pub struct TextRun { pub len: usize, pub font: Font, pub color: Hsla, + pub background_color: Option, pub underline: Option, } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 707274ad33..d05ae9468d 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -1,5 +1,5 @@ use crate::{ - black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size, + black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; @@ -14,23 +14,17 @@ pub struct DecorationRun { } #[derive(Clone, Default, Debug, Deref, DerefMut)] -pub struct Line { +pub struct ShapedLine { #[deref] #[deref_mut] - pub(crate) layout: Arc, - pub(crate) decorations: SmallVec<[DecorationRun; 32]>, + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, } -impl Line { - pub fn size(&self, line_height: Pixels) -> Size { - size( - self.layout.width, - line_height * (self.layout.wrap_boundaries.len() + 1), - ) - } - - pub fn wrap_count(&self) -> usize { - self.layout.wrap_boundaries.len() +impl ShapedLine { + pub fn len(&self) -> usize { + self.layout.len } pub fn paint( @@ -39,75 +33,84 @@ impl Line { line_height: Pixels, cx: &mut WindowContext, ) -> Result<()> { - let padding_top = - (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.; - let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent); + paint_line( + origin, + &self.layout, + line_height, + &self.decoration_runs, + None, + &[], + cx, + )?; - let mut style_runs = self.decorations.iter(); - let mut wraps = self.layout.wrap_boundaries.iter().peekable(); - let mut run_end = 0; - let mut color = black(); - let mut current_underline: Option<(Point, UnderlineStyle)> = None; - let text_system = cx.text_system().clone(); + Ok(()) + } +} - let mut glyph_origin = origin; - let mut prev_glyph_position = Point::default(); - for (run_ix, run) in self.layout.layout.runs.iter().enumerate() { - let max_glyph_size = text_system - .bounding_box(run.font_id, self.layout.layout.font_size)? - .size; +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; +impl WrappedLine { + pub fn len(&self) -> usize { + self.layout.len() + } - if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { - wraps.next(); - if let Some((underline_origin, underline_style)) = current_underline.take() { - cx.paint_underline( - underline_origin, - glyph_origin.x - underline_origin.x, - &underline_style, - )?; - } + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + &self.decoration_runs, + self.wrap_width, + &self.wrap_boundaries, + cx, + )?; - glyph_origin.x = origin.x; - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; + Ok(()) + } +} - let mut finished_underline: Option<(Point, UnderlineStyle)> = None; - if glyph.index >= run_end { - if let Some(style_run) = style_runs.next() { - if let Some((_, underline_style)) = &mut current_underline { - if style_run.underline.as_ref() != Some(underline_style) { - finished_underline = current_underline.take(); - } - } - if let Some(run_underline) = style_run.underline.as_ref() { - current_underline.get_or_insert(( - point( - glyph_origin.x, - origin.y - + baseline_offset.y - + (self.layout.layout.descent * 0.618), - ), - UnderlineStyle { - color: Some(run_underline.color.unwrap_or(style_run.color)), - thickness: run_underline.thickness, - wavy: run_underline.wavy, - }, - )); - } +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + decoration_runs: &[DecorationRun], + wrap_width: Option, + wrap_boundaries: &[WrapBoundary], + cx: &mut WindowContext<'_>, +) -> Result<()> { + let padding_top = (line_height - layout.ascent - layout.descent) / 2.; + let baseline_offset = point(px(0.), padding_top + layout.ascent); + let mut decoration_runs = decoration_runs.iter(); + let mut wraps = wrap_boundaries.iter().peekable(); + let mut run_end = 0; + let mut color = black(); + let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let text_system = cx.text_system().clone(); + let mut glyph_origin = origin; + let mut prev_glyph_position = Point::default(); + for (run_ix, run) in layout.runs.iter().enumerate() { + let max_glyph_size = text_system + .bounding_box(run.font_id, layout.font_size)? + .size; - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = self.layout.text.len(); - finished_underline = current_underline.take(); - } - } + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; - if let Some((underline_origin, underline_style)) = finished_underline { + if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { + wraps.next(); + if let Some((underline_origin, underline_style)) = current_underline.take() { cx.paint_underline( underline_origin, glyph_origin.x - underline_origin.x, @@ -115,42 +118,84 @@ impl Line { )?; } - let max_glyph_bounds = Bounds { - origin: glyph_origin, - size: max_glyph_size, - }; + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; - let content_mask = cx.content_mask(); - if max_glyph_bounds.intersects(&content_mask.bounds) { - if glyph.is_emoji { - cx.paint_emoji( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - self.layout.layout.font_size, - )?; - } else { - cx.paint_glyph( - glyph_origin + baseline_offset, - run.font_id, - glyph.id, - self.layout.layout.font_size, - color, - )?; + let mut finished_underline: Option<(Point, UnderlineStyle)> = None; + if glyph.index >= run_end { + if let Some(style_run) = decoration_runs.next() { + if let Some((_, underline_style)) = &mut current_underline { + if style_run.underline.as_ref() != Some(underline_style) { + finished_underline = current_underline.take(); + } } + if let Some(run_underline) = style_run.underline.as_ref() { + current_underline.get_or_insert(( + point( + glyph_origin.x, + origin.y + baseline_offset.y + (layout.descent * 0.618), + ), + UnderlineStyle { + color: Some(run_underline.color.unwrap_or(style_run.color)), + thickness: run_underline.thickness, + wavy: run_underline.wavy, + }, + )); + } + + run_end += style_run.len as usize; + color = style_run.color; + } else { + run_end = layout.len; + finished_underline = current_underline.take(); + } + } + + if let Some((underline_origin, underline_style)) = finished_underline { + cx.paint_underline( + underline_origin, + glyph_origin.x - underline_origin.x, + &underline_style, + )?; + } + + let max_glyph_bounds = Bounds { + origin: glyph_origin, + size: max_glyph_size, + }; + + let content_mask = cx.content_mask(); + if max_glyph_bounds.intersects(&content_mask.bounds) { + if glyph.is_emoji { + cx.paint_emoji( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + )?; + } else { + cx.paint_glyph( + glyph_origin + baseline_offset, + run.font_id, + glyph.id, + layout.font_size, + color, + )?; } } } - - if let Some((underline_start, underline_style)) = current_underline.take() { - let line_end_x = origin.x + self.layout.layout.width; - cx.paint_underline( - underline_start, - line_end_x - underline_start.x, - &underline_style, - )?; - } - - Ok(()) } + + if let Some((underline_start, underline_style)) = current_underline.take() { + let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); + cx.paint_underline( + underline_start, + line_end_x - underline_start.x, + &underline_style, + )?; + } + + Ok(()) } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 7e9176caca..a5cf814a8c 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -1,5 +1,4 @@ -use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString}; -use derive_more::{Deref, DerefMut}; +use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use smallvec::SmallVec; use std::{ @@ -149,13 +148,11 @@ impl LineLayout { } } -#[derive(Deref, DerefMut, Default, Debug)] +#[derive(Default, Debug)] pub struct WrappedLineLayout { - #[deref] - #[deref_mut] - pub layout: LineLayout, - pub text: SharedString, + pub unwrapped_layout: Arc, pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + pub wrap_width: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -164,31 +161,74 @@ pub struct WrapBoundary { pub glyph_ix: usize, } +impl WrappedLineLayout { + pub fn len(&self) -> usize { + self.unwrapped_layout.len + } + + pub fn width(&self) -> Pixels { + self.wrap_width + .unwrap_or(Pixels::MAX) + .min(self.unwrapped_layout.width) + } + + pub fn size(&self, line_height: Pixels) -> Size { + Size { + width: self.width(), + height: line_height * (self.wrap_boundaries.len() + 1), + } + } + + pub fn ascent(&self) -> Pixels { + self.unwrapped_layout.ascent + } + + pub fn descent(&self) -> Pixels { + self.unwrapped_layout.descent + } + + pub fn wrap_boundaries(&self) -> &[WrapBoundary] { + &self.wrap_boundaries + } + + pub fn font_size(&self) -> Pixels { + self.unwrapped_layout.font_size + } + + pub fn runs(&self) -> &[ShapedRun] { + &self.unwrapped_layout.runs + } +} + pub(crate) struct LineLayoutCache { - prev_frame: Mutex>>, - curr_frame: RwLock>>, + previous_frame: Mutex>>, + current_frame: RwLock>>, + previous_frame_wrapped: Mutex>>, + current_frame_wrapped: RwLock>>, platform_text_system: Arc, } impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> Self { Self { - prev_frame: Mutex::new(HashMap::new()), - curr_frame: RwLock::new(HashMap::new()), + previous_frame: Mutex::default(), + current_frame: RwLock::default(), + previous_frame_wrapped: Mutex::default(), + current_frame_wrapped: RwLock::default(), platform_text_system, } } pub fn start_frame(&self) { - let mut prev_frame = self.prev_frame.lock(); - let mut curr_frame = self.curr_frame.write(); + let mut prev_frame = self.previous_frame.lock(); + let mut curr_frame = self.current_frame.write(); std::mem::swap(&mut *prev_frame, &mut *curr_frame); curr_frame.clear(); } - pub fn layout_line( + pub fn layout_wrapped_line( &self, - text: &SharedString, + text: &str, font_size: Pixels, runs: &[FontRun], wrap_width: Option, @@ -199,34 +239,66 @@ impl LineLayoutCache { runs, wrap_width, } as &dyn AsCacheKeyRef; - let curr_frame = self.curr_frame.upgradable_read(); - if let Some(layout) = curr_frame.get(key) { + + let current_frame = self.current_frame_wrapped.upgradable_read(); + if let Some(layout) = current_frame.get(key) { return layout.clone(); } - let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame); - if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) { - curr_frame.insert(key, layout.clone()); + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) { + current_frame.insert(key, layout.clone()); layout } else { - let layout = self.platform_text_system.layout_line(text, font_size, runs); - let wrap_boundaries = wrap_width - .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width)) - .unwrap_or_default(); - let wrapped_line = Arc::new(WrappedLineLayout { - layout, - text: text.clone(), + let unwrapped_layout = self.layout_line(text, font_size, runs); + let wrap_boundaries = if let Some(wrap_width) = wrap_width { + unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width) + } else { + SmallVec::new() + }; + let layout = Arc::new(WrappedLineLayout { + unwrapped_layout, wrap_boundaries, + wrap_width, }); - let key = CacheKey { - text: text.clone(), + text: text.into(), font_size, runs: SmallVec::from(runs), wrap_width, }; - curr_frame.insert(key, wrapped_line.clone()); - wrapped_line + current_frame.insert(key, layout.clone()); + layout + } + } + + pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc { + let key = &CacheKeyRef { + text, + font_size, + runs, + wrap_width: None, + } as &dyn AsCacheKeyRef; + + let current_frame = self.current_frame.upgradable_read(); + if let Some(layout) = current_frame.get(key) { + return layout.clone(); + } + + let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame); + if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) { + current_frame.insert(key, layout.clone()); + layout + } else { + let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs)); + let key = CacheKey { + text: text.into(), + font_size, + runs: SmallVec::from(runs), + wrap_width: None, + }; + current_frame.insert(key, layout.clone()); + layout } } } @@ -243,7 +315,7 @@ trait AsCacheKeyRef { #[derive(Eq)] struct CacheKey { - text: SharedString, + text: String, font_size: Pixels, runs: SmallVec<[FontRun; 1]>, wrap_width: Option, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b0d9d07df2..6d07f06d94 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -185,10 +185,27 @@ impl Drop for FocusHandle { } } +/// FocusableView allows users of your view to easily +/// focus it (using cx.focus_view(view)) pub trait FocusableView: Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: Render { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle; +} + +pub struct Dismiss; +impl EventEmitter for T {} + +impl FocusableView for T { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.focus_handle(cx) + } +} + // Holds the state for a specific window. pub struct Window { pub(crate) handle: AnyWindowHandle, @@ -311,8 +328,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), - current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), + previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), + current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), default_prevented: true, @@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> { result } + #[must_use] /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which /// layout is being requested, along with the layout ids of any children. This method is called during /// calls to the `Element::layout` trait method and enables any element to participate in layout. @@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = mouse_move.position; InputEvent::MouseMove(mouse_move) } + InputEvent::MouseDown(mouse_down) => { + self.window.mouse_position = mouse_down.position; + InputEvent::MouseDown(mouse_down) + } + InputEvent::MouseUp(mouse_up) => { + self.window.mouse_position = mouse_up.position; + InputEvent::MouseUp(mouse_up) + } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. InputEvent::FileDrop(file_drop) => match file_drop { diff --git a/crates/gpui2/tests/action_macros.rs b/crates/gpui2/tests/action_macros.rs new file mode 100644 index 0000000000..49064ffd86 --- /dev/null +++ b/crates/gpui2/tests/action_macros.rs @@ -0,0 +1,45 @@ +use serde_derive::Deserialize; + +#[test] +fn test_derive() { + use gpui2 as gpui; + + #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)] + struct AnotherTestAction; + + #[gpui2_macros::register_action] + #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)] + struct RegisterableAction {} + + impl gpui::Action for RegisterableAction { + fn boxed_clone(&self) -> Box { + todo!() + } + + fn as_any(&self) -> &dyn std::any::Any { + todo!() + } + + fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { + todo!() + } + + fn name(&self) -> &str { + todo!() + } + + fn debug_name() -> &'static str + where + Self: Sized, + { + todo!() + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + todo!() + } + } +} diff --git a/crates/gpui2_macros/Cargo.toml b/crates/gpui2_macros/Cargo.toml index eb44334095..aab669c1b7 100644 --- a/crates/gpui2_macros/Cargo.toml +++ b/crates/gpui2_macros/Cargo.toml @@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs" proc-macro = true [dependencies] -syn = "1.0.72" +syn = { version = "1.0.72", features = ["full"] } quote = "1.0.9" proc-macro2 = "1.0.66" diff --git a/crates/gpui2_macros/src/action.rs b/crates/gpui2_macros/src/action.rs index 564f35d6a4..abc75a8759 100644 --- a/crates/gpui2_macros/src/action.rs +++ b/crates/gpui2_macros/src/action.rs @@ -15,48 +15,81 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, Error}; + +use crate::register_action::register_action; + +pub fn action(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); -pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as DeriveInput); let name = &input.ident; - let attrs = input - .attrs - .into_iter() - .filter(|attr| !attr.path.is_ident("action")) - .collect::>(); - let attributes = quote! { - #[gpui::register_action] - #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)] - #(#attrs)* + if input.generics.lt_token.is_some() { + return Error::new(name.span(), "Actions must be a concrete type") + .into_compile_error() + .into(); + } + + let is_unit_struct = match input.data { + syn::Data::Struct(struct_data) => struct_data.fields.is_empty(), + syn::Data::Enum(_) => false, + syn::Data::Union(_) => false, }; - let visibility = input.vis; - let output = match input.data { - syn::Data::Struct(ref struct_data) => match &struct_data.fields { - syn::Fields::Named(_) | syn::Fields::Unnamed(_) => { - let fields = &struct_data.fields; - quote! { - #attributes - #visibility struct #name #fields - } + let build_impl = if is_unit_struct { + quote! { + Ok(std::boxed::Box::new(Self {})) + } + } else { + quote! { + Ok(std::boxed::Box::new(gpui::serde_json::from_value::(value)?)) + } + }; + + let register_action = register_action(&name); + + let output = quote! { + const _: fn() = || { + fn assert_impl gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {} + assert_impl::<#name>(); + }; + + impl gpui::Action for #name { + fn name(&self) -> &'static str + { + ::std::any::type_name::<#name>() } - syn::Fields::Unit => { - quote! { - #attributes - #visibility struct #name; - } + + fn debug_name() -> &'static str + where + Self: ::std::marker::Sized + { + ::std::any::type_name::<#name>() } - }, - syn::Data::Enum(ref enum_data) => { - let variants = &enum_data.variants; - quote! { - #attributes - #visibility enum #name { #variants } + + fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box> + where + Self: ::std::marker::Sized { + #build_impl + } + + fn partial_eq(&self, action: &dyn gpui::Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } + + fn boxed_clone(&self) -> std::boxed::Box { + ::std::boxed::Box::new(self.clone()) + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self } } - _ => panic!("Expected a struct or an enum."), + + #register_action }; TokenStream::from(output) diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 80b67e1a12..3ce8373689 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream { style_helpers::style_helpers(args) } -#[proc_macro_attribute] -pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream { - action::action(attr, item) +#[proc_macro_derive(Action)] +pub fn action(input: TokenStream) -> TokenStream { + action::action(input) } #[proc_macro_attribute] pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream { - register_action::register_action(attr, item) + register_action::register_action_macro(attr, item) } #[proc_macro_derive(Component, attributes(component))] diff --git a/crates/gpui2_macros/src/register_action.rs b/crates/gpui2_macros/src/register_action.rs index 68c39ad9bd..3d398c873c 100644 --- a/crates/gpui2_macros/src/register_action.rs +++ b/crates/gpui2_macros/src/register_action.rs @@ -12,22 +12,76 @@ // gpui2::register_action_builder::() // } use proc_macro::TokenStream; +use proc_macro2::Ident; use quote::{format_ident, quote}; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, Error}; -pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); - let type_name = &input.ident; - let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase()); + let registration = register_action(&input.ident); - let expanded = quote! { + let has_action_derive = input + .attrs + .iter() + .find(|attr| { + (|| { + let meta = attr.parse_meta().ok()?; + meta.path().is_ident("derive").then(|| match meta { + syn::Meta::Path(_) => None, + syn::Meta::NameValue(_) => None, + syn::Meta::List(list) => list + .nested + .iter() + .find(|list| match list { + syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"), + syn::NestedMeta::Lit(_) => false, + }) + .map(|_| true), + })? + })() + .unwrap_or(false) + }) + .is_some(); + + if has_action_derive { + return Error::new( + input.ident.span(), + "The Action derive macro has already registered this action", + ) + .into_compile_error() + .into(); + } + + TokenStream::from(quote! { #input - #[allow(non_snake_case)] - #[gpui::ctor] - fn #ctor_fn_name() { - gpui::register_action::<#type_name>() - } - }; - TokenStream::from(expanded) + #registration + }) +} + +pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream { + let static_slice_name = + format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase()); + + let action_builder_fn_name = format_ident!( + "__gpui_actions_builder_{}", + type_name.to_string().to_lowercase() + ); + + quote! { + #[doc(hidden)] + #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)] + #[linkme(crate = gpui::linkme)] + static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name; + + /// This is an auto generated function, do not use. + #[doc(hidden)] + fn #action_builder_fn_name() -> gpui::ActionData { + gpui::ActionData { + name: ::std::any::type_name::<#type_name>(), + type_id: ::std::any::TypeId::of::<#type_name>(), + build: <#type_name as gpui::Action>::build, + } + } + } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0194123bd2..7feffbf3ed 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -17,7 +17,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use futures::FutureExt as _; +use futures::channel::oneshot; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; use parking_lot::Mutex; @@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *}; use theme::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; -use util::{RangeExt, TryFutureExt as _}; +use util::RangeExt; #[cfg(any(test, feature = "test-support"))] pub use {tree_sitter_rust, tree_sitter_typescript}; @@ -62,6 +62,7 @@ pub struct Buffer { saved_mtime: SystemTime, transaction_depth: usize, was_dirty_before_starting_transaction: Option, + reload_task: Option>>, language: Option>, autoindent_requests: Vec>, pending_autoindent: Option>, @@ -509,6 +510,7 @@ impl Buffer { saved_mtime, saved_version: buffer.version(), saved_version_fingerprint: buffer.as_rope().fingerprint(), + reload_task: None, transaction_depth: 0, was_dirty_before_starting_transaction: None, text: buffer, @@ -608,37 +610,52 @@ impl Buffer { cx.notify(); } - pub fn reload(&mut self, cx: &mut ModelContext) -> Task>> { - cx.spawn(|this, mut cx| async move { - if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| { + pub fn reload( + &mut self, + cx: &mut ModelContext, + ) -> oneshot::Receiver> { + let (tx, rx) = futures::channel::oneshot::channel(); + let prev_version = self.text.version(); + self.reload_task = Some(cx.spawn(|this, mut cx| async move { + let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| { let file = this.file.as_ref()?.as_local()?; Some((file.mtime(), file.load(cx))) - }) { - let new_text = new_text.await?; - let diff = this - .read_with(&cx, |this, cx| this.diff(new_text, cx)) - .await; - this.update(&mut cx, |this, cx| { - if this.version() == diff.base_version { - this.finalize_last_transaction(); - this.apply_diff(diff, cx); - if let Some(transaction) = this.finalize_last_transaction().cloned() { - this.did_reload( - this.version(), - this.as_rope().fingerprint(), - this.line_ending(), - new_mtime, - cx, - ); - return Ok(Some(transaction)); - } - } - Ok(None) - }) - } else { - Ok(None) - } - }) + }) else { + return Ok(()); + }; + + let new_text = new_text.await?; + let diff = this + .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx)) + .await; + this.update(&mut cx, |this, cx| { + if this.version() == diff.base_version { + this.finalize_last_transaction(); + this.apply_diff(diff, cx); + tx.send(this.finalize_last_transaction().cloned()).ok(); + + this.did_reload( + this.version(), + this.as_rope().fingerprint(), + this.line_ending(), + new_mtime, + cx, + ); + } else { + this.did_reload( + prev_version, + Rope::text_fingerprint(&new_text), + this.line_ending(), + this.saved_mtime, + cx, + ); + } + + this.reload_task.take(); + }); + Ok(()) + })); + rx } pub fn did_reload( @@ -667,13 +684,8 @@ impl Buffer { cx.notify(); } - pub fn file_updated( - &mut self, - new_file: Arc, - cx: &mut ModelContext, - ) -> Task<()> { + pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { let mut file_changed = false; - let mut task = Task::ready(()); if let Some(old_file) = self.file.as_ref() { if new_file.path() != old_file.path() { @@ -693,8 +705,7 @@ impl Buffer { file_changed = true; if !self.is_dirty() { - let reload = self.reload(cx).log_err().map(drop); - task = cx.foreground().spawn(reload); + self.reload(cx).close(); } } } @@ -708,7 +719,6 @@ impl Buffer { cx.emit(Event::FileHandleChanged); cx.notify(); } - task } pub fn diff_base(&self) -> Option<&str> { diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 2c8c55d577..51ed192b99 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -16,8 +16,9 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use futures::FutureExt as _; -use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task}; +use futures::channel::oneshot; +use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel}; +use lazy_static::lazy_static; use lsp::LanguageServerId; use parking_lot::Mutex; use similar::{ChangeTag, TextDiff}; @@ -44,23 +45,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *}; use theme::SyntaxTheme; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; -use util::{RangeExt, TryFutureExt as _}; +use util::RangeExt; #[cfg(any(test, feature = "test-support"))] pub use {tree_sitter_rust, tree_sitter_typescript}; pub use lsp::DiagnosticSeverity; +lazy_static! { + pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new(); +} + pub struct Buffer { text: TextBuffer, diff_base: Option, git_diff: git::diff::BufferDiff, file: Option>, - saved_version: clock::Global, - saved_version_fingerprint: RopeFingerprint, + /// The mtime of the file when this buffer was last loaded from + /// or saved to disk. saved_mtime: SystemTime, + /// The version vector when this buffer was last loaded from + /// or saved to disk. + saved_version: clock::Global, + /// A hash of the current contents of the buffer's file. + file_fingerprint: RopeFingerprint, transaction_depth: usize, was_dirty_before_starting_transaction: Option, + reload_task: Option>>, language: Option>, autoindent_requests: Vec>, pending_autoindent: Option>, @@ -380,8 +391,7 @@ impl Buffer { .ok_or_else(|| anyhow!("missing line_ending"))?, )); this.saved_version = proto::deserialize_version(&message.saved_version); - this.saved_version_fingerprint = - proto::deserialize_fingerprint(&message.saved_version_fingerprint)?; + this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?; this.saved_mtime = message .saved_mtime .ok_or_else(|| anyhow!("invalid saved_mtime"))? @@ -397,7 +407,7 @@ impl Buffer { diff_base: self.diff_base.as_ref().map(|h| h.to_string()), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, saved_version: proto::serialize_version(&self.saved_version), - saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint), + saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint), saved_mtime: Some(self.saved_mtime.into()), } } @@ -467,7 +477,8 @@ impl Buffer { Self { saved_mtime, saved_version: buffer.version(), - saved_version_fingerprint: buffer.as_rope().fingerprint(), + file_fingerprint: buffer.as_rope().fingerprint(), + reload_task: None, transaction_depth: 0, was_dirty_before_starting_transaction: None, text: buffer, @@ -533,7 +544,7 @@ impl Buffer { } pub fn saved_version_fingerprint(&self) -> RopeFingerprint { - self.saved_version_fingerprint + self.file_fingerprint } pub fn saved_mtime(&self) -> SystemTime { @@ -561,43 +572,58 @@ impl Buffer { cx: &mut ModelContext, ) { self.saved_version = version; - self.saved_version_fingerprint = fingerprint; + self.file_fingerprint = fingerprint; self.saved_mtime = mtime; cx.emit(Event::Saved); cx.notify(); } - pub fn reload(&mut self, cx: &mut ModelContext) -> Task>> { - cx.spawn(|this, mut cx| async move { - if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| { + pub fn reload( + &mut self, + cx: &mut ModelContext, + ) -> oneshot::Receiver> { + let (tx, rx) = futures::channel::oneshot::channel(); + let prev_version = self.text.version(); + self.reload_task = Some(cx.spawn(|this, mut cx| async move { + let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| { let file = this.file.as_ref()?.as_local()?; Some((file.mtime(), file.load(cx))) - })? { - let new_text = new_text.await?; - let diff = this - .update(&mut cx, |this, cx| this.diff(new_text, cx))? - .await; - this.update(&mut cx, |this, cx| { - if this.version() == diff.base_version { - this.finalize_last_transaction(); - this.apply_diff(diff, cx); - if let Some(transaction) = this.finalize_last_transaction().cloned() { - this.did_reload( - this.version(), - this.as_rope().fingerprint(), - this.line_ending(), - new_mtime, - cx, - ); - return Some(transaction); - } - } - None - }) - } else { - Ok(None) - } - }) + })? + else { + return Ok(()); + }; + + let new_text = new_text.await?; + let diff = this + .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))? + .await; + this.update(&mut cx, |this, cx| { + if this.version() == diff.base_version { + this.finalize_last_transaction(); + this.apply_diff(diff, cx); + tx.send(this.finalize_last_transaction().cloned()).ok(); + + this.did_reload( + this.version(), + this.as_rope().fingerprint(), + this.line_ending(), + new_mtime, + cx, + ); + } else { + this.did_reload( + prev_version, + Rope::text_fingerprint(&new_text), + this.line_ending(), + this.saved_mtime, + cx, + ); + } + + this.reload_task.take(); + }) + })); + rx } pub fn did_reload( @@ -609,14 +635,14 @@ impl Buffer { cx: &mut ModelContext, ) { self.saved_version = version; - self.saved_version_fingerprint = fingerprint; + self.file_fingerprint = fingerprint; self.text.set_line_ending(line_ending); self.saved_mtime = mtime; if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) { file.buffer_reloaded( self.remote_id(), &self.saved_version, - self.saved_version_fingerprint, + self.file_fingerprint, self.line_ending(), self.saved_mtime, cx, @@ -626,13 +652,8 @@ impl Buffer { cx.notify(); } - pub fn file_updated( - &mut self, - new_file: Arc, - cx: &mut ModelContext, - ) -> Task<()> { + pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { let mut file_changed = false; - let mut task = Task::ready(()); if let Some(old_file) = self.file.as_ref() { if new_file.path() != old_file.path() { @@ -652,8 +673,7 @@ impl Buffer { file_changed = true; if !self.is_dirty() { - let reload = self.reload(cx).log_err().map(drop); - task = cx.background_executor().spawn(reload); + self.reload(cx).close(); } } } @@ -667,7 +687,6 @@ impl Buffer { cx.emit(Event::FileHandleChanged); cx.notify(); } - task } pub fn diff_base(&self) -> Option<&str> { @@ -1118,36 +1137,72 @@ impl Buffer { pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { let old_text = self.as_rope().clone(); let base_version = self.version(); - cx.background_executor().spawn(async move { - let old_text = old_text.to_string(); - let line_ending = LineEnding::detect(&new_text); - LineEnding::normalize(&mut new_text); - let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str()); - let mut edits = Vec::new(); - let mut offset = 0; - let empty: Arc = "".into(); - for change in diff.iter_all_changes() { - let value = change.value(); - let end_offset = offset + value.len(); - match change.tag() { - ChangeTag::Equal => { - offset = end_offset; + cx.background_executor() + .spawn_labeled(*BUFFER_DIFF_TASK, async move { + let old_text = old_text.to_string(); + let line_ending = LineEnding::detect(&new_text); + LineEnding::normalize(&mut new_text); + + let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str()); + let empty: Arc = "".into(); + + let mut edits = Vec::new(); + let mut old_offset = 0; + let mut new_offset = 0; + let mut last_edit: Option<(Range, Range)> = None; + for change in diff.iter_all_changes().map(Some).chain([None]) { + if let Some(change) = &change { + let len = change.value().len(); + match change.tag() { + ChangeTag::Equal => { + old_offset += len; + new_offset += len; + } + ChangeTag::Delete => { + let old_end_offset = old_offset + len; + if let Some((last_old_range, _)) = &mut last_edit { + last_old_range.end = old_end_offset; + } else { + last_edit = + Some((old_offset..old_end_offset, new_offset..new_offset)); + } + old_offset = old_end_offset; + } + ChangeTag::Insert => { + let new_end_offset = new_offset + len; + if let Some((_, last_new_range)) = &mut last_edit { + last_new_range.end = new_end_offset; + } else { + last_edit = + Some((old_offset..old_offset, new_offset..new_end_offset)); + } + new_offset = new_end_offset; + } + } } - ChangeTag::Delete => { - edits.push((offset..end_offset, empty.clone())); - offset = end_offset; - } - ChangeTag::Insert => { - edits.push((offset..offset, value.into())); + + if let Some((old_range, new_range)) = &last_edit { + if old_offset > old_range.end + || new_offset > new_range.end + || change.is_none() + { + let text = if new_range.is_empty() { + empty.clone() + } else { + new_text[new_range.clone()].into() + }; + edits.push((old_range.clone(), text)); + last_edit.take(); + } } } - } - Diff { - base_version, - line_ending, - edits, - } - }) + + Diff { + base_version, + line_ending, + edits, + } + }) } /// Spawn a background task that searches the buffer for any whitespace @@ -1231,12 +1286,12 @@ impl Buffer { } pub fn is_dirty(&self) -> bool { - self.saved_version_fingerprint != self.as_rope().fingerprint() + self.file_fingerprint != self.as_rope().fingerprint() || self.file.as_ref().map_or(false, |file| file.is_deleted()) } pub fn has_conflict(&self) -> bool { - self.saved_version_fingerprint != self.as_rope().fingerprint() + self.file_fingerprint != self.as_rope().fingerprint() && self .file .as_ref() diff --git a/crates/live_kit_client2/Cargo.toml b/crates/live_kit_client2/Cargo.toml index b606434b05..073c0017b0 100644 --- a/crates/live_kit_client2/Cargo.toml +++ b/crates/live_kit_client2/Cargo.toml @@ -10,7 +10,7 @@ path = "src/live_kit_client2.rs" doctest = false [[example]] -name = "test_app" +name = "test_app2" [features] test-support = [ diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app2.rs similarity index 98% rename from crates/live_kit_client2/examples/test_app.rs rename to crates/live_kit_client2/examples/test_app2.rs index 0b9e54f9b0..00aec53baf 100644 --- a/crates/live_kit_client2/examples/test_app.rs +++ b/crates/live_kit_client2/examples/test_app2.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use futures::StreamExt; -use gpui::KeyBinding; +use gpui::{Action, KeyBinding}; use live_kit_client2::{ LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, }; @@ -10,7 +10,7 @@ use log::LevelFilter; use serde_derive::Deserialize; use simplelog::SimpleLogger; -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)] struct Quit; fn main() { diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 72a2f812e9..3491fc3d4a 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task, - UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, + MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, TextColor}; @@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static { ) -> Self::ListItem; } +impl FocusableView for Picker { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let editor = cx.build_view(|cx| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 322b2ae894..ab6cbd88c0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -6190,7 +6190,7 @@ impl Project { .log_err(); } - buffer.file_updated(Arc::new(new_file), cx).detach(); + buffer.file_updated(Arc::new(new_file), cx); } } }); @@ -7182,7 +7182,7 @@ impl Project { .ok_or_else(|| anyhow!("no such worktree"))?; let file = File::from_proto(file, worktree, cx)?; buffer.update(cx, |buffer, cx| { - buffer.file_updated(Arc::new(file), cx).detach(); + buffer.file_updated(Arc::new(file), cx); }); this.detect_language_for_buffer(&buffer, cx); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 785ce58bb8..d59885225a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -959,7 +959,7 @@ impl LocalWorktree { buffer_handle.update(&mut cx, |buffer, cx| { if has_changed_file { - buffer.file_updated(new_file, cx).detach(); + buffer.file_updated(new_file, cx); } }); } diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 61ad500a73..f2e47b7184 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -6262,7 +6262,7 @@ impl Project { .log_err(); } - buffer.file_updated(Arc::new(new_file), cx).detach(); + buffer.file_updated(Arc::new(new_file), cx); } } }); @@ -7256,7 +7256,7 @@ impl Project { .ok_or_else(|| anyhow!("no such worktree"))?; let file = File::from_proto(file, worktree, cx)?; buffer.update(cx, |buffer, cx| { - buffer.file_updated(Arc::new(file), cx).detach(); + buffer.file_updated(Arc::new(file), cx); }); this.detect_language_for_buffer(&buffer, cx); } diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 97b6ed9c74..9eb9a49e49 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } +#[gpui::test(iterations = 30)] +async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/dir", + json!({ + "file1": "the original contents", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap()); + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx)) + .await + .unwrap(); + + // Simulate buffer diffs being slow, so that they don't complete before + // the next file change occurs. + cx.executor().deprioritize(*language::BUFFER_DIFF_TASK); + + // Change the buffer's file on disk, and then wait for the file change + // to be detected by the worktree, so that the buffer starts reloading. + fs.save( + "/dir/file1".as_ref(), + &"the first contents".into(), + Default::default(), + ) + .await + .unwrap(); + worktree.next_event(cx); + + // Change the buffer's file again. Depending on the random seed, the + // previous file change may still be in progress. + fs.save( + "/dir/file1".as_ref(), + &"the second contents".into(), + Default::default(), + ) + .await + .unwrap(); + worktree.next_event(cx); + + cx.executor().run_until_parked(); + let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap(); + buffer.read_with(cx, |buffer, _| { + let buffer_text = buffer.text(); + if buffer_text == on_disk_text { + assert!( + !buffer.is_dirty() && !buffer.has_conflict(), + "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}", + ); + } + // If the file change occurred while the buffer was processing the first + // change, the buffer will be in a conflicting state. + else { + assert!( + buffer.is_dirty() && buffer.has_conflict(), + "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}" + ); + } + }); +} + #[gpui::test] async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 9444dd9185..a020e8db4c 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -276,6 +276,7 @@ struct ShareState { _maintain_remote_snapshot: Task>, } +#[derive(Clone)] pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet), @@ -961,7 +962,7 @@ impl LocalWorktree { buffer_handle.update(&mut cx, |buffer, cx| { if has_changed_file { - buffer.file_updated(new_file, cx).detach(); + buffer.file_updated(new_file, cx); } })?; } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index f33633afb9..7a455fe8ce 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -1579,7 +1579,7 @@ mod tests { path::{Path, PathBuf}, sync::atomic::{self, AtomicUsize}, }; - use workspace::{pane, AppState}; + use workspace::AppState; #[gpui::test] async fn test_visible_list(cx: &mut gpui::TestAppContext) { @@ -2785,7 +2785,7 @@ mod tests { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); init_settings(cx); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); editor::init_settings(cx); crate::init((), cx); @@ -2798,11 +2798,10 @@ mod tests { fn init_test_with_editor(cx: &mut TestAppContext) { cx.update(|cx| { let app_state = AppState::test(cx); - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); init_settings(cx); language::init(cx); editor::init(cx); - pane::init(cx); crate::init((), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 9c764c468e..4cea1d4759 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -41,6 +41,10 @@ impl Rope { Self::default() } + pub fn text_fingerprint(text: &str) -> RopeFingerprint { + bromberg_sl2::hash_strict(text.as_bytes()) + } + pub fn append(&mut self, rope: Rope) { let mut chunks = rope.chunks.cursor::<()>(); chunks.next(&()); @@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary { fn from(text: &'a str) -> Self { Self { text: TextSummary::from(text), - fingerprint: bromberg_sl2::hash_strict(text.as_bytes()), + fingerprint: Rope::text_fingerprint(text), } } } diff --git a/crates/rope2/src/rope2.rs b/crates/rope2/src/rope2.rs index 9c764c468e..4cea1d4759 100644 --- a/crates/rope2/src/rope2.rs +++ b/crates/rope2/src/rope2.rs @@ -41,6 +41,10 @@ impl Rope { Self::default() } + pub fn text_fingerprint(text: &str) -> RopeFingerprint { + bromberg_sl2::hash_strict(text.as_bytes()) + } + pub fn append(&mut self, rope: Rope) { let mut chunks = rope.chunks.cursor::<()>(); chunks.next(&()); @@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary { fn from(text: &'a str) -> Self { Self { text: TextSummary::from(text), - fingerprint: bromberg_sl2::hash_strict(text.as_bytes()), + fingerprint: Rope::text_fingerprint(text), } } } diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 9f279864ee..93635935cb 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -73,9 +73,9 @@ impl KeymapFile { "Expected first item in array to be a string." ))); }; - gpui::build_action(&name, Some(data)) + cx.build_action(&name, Some(data)) } - Value::String(name) => gpui::build_action(&name, None), + Value::String(name) => cx.build_action(&name, None), Value::Null => Ok(no_action()), _ => { return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index 6f2c8d374f..fc4ad5882e 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -16,6 +16,9 @@ pub fn test_settings() -> String { .unwrap(); util::merge_non_null_json_value_into( serde_json::json!({ + "ui_font_family": "Courier", + "ui_font_features": {}, + "ui_font_size": 14, "buffer_font_family": "Courier", "buffer_font_features": {}, "buffer_font_size": 14, diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c4c1d75eac..a0bc7cd72f 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -60,13 +60,12 @@ fn main() { .unwrap(); cx.set_global(store); - theme2::init(cx); + theme2::init(theme2::LoadThemes::All, cx); let selector = story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace)); let theme_registry = cx.global::(); - let mut theme_settings = ThemeSettings::get_global(cx).clone(); theme_settings.active_theme = theme_registry.get(&theme_name).unwrap(); ThemeSettings::override_global(theme_settings, cx); @@ -114,6 +113,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() + .font("Zed Mono") .child(self.story.clone()) } } diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml new file mode 100644 index 0000000000..8b04e4d44b --- /dev/null +++ b/crates/storybook3/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "storybook3" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "storybook" +path = "src/storybook3.rs" + +[dependencies] +anyhow.workspace = true + +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2", features = ["stories"] } +theme = { package = "theme2", path = "../theme2", features = ["stories"] } +settings = { package = "settings2", path = "../settings2"} diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs new file mode 100644 index 0000000000..291f8ce2ac --- /dev/null +++ b/crates/storybook3/src/storybook3.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use gpui::AssetSource; +use gpui::{ + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + WindowOptions, +}; +use settings::{default_settings, Settings, SettingsStore}; +use std::borrow::Cow; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::{prelude::*, ContextMenuStory}; + +struct Assets; + +impl AssetSource for Assets { + fn load(&self, _path: &str) -> Result> { + todo!(); + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +fn main() { + let asset_source = Arc::new(Assets); + gpui::App::production(asset_source).run(move |cx| { + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); + ui::settings::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds { + origin: Default::default(), + size: size(px(1500.), px(780.)).into(), + }), + ..Default::default() + }, + move |cx| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); + + cx.build_view(|cx| TestView { + story: cx.build_view(|_| ContextMenuStory).into(), + }) + }, + ); + + cx.activate(true); + }) +} + +struct TestView { + story: AnyView, +} + +impl Render for TestView { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .flex() + .flex_col() + .size_full() + .font("Helvetica") + .child(self.story.clone()) + } +} diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index fbb1bd5352..944cd912be 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -304,13 +304,13 @@ impl TerminalPanel { .pane .read(cx) .items() - .map(|item| item.id().as_u64()) + .map(|item| item.item_id().as_u64()) .collect::>(); let active_item_id = self .pane .read(cx) .active_item() - .map(|item| item.id().as_u64()); + .map(|item| item.item_id().as_u64()); let height = self.height; let width = self.width; self.pending_serialization = cx.background_executor().spawn( diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 2ed7c8f472..179e7508d4 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,7 +9,7 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div, + actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div, EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, @@ -32,7 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, ContextMenuItem, Label}, + ui::{ContextMenu, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -55,12 +55,10 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); -#[register_action] -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)] pub struct SendText(String); -#[register_action] -#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)] pub struct SendKeystroke(String); actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest); @@ -87,7 +85,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: Option, + context_menu: Option>, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -302,10 +300,14 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::new(vec![ - ContextMenuItem::entry(Label::new("Clear"), Clear), - ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }), - ])); + self.context_menu = Some(cx.build_view(|cx| { + ContextMenu::new(cx) + .entry(Label::new("Clear"), Box::new(Clear)) + .entry( + Label::new("Close"), + Box::new(CloseActiveItem { save_intent: None }), + ) + })); dbg!(&position); // todo!() // self.context_menu @@ -1126,7 +1128,7 @@ mod tests { pub async fn init_test(cx: &mut TestAppContext) -> (Model, View) { let params = cx.update(AppState::test); cx.update(|cx| { - theme::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); Project::init_settings(cx); language::init(cx); }); diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 19af0ede51..919dd1b109 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -100,6 +100,11 @@ impl ThemeRegistry { .ok_or_else(|| anyhow!("theme not found: {}", name)) .cloned() } + + pub fn load_user_themes(&mut self) { + #[cfg(not(feature = "importing-themes"))] + self.insert_user_theme_familes(crate::all_user_themes()); + } } impl Default for ThemeRegistry { @@ -110,9 +115,6 @@ impl Default for ThemeRegistry { this.insert_theme_families([one_family()]); - #[cfg(not(feature = "importing-themes"))] - this.insert_user_theme_familes(crate::all_user_themes()); - this } } diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 5e3329ffa1..01951f2ed0 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -34,6 +34,10 @@ pub struct ThemeSettingsContent { #[serde(default)] pub ui_font_size: Option, #[serde(default)] + pub ui_font_family: Option, + #[serde(default)] + pub ui_font_features: Option, + #[serde(default)] pub buffer_font_family: Option, #[serde(default)] pub buffer_font_size: Option, @@ -117,13 +121,13 @@ impl settings::Settings for ThemeSettings { user_values: &[&Self::FileContent], cx: &mut AppContext, ) -> Result { - let themes = cx.default_global::>(); + let themes = cx.default_global::(); let mut this = Self { - ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), + ui_font_size: defaults.ui_font_size.unwrap().into(), ui_font: Font { - family: "Helvetica".into(), - features: Default::default(), + family: defaults.ui_font_family.clone().unwrap().into(), + features: defaults.ui_font_features.clone().unwrap(), weight: Default::default(), style: Default::default(), }, @@ -149,6 +153,13 @@ impl settings::Settings for ThemeSettings { this.buffer_font.features = value; } + if let Some(value) = value.ui_font_family { + this.ui_font.family = value.into(); + } + if let Some(value) = value.ui_font_features { + this.ui_font.features = value; + } + if let Some(value) = &value.theme { if let Some(theme) = themes.get(value).log_err() { this.active_theme = theme; diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index b6790b5a6f..05e41ba368 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -31,8 +31,25 @@ pub enum Appearance { Dark, } -pub fn init(cx: &mut AppContext) { +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum LoadThemes { + /// Only load the base theme. + /// + /// No user themes will be loaded. + JustBase, + + /// Load all of the built-in themes. + All, +} + +pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) { cx.set_global(ThemeRegistry::default()); + + match themes_to_load { + LoadThemes::JustBase => (), + LoadThemes::All => cx.global_mut::().load_user_themes(), + } + ThemeSettings::register(cx); } diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 397ce4f4c4..de055bcd5c 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -178,6 +178,7 @@ impl Button { .text_ui() .rounded_md() .bg(self.variant.bg_color(cx)) + .cursor_pointer() .hover(|style| style.bg(self.variant.bg_color_hover(cx))) .active(|style| style.bg(self.variant.bg_color_active(cx))); diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8f32c3ed56..d3214cbff1 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,82 +1,258 @@ -use crate::{prelude::*, ListItemVariant}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; +use gpui::{ + overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, +}; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - 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(self) -> ListItem { - match self { - ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label, action) => ListEntry::new(label) - .variant(ListItemVariant::Inset) - .on_click(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), - } - } - - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) - } - - pub fn separator() -> Self { - Self::Separator - } - - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) - } -} - -#[derive(Component, Clone)] pub struct ContextMenu { - items: Vec, + items: Vec, + focus_handle: FocusHandle, +} + +impl ManagedView for ContextMenu { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } } impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { + pub fn new(cx: &mut WindowContext) -> Self { Self { - items: items.into_iter().collect(), + items: Default::default(), + focus_handle: cx.focus_handle(), } } - // 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(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - v_stack() - .flex() - .bg(cx.theme().colors().elevated_surface_background) - .border() - .border_color(cx.theme().colors().border) - .child(List::new( - self.items - .into_iter() - .map(ContextMenuItem::to_list_item::) - .collect(), - )) - .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) + + pub fn header(mut self, title: impl Into) -> Self { + self.items.push(ListItem::Header(ListSubHeader::new(title))); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(ListItem::Separator(ListSeparator)); + self + } + + pub fn entry(mut self, label: Label, action: Box) -> Self { + self.items.push(ListEntry::new(label).action(action).into()); + self + } + + pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(Dismiss); + } + + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(Dismiss); + } +} + +impl Render for ContextMenu { + type Element = Div; + // todo!() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().elevation_2(cx).flex().flex_row().child( + v_stack() + .min_w(px(200.)) + .track_focus(&self.focus_handle) + .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)) + // .on_action(ContextMenu::select_first) + // .on_action(ContextMenu::select_last) + // .on_action(ContextMenu::select_next) + // .on_action(ContextMenu::select_prev) + .on_action(ContextMenu::confirm) + .on_action(ContextMenu::cancel) + .flex_none() + // .bg(cx.theme().colors().elevated_surface_background) + // .border() + // .border_color(cx.theme().colors().border) + .child(List::new(self.items.clone())), + ) + } +} + +pub struct MenuHandle { + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, + + anchor: Option, + attach: Option, +} + +impl MenuHandle { + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { + self.child_builder = Some(Box::new(|b| f(b).render())); + self + } + + /// anchor defines which corner of the menu to anchor to the attachment point + /// (by default the cursor position, but see attach) + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor = Some(anchor); + self + } + + /// attach defines which corner of the handle to attach the menu's anchor to + pub fn attach(mut self, attach: AnchorCorner) -> Self { + self.attach = Some(attach); + self + } +} + +pub fn menu_handle() -> MenuHandle { + MenuHandle { + id: None, + child_builder: None, + menu_builder: None, + anchor: None, + attach: None, + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + position: Rc>>, + child_layout_id: Option, + child_element: Option>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone().expect("menu_handle must have an id()")) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) + } else { + (Rc::default(), Rc::default()) + }; + + let mut menu_layout_id = None; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay::().snap_to_window(); + if let Some(anchor) = self.anchor { + overlay = overlay.anchor(anchor); + } + overlay = overlay.position(*position.borrow()); + + let mut view = overlay.child(menu.clone()).render(); + menu_layout_id = Some(view.layout(view_state, cx)); + view + }); + + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.borrow().is_some())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(view_state, cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + MenuHandleState { + menu, + position, + child_element, + child_layout_id, + menu_element, + }, + ) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + if let Some(child) = element_state.child_element.as_mut() { + child.paint(view_state, cx); + } + + if let Some(menu) = element_state.menu_element.as_mut() { + menu.paint(view_state, cx); + return; + } + + let Some(builder) = self.menu_builder.clone() else { + return; + }; + let menu = element_state.menu.clone(); + let position = element_state.position.clone(); + let attach = self.attach.clone(); + let child_layout_id = element_state.child_layout_id.clone(); + + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && bounds.contains_point(&event.position) + { + cx.stop_propagation(); + cx.prevent_default(); + + let new_menu = (builder)(view_state, cx); + let menu2 = menu.clone(); + cx.subscribe(&new_menu, move |this, modal, e, cx| match e { + &Dismiss => { + *menu2.borrow_mut() = None; + cx.notify(); + } + }) + .detach(); + *menu.borrow_mut() = Some(new_menu); + + *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { + attach + .unwrap() + .corner(cx.layout_bounds(child_layout_id.unwrap())) + } else { + cx.mouse_position() + }; + cx.notify(); + } + }); + } +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) } } -use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -84,7 +260,18 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{action, Div, Render}; + use gpui::{actions, Div, Render, VisualContext}; + + actions!(PrintCurrentDate); + + fn build_menu(cx: &mut WindowContext, header: impl Into) -> View { + cx.build_view(|cx| { + ContextMenu::new(cx).header(header).separator().entry( + Label::new("Print current time"), + PrintCurrentDate.boxed_clone(), + ) + }) + } pub struct ContextMenuStory; @@ -92,22 +279,84 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> 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("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()); } }) + .flex() + .flex_row() + .justify_between() + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test2") + .child(|is_open| { + Label::new(if is_open { + "TOP LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .menu(move |_, cx| build_menu(cx, "top left")), + ) + .child( + menu_handle() + .id("test1") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomLeft) + .attach(AnchorCorner::TopLeft) + .menu(move |_, cx| build_menu(cx, "bottom left")), + ), + ) + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test3") + .child(|is_open| { + Label::new(if is_open { + "TOP RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "top right")), + ) + .child( + menu_handle() + .id("test4") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomRight) + .attach(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "bottom right")), + ), + ) } } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 4408c51f62..9b8548e3f9 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement}; -use gpui::{prelude::*, AnyView, MouseButton}; +use gpui::{prelude::*, Action, AnyView, MouseButton}; use std::sync::Arc; struct IconButtonHandlers { @@ -19,6 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, + selected: bool, tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -31,6 +32,7 @@ impl IconButton { color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + selected: false, tooltip: None, handlers: IconButtonHandlers::default(), } @@ -56,6 +58,11 @@ impl IconButton { self } + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + pub fn tooltip( mut self, tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, @@ -69,14 +76,18 @@ impl IconButton { self } + pub fn action(self, action: Box) -> Self { + self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + } + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, - (InteractionState::Active, _) => TextColor::Error, + (InteractionState::Active, _) => TextColor::Selected, _ => self.color, }; - let (bg_color, bg_hover_color, bg_active_color) = match self.variant { + let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( cx.theme().colors().element_background, cx.theme().colors().element_hover, @@ -89,27 +100,32 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() .rounded_md() .p_1() .bg(bg_color) + .cursor_pointer() .hover(|style| style.bg(bg_hover_color)) .active(|style| style.bg(bg_active_color)) .child(IconElement::new(self.icon).color(icon_color)); if let Some(click_handler) = self.handlers.click.clone() { - button = button - .on_mouse_down(MouseButton::Left, move |state, event, cx| { - cx.stop_propagation(); - click_handler(state, cx); - }) - .cursor_pointer(); + button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { + cx.stop_propagation(); + click_handler(state, cx); + }) } if let Some(tooltip) = self.tooltip.take() { - button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + if !self.selected { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + } } button diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 04e036f365..8da5273bf5 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -81,13 +81,12 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{action, Div, Render}; + use gpui::{actions, Div, Render}; use itertools::Itertools; pub struct KeybindingStory; - #[action] - struct NoAction {} + actions!(NoAction); pub fn binding(key: &str) -> gpui::KeyBinding { gpui::KeyBinding::new(key, NoAction {}, None) diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 4b355dd5b6..b9508c5413 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -117,7 +117,7 @@ impl ListHeader { } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -172,7 +172,7 @@ pub enum ListEntrySize { Medium, } -#[derive(Component)] +#[derive(Component, Clone)] pub enum ListItem { Entry(ListEntry), Separator(ListSeparator), @@ -234,6 +234,24 @@ pub struct ListEntry { on_click: Option>, } +impl Clone for ListEntry { + fn clone(&self) -> Self { + Self { + disabled: self.disabled, + // TODO: Reintroduce this + // disclosure_control_style: DisclosureControlVisibility, + indent_level: self.indent_level, + label: self.label.clone(), + left_slot: self.left_slot.clone(), + overflow: self.overflow, + size: self.size, + toggle: self.toggle, + variant: self.variant, + on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()), + } + } +} + impl ListEntry { pub fn new(label: Label) -> Self { Self { @@ -249,7 +267,7 @@ impl ListEntry { } } - pub fn on_click(mut self, action: impl Into>) -> Self { + pub fn action(mut self, action: impl Into>) -> Self { self.on_click = Some(action.into()); self } diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index 94e38267f4..c98cfa012f 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -12,7 +12,6 @@ impl Story { .flex_col() .pt_2() .px_4() - .font("Zed Mono") .bg(cx.theme().colors().background) } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index d9911e6833..9037682807 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize}; fn elevated(this: E, cx: &mut ViewContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) + .z_index(index.z_index()) .rounded_lg() .border() .border_color(cx.theme().colors().border_variant) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 8e7f08252c..9603875aed 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,13 +1,14 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext, - WeakView, WindowContext, + div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, + EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, + Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use ui::{h_stack, IconButton, InteractionState, Tooltip}; +use theme2::ActiveTheme; +use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -216,11 +217,11 @@ impl Dock { // .map_or(false, |panel| panel.has_focus(cx)) // } - // pub fn panel(&self) -> Option> { - // self.panel_entries - // .iter() - // .find_map(|entry| entry.panel.as_any().clone().downcast()) - // } + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.to_any().clone().downcast().ok()) + } pub fn panel_index_for_type(&self) -> Option { self.panel_entries @@ -416,23 +417,13 @@ impl Dock { } } - // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { - // todo!() - // if let Some(active_entry) = self.visible_entry() { - // Empty::new() - // .into_any() - // .contained() - // .with_style(self.style(cx)) - // .resizable::( - // self.position.to_resize_handle_side(), - // active_entry.panel.size(cx), - // |_, _, _| {}, - // ) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // } + pub fn toggle_action(&self) -> Box { + match self.position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + } + } } impl Render for Dock { @@ -443,10 +434,16 @@ impl Render for Dock { let size = entry.panel.size(cx); div() + .border_color(cx.theme().colors().border) .map(|this| match self.position().axis() { Axis::Horizontal => this.w(px(size)).h_full(), Axis::Vertical => this.h(px(size)).w_full(), }) + .map(|this| match self.position() { + DockPosition::Left => this.border_r(), + DockPosition::Right => this.border_l(), + DockPosition::Bottom => this.border_t(), + }) .child(entry.panel.to_any()) } else { div() @@ -454,40 +451,6 @@ impl Render for Dock { } } -// todo!() -// impl View for Dock { -// fn ui_name() -> &'static str { -// "Dock" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// if let Some(active_entry) = self.visible_entry() { -// let style = self.style(cx); -// ChildView::new(active_entry.panel.as_any(), cx) -// .contained() -// .with_style(style) -// .resizable::( -// self.position.to_resize_handle_side(), -// active_entry.panel.size(cx), -// |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), -// ) -// .into_any() -// } else { -// Empty::new().into_any() -// } -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// if let Some(active_entry) = self.visible_entry() { -// cx.focus(active_entry.panel.as_any()); -// } else { -// cx.focus_parent(); -// } -// } -// } -// } - impl PanelButtons { pub fn new( dock: View, @@ -648,6 +611,7 @@ impl PanelButtons { // } // } +// here be kittens impl Render for PanelButtons { type Element = Div; @@ -657,6 +621,13 @@ impl Render for PanelButtons { let active_index = dock.active_panel_index; let is_open = dock.is_open; + let (menu_anchor, menu_attach) = match dock.position { + DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), + DockPosition::Bottom | DockPosition::Right => { + (AnchorCorner::BottomRight, AnchorCorner::TopRight) + } + }; + let buttons = dock .panel_entries .iter() @@ -664,18 +635,36 @@ impl Render for PanelButtons { .filter_map(|(i, panel)| { let icon = panel.panel.icon(cx)?; let name = panel.panel.persistent_name(); - let action = panel.panel.toggle_action(cx); - let action2 = action.boxed_clone(); - let mut button = IconButton::new(panel.panel.persistent_name(), icon) - .when(i == active_index, |el| el.state(InteractionState::Active)) - .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) - .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx)); + let mut button: IconButton = if i == active_index && is_open { + let action = dock.toggle_action(); + let tooltip: SharedString = + format!("Close {} dock", dock.position.to_label()).into(); + IconButton::new(name, icon) + .state(InteractionState::Active) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) + } else { + let action = panel.panel.toggle_action(cx); - Some(button) + IconButton::new(name, icon) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx)) + }; + + Some( + menu_handle() + .id(name) + .menu(move |_, cx| { + cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) + }) + .anchor(menu_anchor) + .attach(menu_attach) + .child(|is_open| button.selected(is_open)), + ) }); - h_stack().children(buttons) + h_stack().gap_0p5().children(buttons) } } diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 7252e7135a..5b37656d96 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -240,7 +240,7 @@ pub trait ItemHandle: 'static + Send { fn deactivated(&self, cx: &mut WindowContext); fn workspace_deactivated(&self, cx: &mut WindowContext); fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool; - fn id(&self) -> EntityId; + fn item_id(&self) -> EntityId; fn to_any(&self) -> AnyView; fn is_dirty(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; @@ -399,7 +399,7 @@ impl ItemHandle for View { if workspace .panes_by_item - .insert(self.id(), pane.downgrade()) + .insert(self.item_id(), pane.downgrade()) .is_none() { let mut pending_autosave = DelayedDebouncedEditAction::new(); @@ -410,7 +410,7 @@ impl ItemHandle for View { Some(cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = workspace .panes_by_item - .get(&item.id()) + .get(&item.item_id()) .and_then(|pane| pane.upgrade()) { pane @@ -463,7 +463,7 @@ impl ItemHandle for View { match event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { - pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) + pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx) }) .detach_and_log_err(cx); return; @@ -502,7 +502,7 @@ impl ItemHandle for View { // }) // .detach(); - let item_id = self.id(); + let item_id = self.item_id(); cx.observe_release(self, move |workspace, _, _| { workspace.panes_by_item.remove(&item_id); event_subscription.take(); @@ -527,7 +527,7 @@ impl ItemHandle for View { self.update(cx, |this, cx| this.navigate(data, cx)) } - fn id(&self) -> EntityId { + fn item_id(&self) -> EntityId { self.entity_id() } @@ -712,7 +712,7 @@ impl FollowableItemHandle for View { self.read(cx).remote_id().or_else(|| { client.peer_id().map(|creator| ViewId { creator, - id: self.id().as_u64(), + id: self.item_id().as_u64(), }) }) } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index cd5995d65e..8afd8317f9 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View, - ViewContext, WindowContext, + div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View, + ViewContext, }; use ui::{h_stack, v_stack}; @@ -15,14 +15,6 @@ pub struct ModalLayer { active_modal: Option, } -pub trait Modal: Render + EventEmitter { - fn focus(&self, cx: &mut WindowContext); -} - -pub enum ModalEvent { - Dismissed, -} - impl ModalLayer { pub fn new() -> Self { Self { active_modal: None } @@ -30,7 +22,7 @@ impl ModalLayer { pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: Modal, + V: ManagedView, B: FnOnce(&mut ViewContext) -> V, { if let Some(active_modal) = &self.active_modal { @@ -46,17 +38,15 @@ impl ModalLayer { pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) where - V: Modal, + V: ManagedView, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), - subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.hide_modal(cx), - }), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)), previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); - new_modal.update(cx, |modal, cx| modal.focus(cx)); + cx.focus_view(&new_modal); cx.notify(); } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 3b5ac37427..5967208fc2 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -7,9 +7,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View, - ViewContext, VisualContext, WeakView, WindowContext, + actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId, + EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, + Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize); // pub pane: WeakView, // } -#[register_action] -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)] #[serde(rename_all = "camelCase")] pub struct CloseActiveItem { pub save_intent: Option, } -#[register_action] -#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)] #[serde(rename_all = "camelCase")] pub struct CloseAllItems { pub save_intent: Option, @@ -104,29 +102,6 @@ actions!( const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; -pub fn init(cx: &mut AppContext) { - // todo!() - // cx.add_action(Pane::toggle_zoom); - // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - // pane.activate_item(action.0, true, true, cx); - // }); - // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { - // pane.activate_item(pane.items.len() - 1, true, true, cx); - // }); - // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { - // pane.activate_prev_item(true, cx); - // }); - // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { - // pane.activate_next_item(true, cx); - // }); - // cx.add_async_action(Pane::close_active_item); - // cx.add_async_action(Pane::close_inactive_items); - // cx.add_async_action(Pane::close_clean_items); - // cx.add_async_action(Pane::close_items_to_the_left); - // cx.add_async_action(Pane::close_items_to_the_right); - // cx.add_async_action(Pane::close_all_items); -} - pub enum Event { AddItem { item: Box }, ActivateItem { local: bool }, @@ -142,7 +117,10 @@ pub enum Event { impl fmt::Debug for Event { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Event::AddItem { item } => f.debug_struct("AddItem").field("item", &item.id()).finish(), + Event::AddItem { item } => f + .debug_struct("AddItem") + .field("item", &item.item_id()) + .finish(), Event::ActivateItem { local } => f .debug_struct("ActivateItem") .field("local", local) @@ -526,7 +504,7 @@ impl Pane { .0 .lock() .paths_by_item - .insert(item.id(), (project_path, abs_path)); + .insert(item.item_id(), (project_path, abs_path)); } } } @@ -550,7 +528,7 @@ impl Pane { }; let existing_item_index = self.items.iter().position(|existing_item| { - if existing_item.id() == item.id() { + if existing_item.item_id() == item.item_id() { true } else if existing_item.is_singleton(cx) { existing_item @@ -615,21 +593,21 @@ impl Pane { self.items.iter() } - // pub fn items_of_type(&self) -> impl '_ + Iterator> { - // self.items - // .iter() - // .filter_map(|item| item.as_any().clone().downcast()) - // } + pub fn items_of_type(&self) -> impl '_ + Iterator> { + self.items + .iter() + .filter_map(|item| item.to_any().downcast().ok()) + } pub fn active_item(&self) -> Option> { self.items.get(self.active_item_index).cloned() } - // pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - // self.items - // .get(self.active_item_index)? - // .pixel_position_of_cursor(cx) - // } + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + self.items + .get(self.active_item_index)? + .pixel_position_of_cursor(cx) + } pub fn item_for_entry( &self, @@ -646,24 +624,26 @@ impl Pane { } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.items.iter().position(|i| i.id() == item.id()) + self.items + .iter() + .position(|i| i.item_id() == item.item_id()) } - // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { - // // Potentially warn the user of the new keybinding - // let workspace_handle = self.workspace().clone(); - // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) - // .detach(); + // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // // Potentially warn the user of the new keybinding + // let workspace_handle = self.workspace().clone(); + // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) + // .detach(); - // if self.zoomed { - // cx.emit(Event::ZoomOut); - // } else if !self.items.is_empty() { - // if !self.has_focus { - // cx.focus_self(); - // } - // cx.emit(Event::ZoomIn); + // if self.zoomed { + // cx.emit(Event::ZoomOut); + // } else if !self.items.is_empty() { + // if !self.has_focus { + // cx.focus_self(); // } + // cx.emit(Event::ZoomIn); // } + // } pub fn activate_item( &mut self, @@ -691,9 +671,9 @@ impl Pane { if let Some(newly_active_item) = self.items.get(index) { self.activation_history .retain(|&previously_active_item_id| { - previously_active_item_id != newly_active_item.id() + previously_active_item_id != newly_active_item.item_id() }); - self.activation_history.push(newly_active_item.id()); + self.activation_history.push(newly_active_item.item_id()); } self.update_toolbar(cx); @@ -707,25 +687,25 @@ impl Pane { } } - // pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { - // let mut index = self.active_item_index; - // if index > 0 { - // index -= 1; - // } else if !self.items.is_empty() { - // index = self.items.len() - 1; - // } - // self.activate_item(index, activate_pane, activate_pane, cx); - // } + pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { + let mut index = self.active_item_index; + if index > 0 { + index -= 1; + } else if !self.items.is_empty() { + index = self.items.len() - 1; + } + self.activate_item(index, activate_pane, activate_pane, cx); + } - // pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { - // let mut index = self.active_item_index; - // if index + 1 < self.items.len() { - // index += 1; - // } else { - // index = 0; - // } - // self.activate_item(index, activate_pane, activate_pane, cx); - // } + pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { + let mut index = self.active_item_index; + if index + 1 < self.items.len() { + index += 1; + } else { + index = 0; + } + self.activate_item(index, activate_pane, activate_pane, cx); + } pub fn close_active_item( &mut self, @@ -735,7 +715,7 @@ impl Pane { if self.items.is_empty() { return None; } - let active_item_id = self.items[self.active_item_index].id(); + let active_item_id = self.items[self.active_item_index].item_id(); Some(self.close_item_by_id( active_item_id, action.save_intent.unwrap_or(SaveIntent::Close), @@ -752,106 +732,106 @@ impl Pane { self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close) } - // pub fn close_inactive_items( - // &mut self, - // _: &CloseInactiveItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + pub fn close_inactive_items( + &mut self, + _: &CloseInactiveItems, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_id != active_item_id - // })) - // } + let active_item_id = self.items[self.active_item_index].item_id(); + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_id != active_item_id + })) + } - // pub fn close_clean_items( - // &mut self, - // _: &CloseCleanItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // let item_ids: Vec<_> = self - // .items() - // .filter(|item| !item.is_dirty(cx)) - // .map(|item| item.id()) - // .collect(); - // Some(self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // })) - // } + pub fn close_clean_items( + &mut self, + _: &CloseCleanItems, + cx: &mut ViewContext, + ) -> Option>> { + let item_ids: Vec<_> = self + .items() + .filter(|item| !item.is_dirty(cx)) + .map(|item| item.item_id()) + .collect(); + Some(self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + })) + } - // pub fn close_items_to_the_left( - // &mut self, - // _: &CloseItemsToTheLeft, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_left_by_id(active_item_id, cx)) - // } + pub fn close_items_to_the_left( + &mut self, + _: &CloseItemsToTheLeft, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].item_id(); + Some(self.close_items_to_the_left_by_id(active_item_id, cx)) + } - // pub fn close_items_to_the_left_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + pub fn close_items_to_the_left_by_id( + &mut self, + item_id: EntityId, + cx: &mut ViewContext, + ) -> Task> { + let item_ids: Vec<_> = self + .items() + .take_while(|item| item.item_id() != item_id) + .map(|item| item.item_id()) + .collect(); + self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + }) + } - // pub fn close_items_to_the_right( - // &mut self, - // _: &CloseItemsToTheRight, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_items_to_the_right_by_id(active_item_id, cx)) - // } + pub fn close_items_to_the_right( + &mut self, + _: &CloseItemsToTheRight, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].item_id(); + Some(self.close_items_to_the_right_by_id(active_item_id, cx)) + } - // pub fn close_items_to_the_right_by_id( - // &mut self, - // item_id: usize, - // cx: &mut ViewContext, - // ) -> Task> { - // let item_ids: Vec<_> = self - // .items() - // .rev() - // .take_while(|item| item.id() != item_id) - // .map(|item| item.id()) - // .collect(); - // self.close_items(cx, SaveIntent::Close, move |item_id| { - // item_ids.contains(&item_id) - // }) - // } + pub fn close_items_to_the_right_by_id( + &mut self, + item_id: EntityId, + cx: &mut ViewContext, + ) -> Task> { + let item_ids: Vec<_> = self + .items() + .rev() + .take_while(|item| item.item_id() != item_id) + .map(|item| item.item_id()) + .collect(); + self.close_items(cx, SaveIntent::Close, move |item_id| { + item_ids.contains(&item_id) + }) + } - // pub fn close_all_items( - // &mut self, - // action: &CloseAllItems, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + pub fn close_all_items( + &mut self, + action: &CloseAllItems, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } - // Some( - // self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { - // true - // }), - // ) - // } + Some( + self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| { + true + }), + ) + } pub(super) fn file_names_for_prompt( items: &mut dyn Iterator>, @@ -898,7 +878,7 @@ impl Pane { let mut items_to_close = Vec::new(); let mut dirty_items = Vec::new(); for item in &self.items { - if should_close(item.id()) { + if should_close(item.item_id()) { items_to_close.push(item.boxed_clone()); if item.is_dirty(cx) { dirty_items.push(item.boxed_clone()); @@ -951,7 +931,7 @@ impl Pane { for item in workspace.items(cx) { if !items_to_close .iter() - .any(|item_to_close| item_to_close.id() == item.id()) + .any(|item_to_close| item_to_close.item_id() == item.item_id()) { let other_project_item_ids = item.project_item_model_ids(cx); project_item_ids.retain(|id| !other_project_item_ids.contains(id)); @@ -979,7 +959,11 @@ impl Pane { // Remove the item from the pane. pane.update(&mut cx, |pane, cx| { - if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { + if let Some(item_ix) = pane + .items + .iter() + .position(|i| i.item_id() == item.item_id()) + { pane.remove_item(item_ix, false, cx); } })?; @@ -997,7 +981,7 @@ impl Pane { cx: &mut ViewContext, ) { self.activation_history - .retain(|&history_entry| history_entry != self.items[item_index].id()); + .retain(|&history_entry| history_entry != self.items[item_index].item_id()); if item_index == self.active_item_index { let index_to_activate = self @@ -1005,7 +989,7 @@ impl Pane { .pop() .and_then(|last_activated_item| { self.items.iter().enumerate().find_map(|(index, item)| { - (item.id() == last_activated_item).then_some(index) + (item.item_id() == last_activated_item).then_some(index) }) }) // We didn't have a valid activation history entry, so fallback @@ -1022,7 +1006,9 @@ impl Pane { let item = self.items.remove(item_index); - cx.emit(Event::RemoveItem { item_id: item.id() }); + cx.emit(Event::RemoveItem { + item_id: item.item_id(), + }); if self.items.is_empty() { item.deactivated(cx); self.update_toolbar(cx); @@ -1043,16 +1029,20 @@ impl Pane { .0 .lock() .paths_by_item - .get(&item.id()) + .get(&item.item_id()) .and_then(|(_, abs_path)| abs_path.clone()); self.nav_history .0 .lock() .paths_by_item - .insert(item.id(), (path, abs_path)); + .insert(item.item_id(), (path, abs_path)); } else { - self.nav_history.0.lock().paths_by_item.remove(&item.id()); + self.nav_history + .0 + .lock() + .paths_by_item + .remove(&item.item_id()); } if self.items.is_empty() && self.zoomed { @@ -1325,7 +1315,7 @@ impl Pane { ) -> Option<()> { let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { - Some((i, item.id())) + Some((i, item.item_id())) } else { None } @@ -1356,10 +1346,10 @@ impl Pane { ) -> impl Component { let label = item.tab_content(Some(detail), cx); let close_icon = || { - let id = item.id(); + let id = item.item_id(); div() - .id(item.id()) + .id(item.item_id()) .invisible() .group_hover("", |style| style.visible()) .child(IconButton::new("close_tab", Icon::Close).on_click( @@ -1389,7 +1379,7 @@ impl Pane { div() .group("") - .id(item.id()) + .id(item.item_id()) .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) @@ -1916,8 +1906,27 @@ impl Render for Pane { .on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)) .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)) .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)) + // cx.add_action(Pane::toggle_zoom); + // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { + // pane.activate_item(action.0, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| { + // pane.activate_item(pane.items.len() - 1, true, true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { + // pane.activate_prev_item(true, cx); + // }); + // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| { + // pane.activate_next_item(true, cx); + // }); + // cx.add_async_action(Pane::close_active_item); + // cx.add_async_action(Pane::close_inactive_items); + // cx.add_async_action(Pane::close_clean_items); + // cx.add_async_action(Pane::close_items_to_the_left); + // cx.add_async_action(Pane::close_items_to_the_right); + // cx.add_async_action(Pane::close_all_items); .size_full() - .on_action(|pane: &mut Self, action, cx| { + .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| { pane.close_active_item(action, cx) .map(|task| task.detach_and_log_err(cx)); }) diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs index ef3a5f08fc..6d1c112b71 100644 --- a/crates/workspace2/src/searchable.rs +++ b/crates/workspace2/src/searchable.rs @@ -240,7 +240,7 @@ impl From<&Box> for AnyView { impl PartialEq for Box { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() + self.item_id() == other.item_id() } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index ca771140e9..379dd84e7a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,11 +29,12 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, - AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, - ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, + AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model, + ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -49,7 +50,7 @@ pub use persistence::{ WorkspaceDb, DB, }; use postage::stream::Stream; -use project2::{Project, ProjectEntryId, ProjectPath, Worktree}; +use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; use settings2::Settings; use status_bar::StatusBar; @@ -57,7 +58,7 @@ pub use status_bar::StatusItemView; use std::{ any::TypeId, borrow::Cow, - env, + cmp, env, path::{Path, PathBuf}, sync::{atomic::AtomicUsize, Arc}, time::Duration, @@ -85,8 +86,8 @@ lazy_static! { .and_then(parse_pixel_position_env_var); } -// #[derive(Clone, PartialEq)] -// pub struct RemoveWorktreeFromProject(pub WorktreeId); +#[derive(Clone, PartialEq)] +pub struct RemoveWorktreeFromProject(pub WorktreeId); actions!( Open, @@ -115,40 +116,40 @@ actions!( CloseAllDocks, ); -// #[derive(Clone, PartialEq)] -// pub struct OpenPaths { -// pub paths: Vec, -// } +#[derive(Clone, PartialEq)] +pub struct OpenPaths { + pub paths: Vec, +} -// #[derive(Clone, Deserialize, PartialEq)] -// pub struct ActivatePane(pub usize); +#[derive(Clone, Deserialize, PartialEq, Action)] +pub struct ActivatePane(pub usize); -// #[derive(Clone, Deserialize, PartialEq)] -// pub struct ActivatePaneInDirection(pub SplitDirection); +#[derive(Clone, Deserialize, PartialEq, Action)] +pub struct ActivatePaneInDirection(pub SplitDirection); -// #[derive(Clone, Deserialize, PartialEq)] -// pub struct SwapPaneInDirection(pub SplitDirection); +#[derive(Clone, Deserialize, PartialEq, Action)] +pub struct SwapPaneInDirection(pub SplitDirection); -// #[derive(Clone, Deserialize, PartialEq)] -// pub struct NewFileInDirection(pub SplitDirection); +#[derive(Clone, Deserialize, PartialEq, Action)] +pub struct NewFileInDirection(pub SplitDirection); -// #[derive(Clone, PartialEq, Debug, Deserialize)] -// #[serde(rename_all = "camelCase")] -// pub struct SaveAll { -// pub save_intent: Option, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Action)] +#[serde(rename_all = "camelCase")] +pub struct SaveAll { + pub save_intent: Option, +} -// #[derive(Clone, PartialEq, Debug, Deserialize)] -// #[serde(rename_all = "camelCase")] -// pub struct Save { -// pub save_intent: Option, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Action)] +#[serde(rename_all = "camelCase")] +pub struct Save { + pub save_intent: Option, +} -// #[derive(Clone, PartialEq, Debug, Deserialize, Default)] -// #[serde(rename_all = "camelCase")] -// pub struct CloseAllItemsAndPanes { -// pub save_intent: Option, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItemsAndPanes { + pub save_intent: Option, +} #[derive(Deserialize)] pub struct Toast { @@ -195,26 +196,11 @@ impl Clone for Toast { } } -#[register_action] -#[derive(Debug, Default, Clone, Deserialize, PartialEq)] +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)] pub struct OpenTerminal { pub working_directory: PathBuf, } -// impl_actions!( -// workspace, -// [ -// ActivatePane, -// ActivatePaneInDirection, -// SwapPaneInDirection, -// NewFileInDirection, -// Toast, -// SaveAll, -// Save, -// CloseAllItemsAndPanes, -// ] -// ); - pub type WorkspaceId = i64; pub fn init_settings(cx: &mut AppContext) { @@ -224,7 +210,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); - pane::init(cx); notifications::init(cx); // cx.add_global_action({ @@ -356,7 +341,7 @@ impl AppState { let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); - theme2::init(cx); + theme2::init(theme2::LoadThemes::JustBase, cx); client2::init(&client, cx); crate::init_settings(cx); @@ -425,6 +410,7 @@ pub enum Event { } pub struct Workspace { + window_self: WindowHandle, weak_self: WeakView, workspace_actions: Vec) -> Div>>, zoomed: Option, @@ -457,6 +443,8 @@ pub struct Workspace { pane_history_timestamp: Arc, } +impl EventEmitter for Workspace {} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ViewId { pub creator: PeerId, @@ -534,8 +522,8 @@ impl Workspace { ) }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); - // todo!() - // cx.focus(¢er_pane); + + cx.focus_view(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); let window_handle = cx.window_handle().downcast::().unwrap(); @@ -638,10 +626,16 @@ impl Workspace { this.serialize_workspace(cx); cx.notify(); }), + cx.on_release(|this, cx| { + this.app_state.workspace_store.update(cx, |store, _| { + store.workspaces.remove(&this.window_self); + }) + }), ]; cx.defer(|this, cx| this.update_window_title(cx)); Workspace { + window_self: window_handle, weak_self: weak_handle.clone(), zoomed: None, zoomed_position: None, @@ -781,19 +775,6 @@ impl Workspace { })? }; - // todo!() Ask how to do this - // let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?; - // let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?; - - // (app_state.initialize_workspace)( - // weak_view, - // serialized_workspace.is_some(), - // app_state.clone(), - // async_cx, - // ) - // .await - // .log_err(); - window .update(&mut cx, |_, cx| cx.activate_window()) .log_err(); @@ -966,12 +947,12 @@ impl Workspace { if let Some((project_entry_id, build_item)) = task.log_err() { let prev_active_item_id = pane.update(&mut cx, |pane, _| { pane.nav_history_mut().set_mode(mode); - pane.active_item().map(|p| p.id()) + pane.active_item().map(|p| p.item_id()) })?; pane.update(&mut cx, |pane, cx| { let item = pane.open_item(project_entry_id, true, cx, build_item); - navigated |= Some(item.id()) != prev_active_item_id; + navigated |= Some(item.item_id()) != prev_active_item_id; pane.nav_history_mut().set_mode(NavigationMode::Normal); if let Some(data) = entry.data { navigated |= item.navigate(data, cx); @@ -1079,35 +1060,40 @@ impl Workspace { } } - // pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { - // cx.spawn(|mut cx| async move { - // let window = cx - // .windows() - // .into_iter() - // .find(|window| window.is_active(&cx).unwrap_or(false)); - // if let Some(window) = window { - // //This can only get called when the window's project connection has been lost - // //so we don't need to prompt the user for anything and instead just close the window - // window.remove(&mut cx); - // } - // }) - // .detach(); - // } + // todo!(Non-window-actions) + pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { + cx.windows().iter().find(|window| { + window + .update(cx, |_, window| { + if window.is_window_active() { + //This can only get called when the window's project connection has been lost + //so we don't need to prompt the user for anything and instead just close the window + window.remove_window(); + true + } else { + false + } + }) + .unwrap_or(false) + }); + } - // pub fn close( - // &mut self, - // _: &CloseWindow, - // cx: &mut ViewContext, - // ) -> Option>> { - // let window = cx.window(); - // let prepare = self.prepare_to_close(false, cx); - // Some(cx.spawn(|_, mut cx| async move { - // if prepare.await? { - // window.remove(&mut cx); - // } - // Ok(()) - // })) - // } + pub fn close( + &mut self, + _: &CloseWindow, + cx: &mut ViewContext, + ) -> Option>> { + let window = cx.window_handle(); + let prepare = self.prepare_to_close(false, cx); + Some(cx.spawn(|_, mut cx| async move { + if prepare.await? { + window.update(&mut cx, |_, cx| { + cx.remove_window(); + })?; + } + Ok(()) + })) + } pub fn prepare_to_close( &mut self, @@ -1115,184 +1101,177 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { //todo!(saveing) - // let active_call = self.active_call().cloned(); - // let window = cx.window(); + let active_call = self.active_call().cloned(); + let window = cx.window_handle(); cx.spawn(|this, mut cx| async move { - // let workspace_count = cx - // .windows() - // .into_iter() - // .filter(|window| window.root_is::()) - // .count(); + let workspace_count = cx.update(|_, cx| { + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .count() + })?; - // if let Some(active_call) = active_call { - // if !quitting - // && workspace_count == 1 - // && active_call.read_with(&cx, |call, _| call.room().is_some()) - // { - // let answer = window.prompt( - // PromptLevel::Warning, - // "Do you want to leave the current call?", - // &["Close window and hang up", "Cancel"], - // &mut cx, - // ); + if let Some(active_call) = active_call { + if !quitting + && workspace_count == 1 + && active_call.read_with(&cx, |call, _| call.room().is_some())? + { + let answer = window.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + &["Close window and hang up", "Cancel"], + ) + })?; - // if let Some(mut answer) = answer { - // if answer.next().await == Some(1) { - // return anyhow::Ok(false); - // } else { - // active_call - // .update(&mut cx, |call, cx| call.hang_up(cx)) - // .await - // .log_err(); - // } - // } - // } - // } + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(&mut cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); + } + } + } - Ok( - false, // this - // .update(&mut cx, |this, cx| { - // this.save_all_internal(SaveIntent::Close, cx) - // })? - // .await? - ) + Ok(this + .update(&mut cx, |this, cx| { + this.save_all_internal(SaveIntent::Close, cx) + })? + .await?) }) } - // fn save_all( - // &mut self, - // action: &SaveAll, - // cx: &mut ViewContext, - // ) -> Option>> { - // let save_all = - // self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx); - // Some(cx.foreground().spawn(async move { - // save_all.await?; - // Ok(()) - // })) - // } + fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext) { + let save_all = self + .save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx) + .detach_and_log_err(cx); + } - // fn save_all_internal( - // &mut self, - // mut save_intent: SaveIntent, - // cx: &mut ViewContext, - // ) -> Task> { - // if self.project.read(cx).is_read_only() { - // return Task::ready(Ok(true)); - // } - // let dirty_items = self - // .panes - // .iter() - // .flat_map(|pane| { - // pane.read(cx).items().filter_map(|item| { - // if item.is_dirty(cx) { - // Some((pane.downgrade(), item.boxed_clone())) - // } else { - // None - // } - // }) - // }) - // .collect::>(); + fn save_all_internal( + &mut self, + mut save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + if self.project.read(cx).is_read_only() { + return Task::ready(Ok(true)); + } + let dirty_items = self + .panes + .iter() + .flat_map(|pane| { + pane.read(cx).items().filter_map(|item| { + if item.is_dirty(cx) { + Some((pane.downgrade(), item.boxed_clone())) + } else { + None + } + }) + }) + .collect::>(); - // let project = self.project.clone(); - // cx.spawn(|workspace, mut cx| async move { - // // Override save mode and display "Save all files" prompt - // if save_intent == SaveIntent::Close && dirty_items.len() > 1 { - // let mut answer = workspace.update(&mut cx, |_, cx| { - // let prompt = Pane::file_names_for_prompt( - // &mut dirty_items.iter().map(|(_, handle)| handle), - // dirty_items.len(), - // cx, - // ); - // cx.prompt( - // PromptLevel::Warning, - // &prompt, - // &["Save all", "Discard all", "Cancel"], - // ) - // })?; - // match answer.next().await { - // Some(0) => save_intent = SaveIntent::SaveAll, - // Some(1) => save_intent = SaveIntent::Skip, - // _ => {} - // } - // } - // for (pane, item) in dirty_items { - // let (singleton, project_entry_ids) = - // cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); - // if singleton || !project_entry_ids.is_empty() { - // if let Some(ix) = - // pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))? - // { - // if !Pane::save_item( - // project.clone(), - // &pane, - // ix, - // &*item, - // save_intent, - // &mut cx, - // ) - // .await? - // { - // return Ok(false); - // } - // } - // } - // } - // Ok(true) - // }) - // } + let project = self.project.clone(); + cx.spawn(|workspace, mut cx| async move { + // Override save mode and display "Save all files" prompt + if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let mut answer = workspace.update(&mut cx, |_, cx| { + let prompt = Pane::file_names_for_prompt( + &mut dirty_items.iter().map(|(_, handle)| handle), + dirty_items.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.await.log_err() { + Some(0) => save_intent = SaveIntent::SaveAll, + Some(1) => save_intent = SaveIntent::Skip, + _ => {} + } + } + for (pane, item) in dirty_items { + let (singleton, project_entry_ids) = + cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; + if singleton || !project_entry_ids.is_empty() { + if let Some(ix) = + pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))? + { + if !Pane::save_item( + project.clone(), + &pane, + ix, + &*item, + save_intent, + &mut cx, + ) + .await? + { + return Ok(false); + } + } + } + } + Ok(true) + }) + } - // pub fn open(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: true, - // directories: true, - // multiple: true, - // }); + pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); - // Some(cx.spawn(|this, mut cx| async move { - // if let Some(paths) = paths.recv().await.flatten() { - // if let Some(task) = this - // .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) - // .log_err() - // { - // task.await? - // } - // } - // Ok(()) - // })) - // } + cx.spawn(|this, mut cx| async move { + let Some(paths) = paths.await.log_err().flatten() else { + return; + }; - // pub fn open_workspace_for_paths( - // &mut self, - // paths: Vec, - // cx: &mut ViewContext, - // ) -> Task> { - // let window = cx.window().downcast::(); - // let is_remote = self.project.read(cx).is_remote(); - // let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); - // let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); - // let close_task = if is_remote || has_worktree || has_dirty_items { - // None - // } else { - // Some(self.prepare_to_close(false, cx)) - // }; - // let app_state = self.app_state.clone(); + if let Some(task) = this + .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) + .log_err() + { + task.await.log_err(); + } + }) + .detach() + } - // cx.spawn(|_, mut cx| async move { - // let window_to_replace = if let Some(close_task) = close_task { - // if !close_task.await? { - // return Ok(()); - // } - // window - // } else { - // None - // }; - // cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx)) - // .await?; - // Ok(()) - // }) - // } + pub fn open_workspace_for_paths( + &mut self, + paths: Vec, + cx: &mut ViewContext, + ) -> Task> { + let window = cx.window_handle().downcast::(); + let is_remote = self.project.read(cx).is_remote(); + let has_worktree = self.project.read(cx).worktrees().next().is_some(); + let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); + let close_task = if is_remote || has_worktree || has_dirty_items { + None + } else { + Some(self.prepare_to_close(false, cx)) + }; + let app_state = self.app_state.clone(); + + cx.spawn(|_, mut cx| async move { + let window_to_replace = if let Some(close_task) = close_task { + if !close_task.await? { + return Ok(()); + } + window + } else { + None + }; + cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))? + .await?; + Ok(()) + }) + } #[allow(clippy::type_complexity)] pub fn open_paths( @@ -1370,25 +1349,25 @@ impl Workspace { }) } - // fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: false, - // directories: true, - // multiple: true, - // }); - // cx.spawn(|this, mut cx| async move { - // if let Some(paths) = paths.recv().await.flatten() { - // let results = this - // .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))? - // .await; - // for result in results.into_iter().flatten() { - // result.log_err(); - // } - // } - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx); - // } + fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: false, + directories: true, + multiple: true, + }); + cx.spawn(|this, mut cx| async move { + if let Some(paths) = paths.await.log_err().flatten() { + let results = this + .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))? + .await; + for result in results.into_iter().flatten() { + result.log_err(); + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } fn project_path_for_path( project: Model, @@ -1419,18 +1398,18 @@ impl Workspace { self.panes.iter().flat_map(|pane| pane.read(cx).items()) } - // pub fn item_of_type(&self, cx: &AppContext) -> Option> { - // self.items_of_type(cx).max_by_key(|item| item.id()) - // } + pub fn item_of_type(&self, cx: &AppContext) -> Option> { + self.items_of_type(cx).max_by_key(|item| item.item_id()) + } - // pub fn items_of_type<'a, T: Item>( - // &'a self, - // cx: &'a AppContext, - // ) -> impl 'a + Iterator> { - // self.panes - // .iter() - // .flat_map(|pane| pane.read(cx).items_of_type()) - // } + pub fn items_of_type<'a, T: Item>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes + .iter() + .flat_map(|pane| pane.read(cx).items_of_type()) + } pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() @@ -1467,68 +1446,70 @@ impl Workspace { }) } - // pub fn close_inactive_items_and_panes( - // &mut self, - // _: &CloseInactiveTabsAndPanes, - // cx: &mut ViewContext, - // ) -> Option>> { - // self.close_all_internal(true, SaveIntent::Close, cx) - // } + pub fn close_inactive_items_and_panes( + &mut self, + _: &CloseInactiveTabsAndPanes, + cx: &mut ViewContext, + ) { + self.close_all_internal(true, SaveIntent::Close, cx) + .map(|task| task.detach_and_log_err(cx)); + } - // pub fn close_all_items_and_panes( - // &mut self, - // action: &CloseAllItemsAndPanes, - // cx: &mut ViewContext, - // ) -> Option>> { - // self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) - // } + pub fn close_all_items_and_panes( + &mut self, + action: &CloseAllItemsAndPanes, + cx: &mut ViewContext, + ) { + self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx) + .map(|task| task.detach_and_log_err(cx)); + } - // fn close_all_internal( - // &mut self, - // retain_active_pane: bool, - // save_intent: SaveIntent, - // cx: &mut ViewContext, - // ) -> Option>> { - // let current_pane = self.active_pane(); + fn close_all_internal( + &mut self, + retain_active_pane: bool, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Option>> { + let current_pane = self.active_pane(); - // let mut tasks = Vec::new(); + let mut tasks = Vec::new(); - // if retain_active_pane { - // if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { - // pane.close_inactive_items(&CloseInactiveItems, cx) - // }) { - // tasks.push(current_pane_close); - // }; - // } + if retain_active_pane { + if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) { + tasks.push(current_pane_close); + }; + } - // for pane in self.panes() { - // if retain_active_pane && pane.id() == current_pane.id() { - // continue; - // } + for pane in self.panes() { + if retain_active_pane && pane.entity_id() == current_pane.entity_id() { + continue; + } - // if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { - // pane.close_all_items( - // &CloseAllItems { - // save_intent: Some(save_intent), - // }, - // cx, - // ) - // }) { - // tasks.push(close_pane_items) - // } - // } + if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| { + pane.close_all_items( + &CloseAllItems { + save_intent: Some(save_intent), + }, + cx, + ) + }) { + tasks.push(close_pane_items) + } + } - // if tasks.is_empty() { - // None - // } else { - // Some(cx.spawn(|_, _| async move { - // for task in tasks { - // task.await? - // } - // Ok(()) - // })) - // } - // } + if tasks.is_empty() { + None + } else { + Some(cx.spawn(|_, _| async move { + for task in tasks { + task.await? + } + Ok(()) + })) + } + } pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { let dock = match dock_side { @@ -1636,15 +1617,15 @@ impl Workspace { None } - // pub fn panel(&self, cx: &WindowContext) -> Option> { - // for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - // let dock = dock.read(cx); - // if let Some(panel) = dock.panel::() { - // return Some(panel); - // } - // } - // None - // } + pub fn panel(&self, cx: &WindowContext) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + let dock = dock.read(cx); + if let Some(panel) = dock.panel::() { + return Some(panel); + } + } + None + } fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { @@ -1955,81 +1936,89 @@ impl Workspace { } } - // fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { - // let panes = self.center.panes(); - // if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { - // cx.focus(&pane); - // } else { - // self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx); - // } - // } + fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { + let panes = self.center.panes(); + if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { + cx.focus_view(&pane); + } else { + self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx); + } + } - // pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { - // let panes = self.center.panes(); - // if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - // let next_ix = (ix + 1) % panes.len(); - // let next_pane = panes[next_ix].clone(); - // cx.focus(&next_pane); - // } - // } + pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { + let panes = self.center.panes(); + if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { + let next_ix = (ix + 1) % panes.len(); + let next_pane = panes[next_ix].clone(); + cx.focus_view(&next_pane); + } + } - // pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { - // let panes = self.center.panes(); - // if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - // let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); - // let prev_pane = panes[prev_ix].clone(); - // cx.focus(&prev_pane); - // } - // } + pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { + let panes = self.center.panes(); + if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { + let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); + let prev_pane = panes[prev_ix].clone(); + cx.focus_view(&prev_pane); + } + } - // pub fn activate_pane_in_direction( - // &mut self, - // direction: SplitDirection, - // cx: &mut ViewContext, - // ) { - // if let Some(pane) = self.find_pane_in_direction(direction, cx) { - // cx.focus(pane); - // } - // } + pub fn activate_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) { + if let Some(pane) = self.find_pane_in_direction(direction, cx) { + cx.focus_view(pane); + } + } - // pub fn swap_pane_in_direction( - // &mut self, - // direction: SplitDirection, - // cx: &mut ViewContext, - // ) { - // if let Some(to) = self - // .find_pane_in_direction(direction, cx) - // .map(|pane| pane.clone()) - // { - // self.center.swap(&self.active_pane.clone(), &to); - // cx.notify(); - // } - // } + pub fn swap_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) { + if let Some(to) = self + .find_pane_in_direction(direction, cx) + .map(|pane| pane.clone()) + { + self.center.swap(&self.active_pane.clone(), &to); + cx.notify(); + } + } - // fn find_pane_in_direction( - // &mut self, - // direction: SplitDirection, - // cx: &mut ViewContext, - // ) -> Option<&View> { - // let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else { - // return None; - // }; - // let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); - // let center = match cursor { - // Some(cursor) if bounding_box.contains_point(cursor) => cursor, - // _ => bounding_box.center(), - // }; + fn find_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) -> Option<&View> { + let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else { + return None; + }; + let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); + let center = match cursor { + Some(cursor) if bounding_box.contains_point(&cursor) => cursor, + _ => bounding_box.center(), + }; - // let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; + let distance_to_next = 1.; //todo(pane dividers styling) - // let target = match direction { - // SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()), - // SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()), - // SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next), - // SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next), - // }; - // self.center.pane_at_pixel_position(target) - // } + let target = match direction { + SplitDirection::Left => { + Point::new(bounding_box.origin.x - distance_to_next.into(), center.y) + } + SplitDirection::Right => { + Point::new(bounding_box.right() + distance_to_next.into(), center.y) + } + SplitDirection::Up => { + Point::new(center.x, bounding_box.origin.y - distance_to_next.into()) + } + SplitDirection::Down => { + Point::new(center.x, bounding_box.top() + distance_to_next.into()) + } + }; + self.center.pane_at_pixel_position(target) + } fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { if self.active_pane != pane { @@ -2201,7 +2190,7 @@ impl Workspace { .read(cx) .items() .enumerate() - .find(|(_, item_handle)| item_handle.id() == item_id_to_move); + .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move); if item_to_move.is_none() { log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); @@ -2230,7 +2219,7 @@ impl Workspace { self.unfollow(&pane, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); for removed_item in pane.read(cx).items() { - self.panes_by_item.remove(&removed_item.id()); + self.panes_by_item.remove(&removed_item.item_id()); } cx.notify(); @@ -2974,14 +2963,14 @@ impl Workspace { fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); - let active_item_id = pane.active_item().map(|item| item.id()); + let active_item_id = pane.active_item().map(|item| item.item_id()); ( pane.items() .filter_map(|item_handle| { Some(SerializedItem { kind: Arc::from(item_handle.serialized_item_kind()?), - item_id: item_handle.id().as_u64(), - active: Some(item_handle.id()) == active_item_id, + item_id: item_handle.item_id().as_u64(), + active: Some(item_handle.item_id()) == active_item_id, }) }) .collect::>(), @@ -3214,65 +3203,53 @@ impl Workspace { }) } - fn actions(div: Div) -> Div { - div + fn actions(&self, div: Div) -> Div { + self.add_workspace_actions_listeners(div) // cx.add_async_action(Workspace::open); // cx.add_async_action(Workspace::follow_next_collaborator); // cx.add_async_action(Workspace::close); - // cx.add_async_action(Workspace::close_inactive_items_and_panes); - // cx.add_async_action(Workspace::close_all_items_and_panes); + .on_action(Self::close_inactive_items_and_panes) + .on_action(Self::close_all_items_and_panes) // cx.add_global_action(Workspace::close_global); // cx.add_global_action(restart); - // cx.add_async_action(Workspace::save_all); - // cx.add_action(Workspace::add_folder_to_project); - // cx.add_action( - // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { - // let pane = workspace.active_pane().clone(); - // workspace.unfollow(&pane, cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - // workspace - // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - // workspace - // .save_active_item(SaveIntent::SaveAs, cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { - // workspace.activate_previous_pane(cx) - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { - // workspace.activate_next_pane(cx) - // }); - // cx.add_action( - // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { - // workspace.activate_pane_in_direction(action.0, cx) - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { - // workspace.swap_pane_in_direction(action.0, cx) - // }, - // ); + .on_action(Self::save_all) + .on_action(Self::add_folder_to_project) + .on_action(|workspace, _: &Unfollow, cx| { + let pane = workspace.active_pane().clone(); + workspace.unfollow(&pane, cx); + }) + .on_action(|workspace, action: &Save, cx| { + workspace + .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + .detach_and_log_err(cx); + }) + .on_action(|workspace, _: &SaveAs, cx| { + workspace + .save_active_item(SaveIntent::SaveAs, cx) + .detach_and_log_err(cx); + }) + .on_action(|workspace, _: &ActivatePreviousPane, cx| { + workspace.activate_previous_pane(cx) + }) + .on_action(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)) + .on_action(|workspace, action: &ActivatePaneInDirection, cx| { + workspace.activate_pane_in_direction(action.0, cx) + }) + .on_action(|workspace, action: &SwapPaneInDirection, cx| { + workspace.swap_pane_in_direction(action.0, cx) + }) .on_action(|this, e: &ToggleLeftDock, cx| { this.toggle_dock(DockPosition::Left, cx); }) - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); + .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }) + .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + workspace.toggle_dock(DockPosition::Bottom, cx); + }) + .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + workspace.close_all_docks(cx); + }) // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); @@ -3376,17 +3353,26 @@ impl Workspace { } fn add_workspace_actions_listeners(&self, mut div: Div) -> Div { + let mut div = div + .on_action(Self::close_inactive_items_and_panes) + .on_action(Self::close_all_items_and_panes) + .on_action(Self::add_folder_to_project) + .on_action(Self::save_all) + .on_action(Self::open); for action in self.workspace_actions.iter() { div = (action)(div) } div } - pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + pub fn active_modal( + &mut self, + cx: &ViewContext, + ) -> Option> { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { @@ -3601,8 +3587,6 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA .log_err(); } -impl EventEmitter for Workspace {} - impl FocusableView for Workspace { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.active_pane.focus_handle(cx) @@ -3615,9 +3599,18 @@ impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); - let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - self.add_workspace_actions_listeners(div()) + let (ui_font, ui_font_size) = { + let theme_settings = ThemeSettings::get_global(cx); + ( + theme_settings.ui_font.family.clone(), + theme_settings.ui_font_size.clone(), + ) + }; + + cx.set_rem_size(ui_font_size); + + self.actions(div()) .key_context(context) .relative() .size_full() @@ -3685,20 +3678,6 @@ impl Render for Workspace { ), ) .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) .z_index(8) // Debug .child( @@ -3710,43 +3689,12 @@ impl Render for Workspace { .top_20() .left_1_4() .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), + .gap_2(), ) } } -// todo!() -// impl Entity for Workspace { -// type Event = Event; - -// fn release(&mut self, cx: &mut AppContext) { -// self.app_state.workspace_store.update(cx, |store, _| { -// store.workspaces.remove(&self.weak_self); -// }) -// } -// } // impl View for Workspace { -// fn ui_name() -> &'static str { -// "Workspace" -// } // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { // let theme = theme::current(cx).clone(); @@ -3872,12 +3820,6 @@ impl Render for Workspace { // .into_any_named("workspace") // } -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// cx.focus(&self.active_pane); -// } -// } - // fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { // DragAndDrop::::update_modifiers(e.modifiers, cx) // } @@ -4054,13 +3996,13 @@ impl WorkspaceHandle for View { } } -// impl std::fmt::Debug for OpenPaths { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// f.debug_struct("OpenPaths") -// .field("paths", &self.paths) -// .finish() -// } -// } +impl std::fmt::Debug for OpenPaths { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpenPaths") + .field("paths", &self.paths) + .finish() + } +} pub struct WorkspaceCreated(pub WeakView); @@ -4523,7 +4465,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { // mod tests { // use super::*; // use crate::{ -// dock::test::{TestPanel, TestPanelEvent}, +// dock::test::TestPanel, // item::test::{TestItem, TestItemEvent, TestProjectItem}, // }; // use fs::FakeFs; @@ -4608,7 +4550,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { // let workspace = window.root(cx); // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // let worktree_id = project.read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() +// project.worktrees().next().unwrap().read(cx).id() // }); // let item1 = window.build_view(cx, |cx| { diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 3f896bf817..78cd74ebbc 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -11,7 +11,7 @@ path = "src/zed2.rs" doctest = false [[bin]] -name = "Zed" +name = "Zed2" path = "src/main.rs" [dependencies] diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs index cf9b33d968..f04f59cf6d 100644 --- a/crates/zed2/src/languages/json.rs +++ b/crates/zed2/src/languages/json.rs @@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter { &self, cx: &mut AppContext, ) -> BoxFuture<'static, serde_json::Value> { - let action_names = gpui::all_action_names(); + let action_names = cx.all_action_names(); let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema( diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index d39a1d79d6..74391193c8 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -140,7 +140,7 @@ fn main() { cx.set_global(client.clone()); - theme::init(cx); + theme::init(theme::LoadThemes::All, cx); project::Project::init(&client, cx); client::init(&client, cx); command_palette::init(cx); diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 7f0c19853e..456d1f5973 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,4 +1,5 @@ -use gpui::action; +use gpui::Action; +use serde::Deserialize; // If the zed binary doesn't use anything in this crate, it will be optimized away // and the actions won't initialize. So we just provide an empty initialization function @@ -9,12 +10,12 @@ use gpui::action; // https://github.com/mmastrac/rust-ctor/issues/280 pub fn init() {} -#[action] +#[derive(Clone, PartialEq, Deserialize, Action)] pub struct OpenBrowser { pub url: String, } -#[action] +#[derive(Clone, PartialEq, Deserialize, Action)] pub struct OpenZedURL { pub url: String, }