From 92d2048aa4315713b46c7c731b51430e5989393f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 00:28:05 -0500 Subject: [PATCH 01/16] WIP --- crates/collab_ui2/src/collab_titlebar_item.rs | 26 +++++++++++-- crates/ui2/src/components.rs | 2 + crates/ui2/src/components/popover_menu.rs | 39 +++++++++++++++++++ crates/workspace2/src/workspace2.rs | 2 + 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 crates/ui2/src/components/popover_menu.rs diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index d76242afa3..fc743585f5 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,7 +37,10 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip}; +use ui::{ + h_stack, prelude::*, v_stack, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, + IconElement, KeyBinding, List, ListItem, PopoverMenu, Tooltip, +}; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -288,10 +291,25 @@ impl Render for CollabTitlebarItem { ), ) }) - .map(|this| { + .child(h_stack().px_1p5().map(|this| { if let Some(user) = current_user { this.when_some(user.avatar.clone(), |this, avatar| { - this.child(ui::Avatar::data(avatar)) + this.child( + PopoverMenu::new( + ButtonLike::new("user-menu") + .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( + IconElement::new(Icon::ChevronDown).color(Color::Muted), + )) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) + .into_any_element(), + ) + .children(vec![ + ListItem::new("foo"), + ListItem::new("bar"), + ListItem::new("baz"), + ]), + ) }) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { @@ -305,7 +323,7 @@ impl Render for CollabTitlebarItem { .detach(); })) } - }) + })) } } diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index be95fc1fab..28dc8f3f06 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -9,6 +9,7 @@ mod keybinding; mod label; mod list; mod popover; +mod popover_menu; mod stack; mod tooltip; @@ -26,6 +27,7 @@ pub use keybinding::*; pub use label::*; pub use list::*; pub use popover::*; +pub use popover_menu::*; pub use stack::*; pub use tooltip::*; diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs new file mode 100644 index 0000000000..a0431c57b3 --- /dev/null +++ b/crates/ui2/src/components/popover_menu.rs @@ -0,0 +1,39 @@ +use gpui::{ + div, overlay, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, + Styled, WindowContext, +}; +use smallvec::SmallVec; + +use crate::{prelude::*, ElevationIndex, List, Popover}; + +#[derive(IntoElement)] +pub struct PopoverMenu { + trigger: AnyElement, + children: SmallVec<[AnyElement; 2]>, +} + +impl RenderOnce for PopoverMenu { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .relative() + .child(self.trigger) + .child(overlay().child(Popover::new().children(self.children))) + } +} + +impl PopoverMenu { + pub fn new(trigger: AnyElement) -> Self { + Self { + trigger, + children: SmallVec::new(), + } + } +} + +impl ParentElement for PopoverMenu { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 23907f8dae..2f7d95c344 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3711,6 +3711,8 @@ impl Render for Workspace { .items_start() .text_color(cx.theme().colors().text) .bg(cx.theme().colors().background) + .border() + .border_color(cx.theme().colors().border) .children(self.titlebar_item.clone()) .child( div() From 82b3efa16c83c1555cf7217084a39ca38933512b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 09:12:39 -0500 Subject: [PATCH 02/16] Update collab button styles --- crates/collab_ui2/src/collab_titlebar_item.rs | 126 ++++++++++-------- crates/ui2/src/components/popover_menu.rs | 11 +- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index fc743585f5..c8076376f0 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -239,54 +239,74 @@ impl Render for CollabTitlebarItem { .when(is_in_room, |this| { this.child( h_stack() + .gap_1() .child( h_stack() - .child(Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - )) - .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().hang_up(cx).detach(); - }) - .log_err(); - } - })), + .gap_1() + .child( + Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, + ) + .style(ButtonStyle2::Subtle), + ) + .child( + IconButton::new("leave-call", ui::Icon::Exit) + .style(ButtonStyle2::Subtle) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().hang_up(cx).detach(); + }) + .log_err(); + } + }), + ), ) .child( h_stack() - .child(IconButton::new("mute-microphone", mic_icon).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_mute(cx); - }) - .log_err(); - } - })) - .child(IconButton::new("mute-sound", speakers_icon).on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_deafen(cx); - }) - .log_err(); - } - })) - .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_screen_share(cx); - }) - .log_err(); - }, - )) + .gap_1() + .child( + IconButton::new("mute-microphone", mic_icon) + .style(ButtonStyle2::Subtle) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + }), + ) + .child( + IconButton::new("mute-sound", speakers_icon) + .style(ButtonStyle2::Subtle) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_deafen(cx); + }) + .log_err(); + } + }), + ) + .child( + IconButton::new("screen-share", ui::Icon::Screen) + .style(ButtonStyle2::Subtle) + .on_click(move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_screen_share(cx); + }) + .log_err(); + }), + ) .pl_2(), ), ) @@ -295,20 +315,14 @@ impl Render for CollabTitlebarItem { if let Some(user) = current_user { this.when_some(user.avatar.clone(), |this, avatar| { this.child( - PopoverMenu::new( - ButtonLike::new("user-menu") - .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child(Avatar::data(avatar)).child( IconElement::new(Icon::ChevronDown).color(Color::Muted), - )) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) - .into_any_element(), - ) - .children(vec![ - ListItem::new("foo"), - ListItem::new("bar"), - ListItem::new("baz"), - ]), + ), + ) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), ) }) } else { diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs index a0431c57b3..17354e6ec6 100644 --- a/crates/ui2/src/components/popover_menu.rs +++ b/crates/ui2/src/components/popover_menu.rs @@ -1,10 +1,9 @@ -use gpui::{ - div, overlay, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, - Styled, WindowContext, -}; +use gpui::{div, overlay, AnyElement, Div, ParentElement, RenderOnce, Styled, WindowContext}; use smallvec::SmallVec; -use crate::{prelude::*, ElevationIndex, List, Popover}; +use crate::{prelude::*, Popover}; + +// 🚧 Under Construction #[derive(IntoElement)] pub struct PopoverMenu { @@ -15,7 +14,7 @@ pub struct PopoverMenu { impl RenderOnce for PopoverMenu { type Rendered = Div; - fn render(self, cx: &mut WindowContext) -> Self::Rendered { + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { div() .relative() .child(self.trigger) From daf6201debd7dfee0a8dfea1d98e02c3871189f5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 09:35:37 -0500 Subject: [PATCH 03/16] Start plugging selected states into collab ui --- crates/collab_ui2/src/collab_titlebar_item.rs | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c8076376f0..c93d5687a5 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -103,17 +103,18 @@ impl Render for CollabTitlebarItem { .update(cx, |this, cx| this.call_state().remote_participants(cx)) .log_err() .flatten(); - let mic_icon = if self + let is_muted = self .workspace .update(cx, |this, cx| this.call_state().is_muted(cx)) .log_err() .flatten() - .unwrap_or_default() - { - ui::Icon::MicMute - } else { - ui::Icon::Mic - }; + .unwrap_or_default(); + let is_deafened = self + .workspace + .update(cx, |this, cx| this.call_state().is_deafened(cx)) + .log_err() + .flatten() + .unwrap_or_default(); let speakers_icon = if self .workspace .update(cx, |this, cx| this.call_state().is_deafened(cx)) @@ -269,22 +270,39 @@ impl Render for CollabTitlebarItem { h_stack() .gap_1() .child( - IconButton::new("mute-microphone", mic_icon) - .style(ButtonStyle2::Subtle) - .on_click({ - let workspace = workspace.clone(); - move |_, cx| { - workspace - .update(cx, |this, cx| { - this.call_state().toggle_mute(cx); - }) - .log_err(); - } - }), + IconButton::new( + "mute-microphone", + if is_muted.clone() { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }, + ) + .style(ButtonStyle2::Subtle) + .selected(is_muted.clone()) + .on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + }), ) .child( IconButton::new("mute-sound", speakers_icon) .style(ButtonStyle2::Subtle) + .selected(is_deafened.clone()) + .tooltip(move |cx| { + Tooltip::with_meta( + "Deafen Audio", + None, + "Mic will be muted", + cx, + ) + }) .on_click({ let workspace = workspace.clone(); move |_, cx| { From e20309f560dec87c74a1ca9856b622d6a020c1ea Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 09:44:33 -0500 Subject: [PATCH 04/16] Update collab_titlebar_item.rs [no ci] --- crates/collab_ui2/src/collab_titlebar_item.rs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c93d5687a5..52381926d9 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -151,18 +151,20 @@ impl Render for CollabTitlebarItem { h_stack() .gap_1() // TODO - Add player menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("project_owner_indicator") - .child( - Button::new("player", "player") - .style(ButtonStyle2::Subtle) - .color(Some(Color::Player(0))), - ) - .tooltip(move |cx| Tooltip::text("Toggle following", cx)), - ) + .when(is_in_room, |this| { + this.child( + div() + .border() + .border_color(gpui::red()) + .id("project_owner_indicator") + .child( + Button::new("project_owner", "project_owner") + .style(ButtonStyle2::Subtle) + .color(Some(Color::Player(0))), + ) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + ) + }) // TODO - Add project menu .child( div() From 8d4652a4dbe224c0ab8050a3e0e2ea3435eb8427 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 12:41:17 -0500 Subject: [PATCH 05/16] Scaffold out `render_project_owner` Co-Authored-By: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/collab_ui2/src/collab_titlebar_item.rs | 85 +++++++++++++++---- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 52381926d9..ec7677cae7 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -38,8 +38,8 @@ use gpui::{ use project::Project; use theme::ActiveTheme; use ui::{ - h_stack, prelude::*, v_stack, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, - IconElement, KeyBinding, List, ListItem, PopoverMenu, Tooltip, + h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, + KeyBinding, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -150,21 +150,24 @@ impl Render for CollabTitlebarItem { .child( h_stack() .gap_1() - // TODO - Add player menu .when(is_in_room, |this| { - this.child( - div() - .border() - .border_color(gpui::red()) - .id("project_owner_indicator") - .child( - Button::new("project_owner", "project_owner") - .style(ButtonStyle2::Subtle) - .color(Some(Color::Player(0))), - ) - .tooltip(move |cx| Tooltip::text("Toggle following", cx)), - ) + this.children(self.render_project_owner(cx)) }) + // TODO - Add player menu + // .when(is_in_room, |this| { + // this.child( + // div() + // .border() + // .border_color(gpui::red()) + // .id("project_owner_indicator") + // .child( + // Button::new("project_owner", "project_owner") + // .style(ButtonStyle2::Subtle) + // .color(Some(Color::Player(0))), + // ) + // .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + // ) + // }) // TODO - Add project menu .child( div() @@ -274,14 +277,14 @@ impl Render for CollabTitlebarItem { .child( IconButton::new( "mute-microphone", - if is_muted.clone() { + if is_muted { ui::Icon::MicMute } else { ui::Icon::Mic }, ) .style(ButtonStyle2::Subtle) - .selected(is_muted.clone()) + .selected(is_muted) .on_click({ let workspace = workspace.clone(); move |_, cx| { @@ -476,6 +479,54 @@ impl CollabTitlebarItem { } } + // resolve if you are in a room -> render_project_owner + // render_project_owner -> resolve if you are in a room -> Option + + pub fn render_project_owner(&self, cx: &mut ViewContext) -> Option { + // TODO: We can't finish implementing this until project sharing works + // - [ ] Show the project owner when the project is remote (maybe done) + // - [x] Show the project owner when the project is local + // - [ ] Show the project owner with a lock icon when the project is local and unshared + + let remote_id = self.project.read(cx).remote_id(); + let is_local = remote_id.is_none(); + let is_shared = self.project.read(cx).is_shared(); + let (user_name, participant_index) = { + if let Some(host) = self.project.read(cx).host() { + debug_assert!(!is_local); + let (Some(host_user), Some(participant_index)) = ( + self.user_store.read(cx).get_cached_user(host.user_id), + self.user_store + .read(cx) + .participant_indices() + .get(&host.user_id), + ) else { + return None; + }; + (host_user.github_login.clone(), participant_index.0) + } else { + debug_assert!(is_local); + let name = self + .user_store + .read(cx) + .current_user() + .map(|user| user.github_login.clone())?; + (name, 0) + } + }; + Some( + Button::new( + "project_owner_trigger", + format!("{user_name} ({})", !is_shared), + ) + .color(Color::Player(participant_index)) + .style(ButtonStyle2::Subtle) + .into_element(), + ) + + // add lock if you are in a locked project + } + // fn collect_title_root_names( // &self, // theme: Arc, From 679851e3496ecdcb172264efa76324d474dd0ed6 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 12:51:55 -0500 Subject: [PATCH 06/16] Add `render_project_name` and `render_project_branch` Co-Authored-By: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/collab_ui2/src/collab_titlebar_item.rs | 152 +++++++++++------- 1 file changed, 96 insertions(+), 56 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ec7677cae7..38cdb73f85 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -35,7 +35,7 @@ use gpui::{ ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; -use project::Project; +use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, @@ -46,8 +46,8 @@ use workspace::{notifications::NotifyResultExt, Workspace}; use crate::face_pile::FacePile; -// const MAX_PROJECT_NAME_LENGTH: usize = 40; -// const MAX_BRANCH_NAME_LENGTH: usize = 40; +const MAX_PROJECT_NAME_LENGTH: usize = 40; +const MAX_BRANCH_NAME_LENGTH: usize = 40; // actions!( // collab, @@ -153,58 +153,60 @@ impl Render for CollabTitlebarItem { .when(is_in_room, |this| { this.children(self.render_project_owner(cx)) }) - // TODO - Add player menu - // .when(is_in_room, |this| { - // this.child( - // div() - // .border() - // .border_color(gpui::red()) - // .id("project_owner_indicator") - // .child( - // Button::new("project_owner", "project_owner") - // .style(ButtonStyle2::Subtle) - // .color(Some(Color::Player(0))), - // ) - // .tooltip(move |cx| Tooltip::text("Toggle following", cx)), - // ) - // }) - // TODO - Add project menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("titlebar_project_menu_button") - .child( - Button::new("project_name", "project_name") - .style(ButtonStyle2::Subtle), - ) - .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - ) - // TODO - Add git menu - .child( - div() - .border() - .border_color(gpui::red()) - .id("titlebar_git_menu_button") - .child( - Button::new("branch_name", "branch_name") - .style(ButtonStyle2::Subtle) - .color(Some(Color::Muted)), - ) - .tooltip(move |cx| { - cx.build_view(|_| { - Tooltip::new("Recent Branches") - .key_binding(KeyBinding::new(gpui::KeyBinding::new( - "cmd-b", - // todo!() Replace with real action. - gpui::NoAction, - None, - ))) - .meta("Only local branches shown") - }) - .into() - }), - ), + .child(self.render_project_name(cx)) + .children(self.render_project_branch(cx)), + // TODO - Add player menu + // .when(is_in_room, |this| { + // this.child( + // div() + // .border() + // .border_color(gpui::red()) + // .id("project_owner_indicator") + // .child( + // Button::new("project_owner", "project_owner") + // .style(ButtonStyle2::Subtle) + // .color(Some(Color::Player(0))), + // ) + // .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + // ) + // }) + // TODO - Add project menu + // .child( + // div() + // .border() + // .border_color(gpui::red()) + // .id("titlebar_project_menu_button") + // .child( + // Button::new("project_name", "project_name") + // .style(ButtonStyle2::Subtle), + // ) + // .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), + // ) + // TODO - Add git menu + // .child( + // div() + // .border() + // .border_color(gpui::red()) + // .id("titlebar_git_menu_button") + // .child( + // Button::new("branch_name", "branch_name") + // .style(ButtonStyle2::Subtle) + // .color(Some(Color::Muted)), + // ) + // .tooltip(move |cx| { + // cx.build_view(|_| { + // Tooltip::new("Recent Branches") + // .key_binding(KeyBinding::new(gpui::KeyBinding::new( + // "cmd-b", + // // todo!() Replace with real action. + // gpui::NoAction, + // None, + // ))) + // .meta("Only local branches shown") + // }) + // .into() + // }), + // ), ) .when_some( users.zip(current_user.clone()), @@ -523,8 +525,46 @@ impl CollabTitlebarItem { .style(ButtonStyle2::Subtle) .into_element(), ) + } - // add lock if you are in a locked project + pub fn render_project_name(&self, cx: &mut ViewContext) -> impl Element { + let name = { + let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_name() + }); + + names.next().unwrap_or("") + }; + + let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); + + Button::new("project_name_trigger", name) + .style(ButtonStyle2::Subtle) + .into_element() + } + + pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { + let entry = { + let mut names_and_branches = + self.project.read(cx).visible_worktrees(cx).map(|worktree| { + let worktree = worktree.read(cx); + worktree.root_git_entry() + }); + + names_and_branches.next().flatten() + }; + + let branch_name = entry + .as_ref() + .and_then(RepositoryEntry::branch) + .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; + + Some( + Button::new("project_branch_trigger", branch_name) + .style(ButtonStyle2::Subtle) + .into_element(), + ) } // fn collect_title_root_names( From 5eb89781e3c10792c9899e79f0f183b4b1237588 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 13:11:20 -0500 Subject: [PATCH 07/16] Clean up left side titlebar buttons --- crates/collab_ui2/src/collab_titlebar_item.rs | 124 +++++++----------- crates/ui2/src/components/popover_menu.rs | 15 ++- 2 files changed, 63 insertions(+), 76 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 38cdb73f85..bb49011f24 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -39,7 +39,7 @@ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, - KeyBinding, Tooltip, + KeyBinding, PopoverMenu, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -155,58 +155,6 @@ impl Render for CollabTitlebarItem { }) .child(self.render_project_name(cx)) .children(self.render_project_branch(cx)), - // TODO - Add player menu - // .when(is_in_room, |this| { - // this.child( - // div() - // .border() - // .border_color(gpui::red()) - // .id("project_owner_indicator") - // .child( - // Button::new("project_owner", "project_owner") - // .style(ButtonStyle2::Subtle) - // .color(Some(Color::Player(0))), - // ) - // .tooltip(move |cx| Tooltip::text("Toggle following", cx)), - // ) - // }) - // TODO - Add project menu - // .child( - // div() - // .border() - // .border_color(gpui::red()) - // .id("titlebar_project_menu_button") - // .child( - // Button::new("project_name", "project_name") - // .style(ButtonStyle2::Subtle), - // ) - // .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - // ) - // TODO - Add git menu - // .child( - // div() - // .border() - // .border_color(gpui::red()) - // .id("titlebar_git_menu_button") - // .child( - // Button::new("branch_name", "branch_name") - // .style(ButtonStyle2::Subtle) - // .color(Some(Color::Muted)), - // ) - // .tooltip(move |cx| { - // cx.build_view(|_| { - // Tooltip::new("Recent Branches") - // .key_binding(KeyBinding::new(gpui::KeyBinding::new( - // "cmd-b", - // // todo!() Replace with real action. - // gpui::NoAction, - // None, - // ))) - // .meta("Only local branches shown") - // }) - // .into() - // }), - // ), ) .when_some( users.zip(current_user.clone()), @@ -340,15 +288,27 @@ impl Render for CollabTitlebarItem { if let Some(user) = current_user { this.when_some(user.avatar.clone(), |this, avatar| { this.child( - ButtonLike::new("user-menu") - .child( - h_stack().gap_0p5().child(Avatar::data(avatar)).child( + PopoverMenu::new( + ButtonLike::new("user-menu") + .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( IconElement::new(Icon::ChevronDown).color(Color::Muted), - ), - ) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + )) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) + .into_any_element(), + ) + .children(vec![div().w_96().h_96().bg(gpui::red())]), ) + // this.child( + // ButtonLike::new("user-menu") + // .child( + // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // IconElement::new(Icon::ChevronDown).color(Color::Muted), + // ), + // ) + // .style(ButtonStyle2::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // ) }) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { @@ -517,13 +477,15 @@ impl CollabTitlebarItem { } }; Some( - Button::new( - "project_owner_trigger", - format!("{user_name} ({})", !is_shared), - ) - .color(Color::Player(participant_index)) - .style(ButtonStyle2::Subtle) - .into_element(), + div().border().border_color(gpui::red()).child( + Button::new( + "project_owner_trigger", + format!("{user_name} ({})", !is_shared), + ) + .color(Color::Player(participant_index)) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), + ), ) } @@ -539,9 +501,11 @@ impl CollabTitlebarItem { let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); - Button::new("project_name_trigger", name) - .style(ButtonStyle2::Subtle) - .into_element() + div().border().border_color(gpui::red()).child( + Button::new("project_name_trigger", name) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), + ) } pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { @@ -561,9 +525,23 @@ impl CollabTitlebarItem { .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; Some( - Button::new("project_branch_trigger", branch_name) - .style(ButtonStyle2::Subtle) - .into_element(), + div().border().border_color(gpui::red()).child( + Button::new("project_branch_trigger", branch_name) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| { + cx.build_view(|_| { + Tooltip::new("Recent Branches") + .key_binding(KeyBinding::new(gpui::KeyBinding::new( + "cmd-b", + // todo!() Replace with real action. + gpui::NoAction, + None, + ))) + .meta("Local branches only") + }) + .into() + }), + ), ) } diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs index 17354e6ec6..c00bfed921 100644 --- a/crates/ui2/src/components/popover_menu.rs +++ b/crates/ui2/src/components/popover_menu.rs @@ -1,4 +1,4 @@ -use gpui::{div, overlay, AnyElement, Div, ParentElement, RenderOnce, Styled, WindowContext}; +use gpui::{div, overlay, px, AnyElement, Div, ParentElement, RenderOnce, Styled, WindowContext}; use smallvec::SmallVec; use crate::{prelude::*, Popover}; @@ -16,9 +16,18 @@ impl RenderOnce for PopoverMenu { fn render(self, _cx: &mut WindowContext) -> Self::Rendered { div() + .bg(gpui::green()) .relative() - .child(self.trigger) - .child(overlay().child(Popover::new().children(self.children))) + .child(div().bg(gpui::blue()).child(self.trigger)) + .child( + overlay() + .position(gpui::Point { + x: px(100.), + y: px(100.), + }) + .anchor(gpui::AnchorCorner::TopRight) + .child(Popover::new().children(self.children)), + ) } } From 5fdfdb046cbe9dc6da8017b54af80f62cdfbd3be Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 13:13:22 -0500 Subject: [PATCH 08/16] Remove unused import --- crates/collab_ui2/src/collab_titlebar_item.rs | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index bb49011f24..ab042d6f0d 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -39,7 +39,7 @@ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, - KeyBinding, PopoverMenu, Tooltip, + KeyBinding, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -287,28 +287,30 @@ impl Render for CollabTitlebarItem { .child(h_stack().px_1p5().map(|this| { if let Some(user) = current_user { this.when_some(user.avatar.clone(), |this, avatar| { - this.child( - PopoverMenu::new( - ButtonLike::new("user-menu") - .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( - IconElement::new(Icon::ChevronDown).color(Color::Muted), - )) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) - .into_any_element(), - ) - .children(vec![div().w_96().h_96().bg(gpui::red())]), - ) + // TODO: Finish implementing user menu popover + // // this.child( - // ButtonLike::new("user-menu") - // .child( - // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // PopoverMenu::new( + // ButtonLike::new("user-menu") + // .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // ), - // ) - // .style(ButtonStyle2::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // )) + // .style(ButtonStyle2::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) + // .into_any_element(), + // ) + // .children(vec![div().w_96().h_96().bg(gpui::red())]), // ) + this.child( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child(Avatar::data(avatar)).child( + IconElement::new(Icon::ChevronDown).color(Color::Muted), + ), + ) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) }) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { From 180ba42456ed8d86164158b237ffc82f76998e54 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 14:54:44 -0500 Subject: [PATCH 09/16] WIP update popover_menu --- crates/collab_ui2/src/collab_titlebar_item.rs | 41 +++++------ crates/ui2/src/components/popover_menu.rs | 71 ++++++++++++++++--- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ab042d6f0d..d1d560fcdd 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -39,7 +39,7 @@ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, - KeyBinding, Tooltip, + KeyBinding, PopoverMenu, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -289,28 +289,29 @@ impl Render for CollabTitlebarItem { this.when_some(user.avatar.clone(), |this, avatar| { // TODO: Finish implementing user menu popover // - // this.child( - // PopoverMenu::new( - // ButtonLike::new("user-menu") - // .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( - // IconElement::new(Icon::ChevronDown).color(Color::Muted), - // )) - // .style(ButtonStyle2::Subtle) - // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) - // .into_any_element(), - // ) - // .children(vec![div().w_96().h_96().bg(gpui::red())]), - // ) this.child( - ButtonLike::new("user-menu") - .child( - h_stack().gap_0p5().child(Avatar::data(avatar)).child( + PopoverMenu::new( + ButtonLike::new("user-menu") + .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( IconElement::new(Icon::ChevronDown).color(Color::Muted), - ), - ) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + )) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) + .into_any_element(), + ) + .anchor(gpui::AnchorCorner::TopRight) + .children(vec![div().w_96().h_96().bg(gpui::red())]), ) + // this.child( + // ButtonLike::new("user-menu") + // .child( + // h_stack().gap_0p5().child(Avatar::data(avatar)).child( + // IconElement::new(Icon::ChevronDown).color(Color::Muted), + // ), + // ) + // .style(ButtonStyle2::Subtle) + // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + // ) }) } else { this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| { diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs index c00bfed921..9e393ffb34 100644 --- a/crates/ui2/src/components/popover_menu.rs +++ b/crates/ui2/src/components/popover_menu.rs @@ -1,33 +1,70 @@ -use gpui::{div, overlay, px, AnyElement, Div, ParentElement, RenderOnce, Styled, WindowContext}; +use gpui::{ + div, overlay, rems, AnchorCorner, AnyElement, Div, ParentElement, RenderOnce, Styled, + WindowContext, +}; use smallvec::SmallVec; use crate::{prelude::*, Popover}; -// 🚧 Under Construction - #[derive(IntoElement)] pub struct PopoverMenu { + /// The element that triggers the popover menu when clicked + /// Usually a button trigger: AnyElement, + /// The content of the popover menu + /// This will automatically be wrapped in a [Popover] element children: SmallVec<[AnyElement; 2]>, + /// The direction the popover menu will open by default + /// + /// When not enough space is available in the default direction, + /// the popover menu will follow the rules of [gpui2::elements::overlay] + anchor: AnchorCorner, + /// Whether the popover menu is currently open + show_menu: bool, } impl RenderOnce for PopoverMenu { type Rendered = Div; fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + // Default offset = 4px padding + 1px border + let offset = 5. / 16.; + + let (top, right, bottom, left) = match self.anchor { + AnchorCorner::TopRight => (None, Some(-offset), Some(-offset), None), + AnchorCorner::TopLeft => (None, None, Some(-offset), Some(-offset)), + AnchorCorner::BottomRight => (Some(-offset), Some(-offset), None, None), + AnchorCorner::BottomLeft => (Some(-offset), None, None, Some(-offset)), + }; + div() + .flex() + .flex_none() .bg(gpui::green()) .relative() - .child(div().bg(gpui::blue()).child(self.trigger)) .child( - overlay() - .position(gpui::Point { - x: px(100.), - y: px(100.), - }) - .anchor(gpui::AnchorCorner::TopRight) - .child(Popover::new().children(self.children)), + div() + .flex_none() + .relative() + .bg(gpui::blue()) + .child(self.trigger), ) + .when(self.show_menu, |this| { + this.child( + div() + .absolute() + .size_0() + .when_some(top, |this, t| this.top(rems(t))) + .when_some(right, |this, r| this.right(rems(r))) + .when_some(bottom, |this, b| this.bottom(rems(b))) + .when_some(left, |this, l| this.left(rems(l))) + .child( + overlay() + .anchor(AnchorCorner::TopRight) + .child(Popover::new().children(self.children)), + ), + ) + }) } } @@ -36,8 +73,20 @@ impl PopoverMenu { Self { trigger, children: SmallVec::new(), + anchor: AnchorCorner::TopLeft, + show_menu: false, } } + + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor = anchor; + self + } + + pub fn show_menu(mut self, show_menu: bool) -> Self { + self.show_menu = show_menu; + self + } } impl ParentElement for PopoverMenu { From 164084c61cf1bd6a22530273e2a9a390c1c62b7e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 14:55:59 -0500 Subject: [PATCH 10/16] Update collab_titlebar_item.rs --- crates/collab_ui2/src/collab_titlebar_item.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index d1d560fcdd..6af63301b9 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -300,6 +300,8 @@ impl Render for CollabTitlebarItem { .into_any_element(), ) .anchor(gpui::AnchorCorner::TopRight) + // TODO: Show when trigger is clicked + .show_menu(true) .children(vec![div().w_96().h_96().bg(gpui::red())]), ) // this.child( From ebbbeca9a6122e6abdf127d8d70d881fe831d733 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 30 Nov 2023 16:13:44 -0700 Subject: [PATCH 11/16] Flesh out a popover control --- crates/collab_ui2/src/collab_titlebar_item.rs | 31 +- .../ui2/src/components/button/button_like.rs | 6 +- crates/ui2/src/components/popover_menu.rs | 287 +++++++++++++----- 3 files changed, 232 insertions(+), 92 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 6af63301b9..e4ca428678 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -38,8 +38,8 @@ use gpui::{ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ - h_stack, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, Icon, IconButton, IconElement, - KeyBinding, PopoverMenu, Tooltip, + h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, ContextMenu, Icon, + IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -290,19 +290,20 @@ impl Render for CollabTitlebarItem { // TODO: Finish implementing user menu popover // this.child( - PopoverMenu::new( - ButtonLike::new("user-menu") - .child(h_stack().gap_0p5().child(Avatar::data(avatar)).child( - IconElement::new(Icon::ChevronDown).color(Color::Muted), - )) - .style(ButtonStyle2::Subtle) - .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)) - .into_any_element(), - ) - .anchor(gpui::AnchorCorner::TopRight) - // TODO: Show when trigger is clicked - .show_menu(true) - .children(vec![div().w_96().h_96().bg(gpui::red())]), + popover_menu("user-menu") + .menu(|cx| ContextMenu::build(cx, |menu, cx| menu.header("ADADA"))) + .trigger( + ButtonLike::new("user-menu") + .child( + h_stack().gap_0p5().child(Avatar::data(avatar)).child( + IconElement::new(Icon::ChevronDown) + .color(Color::Muted), + ), + ) + .style(ButtonStyle2::Subtle) + .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight), ) // this.child( // ButtonLike::new("user-menu") diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 3c36feb59f..6885ac86e1 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -261,7 +261,11 @@ impl RenderOnce for ButtonLike { |this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)), ) .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |cx| tooltip(cx)) + if !self.selected { + this.tooltip(move |cx| tooltip(cx)) + } else { + this + } }) .children(self.children) } diff --git a/crates/ui2/src/components/popover_menu.rs b/crates/ui2/src/components/popover_menu.rs index 9e393ffb34..4b5144e7c7 100644 --- a/crates/ui2/src/components/popover_menu.rs +++ b/crates/ui2/src/components/popover_menu.rs @@ -1,96 +1,231 @@ +use std::{cell::RefCell, rc::Rc}; + use gpui::{ - div, overlay, rems, AnchorCorner, AnyElement, Div, ParentElement, RenderOnce, Styled, - WindowContext, + overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, + Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent, + ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; -use smallvec::SmallVec; -use crate::{prelude::*, Popover}; +use crate::{Clickable, Selectable}; -#[derive(IntoElement)] -pub struct PopoverMenu { - /// The element that triggers the popover menu when clicked - /// Usually a button - trigger: AnyElement, - /// The content of the popover menu - /// This will automatically be wrapped in a [Popover] element - children: SmallVec<[AnyElement; 2]>, - /// The direction the popover menu will open by default - /// - /// When not enough space is available in the default direction, - /// the popover menu will follow the rules of [gpui2::elements::overlay] +pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {} + +impl PopoverTrigger for T {} + +pub struct PopoverMenu { + id: ElementId, + child_builder: Option< + Box< + dyn FnOnce( + Rc>>>, + Option View + 'static>>, + ) -> AnyElement + + 'static, + >, + >, + menu_builder: Option View + 'static>>, anchor: AnchorCorner, - /// Whether the popover menu is currently open - show_menu: bool, + attach: Option, + offset: Option>, } -impl RenderOnce for PopoverMenu { - type Rendered = Div; - - fn render(self, _cx: &mut WindowContext) -> Self::Rendered { - // Default offset = 4px padding + 1px border - let offset = 5. / 16.; - - let (top, right, bottom, left) = match self.anchor { - AnchorCorner::TopRight => (None, Some(-offset), Some(-offset), None), - AnchorCorner::TopLeft => (None, None, Some(-offset), Some(-offset)), - AnchorCorner::BottomRight => (Some(-offset), Some(-offset), None, None), - AnchorCorner::BottomLeft => (Some(-offset), None, None, Some(-offset)), - }; - - div() - .flex() - .flex_none() - .bg(gpui::green()) - .relative() - .child( - div() - .flex_none() - .relative() - .bg(gpui::blue()) - .child(self.trigger), - ) - .when(self.show_menu, |this| { - this.child( - div() - .absolute() - .size_0() - .when_some(top, |this, t| this.top(rems(t))) - .when_some(right, |this, r| this.right(rems(r))) - .when_some(bottom, |this, b| this.bottom(rems(b))) - .when_some(left, |this, l| this.left(rems(l))) - .child( - overlay() - .anchor(AnchorCorner::TopRight) - .child(Popover::new().children(self.children)), - ), - ) - }) - } -} - -impl PopoverMenu { - pub fn new(trigger: AnyElement) -> Self { - Self { - trigger, - children: SmallVec::new(), - anchor: AnchorCorner::TopLeft, - show_menu: false, - } +impl PopoverMenu { + pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self } + pub fn trigger(mut self, t: T) -> Self { + self.child_builder = Some(Box::new(|menu, builder| { + let open = menu.borrow().is_some(); + t.selected(open) + .when_some(builder, |el, builder| { + el.on_click({ + move |_, cx| { + let new_menu = (builder)(cx); + let menu2 = menu.clone(); + let previous_focus_handle = cx.focused(); + + cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { + if modal.focus_handle(cx).contains_focused(cx) { + if previous_focus_handle.is_some() { + cx.focus(&previous_focus_handle.as_ref().unwrap()) + } + } + *menu2.borrow_mut() = None; + cx.notify(); + }) + .detach(); + cx.focus_view(&new_menu); + *menu.borrow_mut() = Some(new_menu); + } + }) + }) + .into_any_element() + })); + 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 = anchor; self } - pub fn show_menu(mut self, show_menu: bool) -> Self { - self.show_menu = show_menu; + /// 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 } + + /// offset offsets the position of the content by that many pixels. + pub fn offset(mut self, offset: Point) -> Self { + self.offset = Some(offset); + self + } + + fn resolved_attach(&self) -> AnchorCorner { + self.attach.unwrap_or_else(|| match self.anchor { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + }) + } + + fn resolved_offset(&self, cx: &WindowContext) -> Point { + self.offset.unwrap_or_else(|| { + // Default offset = 4px padding + 1px border + let offset = rems(5. / 16.) * cx.rem_size(); + match self.anchor { + AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)), + AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)), + } + }) + } } -impl ParentElement for PopoverMenu { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children +pub fn popover_menu(id: impl Into) -> PopoverMenu { + PopoverMenu { + id: id.into(), + child_builder: None, + menu_builder: None, + anchor: AnchorCorner::TopLeft, + attach: None, + offset: None, + } +} + +pub struct PopoverMenuState { + child_layout_id: Option, + child_element: Option, + child_bounds: Option>, + menu_element: Option, + menu: Rc>>>, +} + +impl Element for PopoverMenu { + type State = PopoverMenuState; + + fn layout( + &mut self, + element_state: Option, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::State) { + let mut menu_layout_id = None; + + let (menu, child_bounds) = if let Some(element_state) = element_state { + (element_state.menu, element_state.child_bounds) + } else { + (Rc::default(), None) + }; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay().snap_to_window().anchor(self.anchor); + + if let Some(child_bounds) = child_bounds { + overlay = overlay.position( + self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx), + ); + } + + let mut element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.layout(cx)); + element + }); + + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + PopoverMenuState { + menu, + child_element, + child_layout_id, + menu_element, + child_bounds, + }, + ) + } + + fn paint( + self, + _: Bounds, + element_state: &mut Self::State, + cx: &mut WindowContext, + ) { + if let Some(child) = element_state.child_element.take() { + child.paint(cx); + } + + if let Some(child_layout_id) = element_state.child_layout_id.take() { + element_state.child_bounds = Some(cx.layout_bounds(child_layout_id)); + } + + if let Some(menu) = element_state.menu_element.take() { + menu.paint(cx); + + if let Some(child_bounds) = element_state.child_bounds { + let interactive_bounds = InteractiveBounds { + bounds: child_bounds, + stacking_order: cx.stacking_order().clone(), + }; + + // Mouse-downing outside the menu dismisses it, so we don't + // want a click on the toggle to re-open it. + cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && interactive_bounds.visibly_contains(&e.position, cx) + { + cx.stop_propagation() + } + }) + } + } + } +} + +impl IntoElement for PopoverMenu { + type Element = Self; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn into_element(self) -> Self::Element { + self } } From e1c8369b3dbc0e6188f3b758971069d16782f5c2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 30 Nov 2023 16:39:43 -0700 Subject: [PATCH 12/16] Rename `menu_handle` to `right_click_menu` and `child` to `trigger` This makes things more in-line with `popover_menu`. --- crates/collab_ui2/src/collab_titlebar_item.rs | 2 +- crates/ui2/src/components.rs | 2 + crates/ui2/src/components/context_menu.rs | 178 +---------------- crates/ui2/src/components/right_click_menu.rs | 185 ++++++++++++++++++ .../src/components/stories/context_menu.rs | 42 +--- crates/workspace2/src/dock.rs | 12 +- 6 files changed, 206 insertions(+), 215 deletions(-) create mode 100644 crates/ui2/src/components/right_click_menu.rs diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index e4ca428678..a04b6a50aa 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -291,7 +291,7 @@ impl Render for CollabTitlebarItem { // this.child( popover_menu("user-menu") - .menu(|cx| ContextMenu::build(cx, |menu, cx| menu.header("ADADA"))) + .menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA"))) .trigger( ButtonLike::new("user-menu") .child( diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 28dc8f3f06..17271de48d 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -10,6 +10,7 @@ mod label; mod list; mod popover; mod popover_menu; +mod right_click_menu; mod stack; mod tooltip; @@ -28,6 +29,7 @@ pub use label::*; pub use list::*; pub use popover::*; pub use popover_menu::*; +pub use right_click_menu::*; pub use stack::*; pub use tooltip::*; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index f071d188a1..562639ec58 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -2,12 +2,11 @@ use crate::{ h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader, }; use gpui::{ - overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase, - Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton, - MouseDownEvent, Pixels, Point, Render, View, VisualContext, + px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + IntoElement, Render, View, VisualContext, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; pub enum ContextMenuItem { Separator, @@ -208,174 +207,3 @@ impl Render for ContextMenu { ) } } - -pub struct MenuHandle { - id: ElementId, - child_builder: Option AnyElement + 'static>>, - menu_builder: Option View + 'static>>, - anchor: Option, - attach: Option, -} - -impl MenuHandle { - pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> 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).into_element().into_any())); - 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(id: impl Into) -> MenuHandle { - MenuHandle { - id: id.into(), - 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 State = MenuHandleState; - - fn layout( - &mut self, - element_state: Option, - cx: &mut WindowContext, - ) -> (gpui::LayoutId, Self::State) { - 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 element = overlay.child(menu.clone()).into_any(); - menu_layout_id = Some(element.layout(cx)); - element - }); - - 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(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( - self, - bounds: Bounds, - element_state: &mut Self::State, - cx: &mut WindowContext, - ) { - if let Some(child) = element_state.child_element.take() { - child.paint(cx); - } - - if let Some(menu) = element_state.menu_element.take() { - menu.paint(cx); - return; - } - - let Some(builder) = self.menu_builder 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 |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)(cx); - let menu2 = menu.clone(); - cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| { - *menu2.borrow_mut() = None; - cx.notify(); - }) - .detach(); - cx.focus_view(&new_menu); - *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 IntoElement for MenuHandle { - type Element = Self; - - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui2/src/components/right_click_menu.rs b/crates/ui2/src/components/right_click_menu.rs new file mode 100644 index 0000000000..27c4fdab96 --- /dev/null +++ b/crates/ui2/src/components/right_click_menu.rs @@ -0,0 +1,185 @@ +use std::{cell::RefCell, rc::Rc}; + +use gpui::{ + overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId, + IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, + View, VisualContext, WindowContext, +}; + +pub struct RightClickMenu { + id: ElementId, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option View + 'static>>, + anchor: Option, + attach: Option, +} + +impl RightClickMenu { + pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn trigger(mut self, e: E) -> Self { + self.child_builder = Some(Box::new(move |_| e.into_any_element())); + 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 right_click_menu(id: impl Into) -> RightClickMenu { + RightClickMenu { + id: id.into(), + 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 RightClickMenu { + type State = MenuHandleState; + + fn layout( + &mut self, + element_state: Option, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::State) { + 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 element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.layout(cx)); + element + }); + + 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(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( + self, + bounds: Bounds, + element_state: &mut Self::State, + cx: &mut WindowContext, + ) { + if let Some(child) = element_state.child_element.take() { + child.paint(cx); + } + + if let Some(menu) = element_state.menu_element.take() { + menu.paint(cx); + return; + } + + let Some(builder) = self.menu_builder 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 |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)(cx); + let menu2 = menu.clone(); + let previous_focus_handle = cx.focused(); + + cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| { + if modal.focus_handle(cx).contains_focused(cx) { + if previous_focus_handle.is_some() { + cx.focus(&previous_focus_handle.as_ref().unwrap()) + } + } + *menu2.borrow_mut() = None; + cx.notify(); + }) + .detach(); + cx.focus_view(&new_menu); + *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 IntoElement for RightClickMenu { + type Element = Self; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn into_element(self) -> Self::Element { + self + } +} diff --git a/crates/ui2/src/components/stories/context_menu.rs b/crates/ui2/src/components/stories/context_menu.rs index d5fb94df4f..dd1fe8d565 100644 --- a/crates/ui2/src/components/stories/context_menu.rs +++ b/crates/ui2/src/components/stories/context_menu.rs @@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View}; use story::Story; use crate::prelude::*; -use crate::{menu_handle, ContextMenu, Label}; +use crate::{right_click_menu, ContextMenu, Label}; actions!(PrintCurrentDate, PrintBestFood); @@ -45,25 +45,13 @@ impl Render for ContextMenuStory { .flex_col() .justify_between() .child( - menu_handle("test2") - .child(|is_open| { - Label::new(if is_open { - "TOP LEFT" - } else { - "RIGHT CLICK ME" - }) - }) + right_click_menu("test2") + .trigger(Label::new("TOP LEFT")) .menu(move |cx| build_menu(cx, "top left")), ) .child( - menu_handle("test1") - .child(|is_open| { - Label::new(if is_open { - "BOTTOM LEFT" - } else { - "RIGHT CLICK ME" - }) - }) + right_click_menu("test1") + .trigger(Label::new("BOTTOM LEFT")) .anchor(AnchorCorner::BottomLeft) .attach(AnchorCorner::TopLeft) .menu(move |cx| build_menu(cx, "bottom left")), @@ -75,26 +63,14 @@ impl Render for ContextMenuStory { .flex_col() .justify_between() .child( - menu_handle("test3") - .child(|is_open| { - Label::new(if is_open { - "TOP RIGHT" - } else { - "RIGHT CLICK ME" - }) - }) + right_click_menu("test3") + .trigger(Label::new("TOP RIGHT")) .anchor(AnchorCorner::TopRight) .menu(move |cx| build_menu(cx, "top right")), ) .child( - menu_handle("test4") - .child(|is_open| { - Label::new(if is_open { - "BOTTOM RIGHT" - } else { - "RIGHT CLICK ME" - }) - }) + right_click_menu("test4") + .trigger(Label::new("BOTTOM RIGHT")) .anchor(AnchorCorner::BottomRight) .attach(AnchorCorner::TopRight) .menu(move |cx| build_menu(cx, "bottom right")), diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 06fdf7f9c1..437e7c0192 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -7,8 +7,8 @@ use gpui::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use ui::prelude::*; -use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip}; +use ui::{h_stack, ContextMenu, IconButton, Tooltip}; +use ui::{prelude::*, right_click_menu}; pub enum PanelEvent { ChangePosition, @@ -702,7 +702,7 @@ impl Render for PanelButtons { }; Some( - menu_handle(name) + right_click_menu(name) .menu(move |cx| { const POSITIONS: [DockPosition; 3] = [ DockPosition::Left, @@ -726,14 +726,14 @@ impl Render for PanelButtons { }) .anchor(menu_anchor) .attach(menu_attach) - .child(move |_is_open| { + .trigger( IconButton::new(name, icon) .selected(is_active_button) .action(action.boxed_clone()) .tooltip(move |cx| { Tooltip::for_action(tooltip.clone(), &*action, cx) - }) - }), + }), + ), ) }); From ab75dbe7aff021461be71735fef7f0016ca58d8f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 1 Dec 2023 10:52:11 -0500 Subject: [PATCH 13/16] Update collab_titlebar_item.rs --- crates/collab_ui2/src/collab_titlebar_item.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index a04b6a50aa..2cdf32ca36 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -38,7 +38,7 @@ use gpui::{ use project::{Project, RepositoryEntry}; use theme::ActiveTheme; use ui::{ - h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle2, ContextMenu, Icon, + h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconElement, KeyBinding, Tooltip, }; use util::ResultExt; @@ -204,11 +204,11 @@ impl Render for CollabTitlebarItem { "toggle_sharing", if is_shared { "Unshare" } else { "Share" }, ) - .style(ButtonStyle2::Subtle), + .style(ButtonStyle::Subtle), ) .child( IconButton::new("leave-call", ui::Icon::Exit) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .on_click({ let workspace = workspace.clone(); move |_, cx| { @@ -233,7 +233,7 @@ impl Render for CollabTitlebarItem { ui::Icon::Mic }, ) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .selected(is_muted) .on_click({ let workspace = workspace.clone(); @@ -248,7 +248,7 @@ impl Render for CollabTitlebarItem { ) .child( IconButton::new("mute-sound", speakers_icon) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .selected(is_deafened.clone()) .tooltip(move |cx| { Tooltip::with_meta( @@ -271,7 +271,7 @@ impl Render for CollabTitlebarItem { ) .child( IconButton::new("screen-share", ui::Icon::Screen) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .on_click(move |_, cx| { workspace .update(cx, |this, cx| { @@ -300,7 +300,7 @@ impl Render for CollabTitlebarItem { .color(Color::Muted), ), ) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), ) .anchor(gpui::AnchorCorner::TopRight), @@ -312,7 +312,7 @@ impl Render for CollabTitlebarItem { // IconElement::new(Icon::ChevronDown).color(Color::Muted), // ), // ) - // .style(ButtonStyle2::Subtle) + // .style(ButtonStyle::Subtle) // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), // ) }) @@ -489,7 +489,7 @@ impl CollabTitlebarItem { format!("{user_name} ({})", !is_shared), ) .color(Color::Player(participant_index)) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Toggle following", cx)), ), ) @@ -509,7 +509,7 @@ impl CollabTitlebarItem { div().border().border_color(gpui::red()).child( Button::new("project_name_trigger", name) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), ) } @@ -533,7 +533,7 @@ impl CollabTitlebarItem { Some( div().border().border_color(gpui::red()).child( Button::new("project_branch_trigger", branch_name) - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .tooltip(move |cx| { cx.build_view(|_| { Tooltip::new("Recent Branches") From aed11ee8cbeb25fca0a089b3d1b921c41a60d3e2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:22:12 +0100 Subject: [PATCH 14/16] editor tests: Reintroduce block_on_ticks. Co-authored-by: Antonio --- crates/editor2/src/display_map/wrap_map.rs | 608 +++++++++---------- crates/gpui2/src/executor.rs | 41 +- crates/gpui2/src/platform/test/dispatcher.rs | 17 +- 3 files changed, 353 insertions(+), 313 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index 5aeecbae97..c8025c7da9 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,337 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, -// MultiBuffer, -// }; -// use gpui::test::observe; -// use rand::prelude::*; -// use settings::SettingsStore; -// use smol::stream::StreamExt; -// use std::{cmp, env, num::NonZeroU32}; -// use text::Rope; +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, + MultiBuffer, + }; + use gpui::test::observe; + use rand::prelude::*; + use settings::SettingsStore; + use smol::stream::StreamExt; + use std::{cmp, env, num::NonZeroU32}; + use text::Rope; -// #[gpui::test(iterations = 100)] -// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { -// init_test(cx); + #[gpui::test(iterations = 100)] + async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { + init_test(cx); -// cx.foreground().set_block_on_ticks(0..=50); -// let operations = env::var("OPERATIONS") -// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) -// .unwrap_or(10); + cx.background_executor.set_block_on_ticks(0..=50); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); -// let font_cache = cx.font_cache().clone(); -// let font_system = cx.platform().fonts(); -// let mut wrap_width = if rng.gen_bool(0.1) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); -// let family_id = font_cache -// .load_family(&["Helvetica"], &Default::default()) -// .unwrap(); -// let font_id = font_cache -// .select_font(family_id, &Default::default()) -// .unwrap(); -// let font_size = 14.0; + let font_cache = cx.read(|cx| cx.font_cache().clone()); + let font_system = cx.platform().fonts(); + let mut wrap_width = if rng.gen_bool(0.1) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); + let family_id = font_cache + .load_family(&["Helvetica"], &Default::default()) + .unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; -// log::info!("Tab size: {}", tab_size); -// log::info!("Wrap width: {:?}", wrap_width); + log::info!("Tab size: {}", tab_size); + log::info!("Wrap width: {:?}", wrap_width); -// let buffer = cx.update(|cx| { -// if rng.gen() { -// MultiBuffer::build_random(&mut rng, cx) -// } else { -// let len = rng.gen_range(0..10); -// let text = util::RandomCharIter::new(&mut rng) -// .take(len) -// .collect::(); -// MultiBuffer::build_simple(&text, cx) -// } -// }); -// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); -// let tabs_snapshot = tab_map.set_max_expansion_column(32); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + let buffer = cx.update(|cx| { + if rng.gen() { + MultiBuffer::build_random(&mut rng, cx) + } else { + let len = rng.gen_range(0..10); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } + }); + let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let tabs_snapshot = tab_map.set_max_expansion_column(32); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); -// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (wrap_map, _) = -// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); -// let mut notifications = observe(&wrap_map, cx); + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); + let mut notifications = observe(&wrap_map, cx); -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } -// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { -// assert!(!map.is_rewrapping()); -// map.sync(tabs_snapshot.clone(), Vec::new(), cx) -// }); + let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); -// let actual_text = initial_snapshot.text(); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); -// log::info!("Wrapped text: {:?}", actual_text); + let actual_text = initial_snapshot.text(); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); + log::info!("Wrapped text: {:?}", actual_text); -// let mut next_inlay_id = 0; -// let mut edits = Vec::new(); -// for _i in 0..operations { -// log::info!("{} ==============================================", _i); + let mut next_inlay_id = 0; + let mut edits = Vec::new(); + for _i in 0..operations { + log::info!("{} ==============================================", _i); -// let mut buffer_edits = Vec::new(); -// match rng.gen_range(0..=100) { -// 0..=19 => { -// wrap_width = if rng.gen_bool(0.2) { -// None -// } else { -// Some(rng.gen_range(0.0..=1000.0)) -// }; -// log::info!("Setting wrap width to {:?}", wrap_width); -// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); -// } -// 20..=39 => { -// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// } -// 40..=59 => { -// let (inlay_snapshot, inlay_edits) = -// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// let (tabs_snapshot, tab_edits) = -// tab_map.sync(fold_snapshot, fold_edits, tab_size); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); -// } -// _ => { -// buffer.update(cx, |buffer, cx| { -// let subscription = buffer.subscribe(); -// let edit_count = rng.gen_range(1..=5); -// buffer.randomly_mutate(&mut rng, edit_count, cx); -// buffer_snapshot = buffer.snapshot(cx); -// buffer_edits.extend(subscription.consume()); -// }); -// } -// } + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=19 => { + wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=1000.0)) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + } + 40..=59 => { + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tabs_snapshot, tab_edits) = + tab_map.sync(fold_snapshot, fold_edits, tab_size); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); + } + _ => { + buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + buffer_edits.extend(subscription.consume()); + }); + } + } -// log::info!("Buffer text: {:?}", buffer_snapshot.text()); -// let (inlay_snapshot, inlay_edits) = -// inlay_map.sync(buffer_snapshot.clone(), buffer_edits); -// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); -// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); -// log::info!("FoldMap text: {:?}", fold_snapshot.text()); -// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); -// log::info!("TabMap text: {:?}", tabs_snapshot.text()); + log::info!("Buffer text: {:?}", buffer_snapshot.text()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + log::info!("FoldMap text: {:?}", fold_snapshot.text()); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); + log::info!("TabMap text: {:?}", tabs_snapshot.text()); -// let unwrapped_text = tabs_snapshot.text(); -// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); -// let (mut snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); -// snapshot.check_invariants(); -// snapshot.verify_chunks(&mut rng); -// edits.push((snapshot, wrap_edits)); + let unwrapped_text = tabs_snapshot.text(); + let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); + let (mut snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } -// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// let (mut wrapped_snapshot, wrap_edits) = -// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); -// let actual_text = wrapped_snapshot.text(); -// let actual_longest_row = wrapped_snapshot.longest_row(); -// log::info!("Wrapping finished: {:?}", actual_text); -// wrapped_snapshot.check_invariants(); -// wrapped_snapshot.verify_chunks(&mut rng); -// edits.push((wrapped_snapshot.clone(), wrap_edits)); -// assert_eq!( -// actual_text, expected_text, -// "unwrapped text is: {:?}", -// unwrapped_text -// ); + if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + let (mut wrapped_snapshot, wrap_edits) = + wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); + let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); + log::info!("Wrapping finished: {:?}", actual_text); + wrapped_snapshot.check_invariants(); + wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); + assert_eq!( + actual_text, expected_text, + "unwrapped text is: {:?}", + unwrapped_text + ); -// let mut summary = TextSummary::default(); -// for (ix, item) in wrapped_snapshot -// .transforms -// .items(&()) -// .into_iter() -// .enumerate() -// { -// summary += &item.summary.output; -// log::info!("{} summary: {:?}", ix, item.summary.output,); -// } + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } -// if tab_size.get() == 1 -// || !wrapped_snapshot -// .tab_snapshot -// .fold_snapshot -// .text() -// .contains('\t') -// { -// let mut expected_longest_rows = Vec::new(); -// let mut longest_line_len = -1; -// for (row, line) in expected_text.split('\n').enumerate() { -// let line_char_count = line.chars().count() as isize; -// if line_char_count > longest_line_len { -// expected_longest_rows.clear(); -// longest_line_len = line_char_count; -// } -// if line_char_count >= longest_line_len { -// expected_longest_rows.push(row as u32); -// } -// } + if tab_size.get() == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } -// assert!( -// expected_longest_rows.contains(&actual_longest_row), -// "incorrect longest row {}. expected {:?} with length {}", -// actual_longest_row, -// expected_longest_rows, -// longest_line_len, -// ) -// } -// } -// } + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } + } + } -// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); -// for (snapshot, patch) in edits { -// let snapshot_text = Rope::from(snapshot.text().as_str()); -// for edit in &patch { -// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); -// let old_end = initial_text.point_to_offset(cmp::min( -// Point::new(edit.new.start + edit.old.len() as u32, 0), -// initial_text.max_point(), -// )); -// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); -// let new_end = snapshot_text.point_to_offset(cmp::min( -// Point::new(edit.new.end, 0), -// snapshot_text.max_point(), -// )); -// let new_text = snapshot_text -// .chunks_in_range(new_start..new_end) -// .collect::(); + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); -// initial_text.replace(old_start..old_end, &new_text); -// } -// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); -// } + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } -// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// log::info!("Waiting for wrapping to finish"); -// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { -// notifications.next().await.unwrap(); -// } -// } -// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); -// } + if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + log::info!("Waiting for wrapping to finish"); + while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { + notifications.next().await.unwrap(); + } + } + wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); + } -// fn init_test(cx: &mut gpui::TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// }); -// } + fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground_executor().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + }); + } -// fn wrap_text( -// unwrapped_text: &str, -// wrap_width: Option, -// line_wrapper: &mut LineWrapper, -// ) -> String { -// if let Some(wrap_width) = wrap_width { -// let mut wrapped_text = String::new(); -// for (row, line) in unwrapped_text.split('\n').enumerate() { -// if row > 0 { -// wrapped_text.push('\n') -// } + fn wrap_text( + unwrapped_text: &str, + wrap_width: Option, + line_wrapper: &mut LineWrapper, + ) -> String { + if let Some(wrap_width) = wrap_width { + let mut wrapped_text = String::new(); + for (row, line) in unwrapped_text.split('\n').enumerate() { + if row > 0 { + wrapped_text.push('\n') + } -// let mut prev_ix = 0; -// for boundary in line_wrapper.wrap_line(line, wrap_width) { -// wrapped_text.push_str(&line[prev_ix..boundary.ix]); -// wrapped_text.push('\n'); -// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); -// prev_ix = boundary.ix; -// } -// wrapped_text.push_str(&line[prev_ix..]); -// } -// wrapped_text -// } else { -// unwrapped_text.to_string() -// } -// } + let mut prev_ix = 0; + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); + wrapped_text.push('\n'); + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; + } + wrapped_text.push_str(&line[prev_ix..]); + } + wrapped_text + } else { + unwrapped_text.to_string() + } + } -// impl WrapSnapshot { -// pub fn text(&self) -> String { -// self.text_chunks(0).collect() -// } + impl WrapSnapshot { + pub fn text(&self) -> String { + self.text_chunks(0).collect() + } -// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { -// self.chunks( -// wrap_row..self.max_point().row() + 1, -// false, -// Highlights::default(), -// ) -// .map(|h| h.text) -// } + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + Highlights::default(), + ) + .map(|h| h.text) + } -// fn verify_chunks(&mut self, rng: &mut impl Rng) { -// for _ in 0..5 { -// let mut end_row = rng.gen_range(0..=self.max_point().row()); -// let start_row = rng.gen_range(0..=end_row); -// end_row += 1; + fn verify_chunks(&mut self, rng: &mut impl Rng) { + for _ in 0..5 { + let mut end_row = rng.gen_range(0..=self.max_point().row()); + let start_row = rng.gen_range(0..=end_row); + end_row += 1; -// let mut expected_text = self.text_chunks(start_row).collect::(); -// if expected_text.ends_with('\n') { -// expected_text.push('\n'); -// } -// let mut expected_text = expected_text -// .lines() -// .take((end_row - start_row) as usize) -// .collect::>() -// .join("\n"); -// if end_row <= self.max_point().row() { -// expected_text.push('\n'); -// } + let mut expected_text = self.text_chunks(start_row).collect::(); + if expected_text.ends_with('\n') { + expected_text.push('\n'); + } + let mut expected_text = expected_text + .lines() + .take((end_row - start_row) as usize) + .collect::>() + .join("\n"); + if end_row <= self.max_point().row() { + expected_text.push('\n'); + } -// let actual_text = self -// .chunks(start_row..end_row, true, Highlights::default()) -// .map(|c| c.text) -// .collect::(); -// assert_eq!( -// expected_text, -// actual_text, -// "chunks != highlighted_chunks for rows {:?}", -// start_row..end_row -// ); -// } -// } -// } -// } + let actual_text = self + .chunks(start_row..end_row, true, Highlights::default()) + .map(|c| c.text) + .collect::(); + assert_eq!( + expected_text, + actual_text, + "chunks != highlighted_chunks for rows {:?}", + start_row..end_row + ); + } + } + } +} diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index cf138a90db..e446a0cb1e 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -128,11 +128,19 @@ impl BackgroundExecutor { #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn block_test(&self, future: impl Future) -> R { - self.block_internal(false, future) + if let Ok(value) = self.block_internal(false, future, usize::MAX) { + value + } else { + unreachable!() + } } pub fn block(&self, future: impl Future) -> R { - self.block_internal(true, future) + if let Ok(value) = self.block_internal(true, future, usize::MAX) { + value + } else { + unreachable!() + } } #[track_caller] @@ -140,7 +148,8 @@ impl BackgroundExecutor { &self, background_only: bool, future: impl Future, - ) -> R { + mut max_ticks: usize, + ) -> Result { pin_mut!(future); let unparker = self.dispatcher.unparker(); let awoken = Arc::new(AtomicBool::new(false)); @@ -156,8 +165,13 @@ impl BackgroundExecutor { loop { match future.as_mut().poll(&mut cx) { - Poll::Ready(result) => return result, + Poll::Ready(result) => return Ok(result), Poll::Pending => { + if max_ticks == 0 { + return Err(()); + } + max_ticks -= 1; + if !self.dispatcher.tick(background_only) { if awoken.swap(false, SeqCst) { continue; @@ -192,16 +206,24 @@ impl BackgroundExecutor { return Err(future); } + let max_ticks = if cfg!(any(test, feature = "test-support")) { + self.dispatcher + .as_test() + .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks()) + } else { + usize::MAX + }; let mut timer = self.timer(duration).fuse(); + let timeout = async { futures::select_biased! { value = future => Ok(value), _ = timer => Err(()), } }; - match self.block(timeout) { - Ok(value) => Ok(value), - Err(_) => Err(future), + match self.block_internal(true, timeout, max_ticks) { + Ok(Ok(value)) => Ok(value), + _ => Err(future), } } @@ -281,6 +303,11 @@ impl BackgroundExecutor { pub fn is_main_thread(&self) -> bool { self.dispatcher.is_main_thread() } + + #[cfg(any(test, feature = "test-support"))] + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher.as_test().unwrap().set_block_on_ticks(range); + } } impl ForegroundExecutor { diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index e77c1c0529..9023627d1e 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -7,6 +7,7 @@ use parking_lot::Mutex; use rand::prelude::*; use std::{ future::Future, + ops::RangeInclusive, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -36,6 +37,7 @@ struct TestDispatcherState { allow_parking: bool, waiting_backtrace: Option, deprioritized_task_labels: HashSet, + block_on_ticks: RangeInclusive, } impl TestDispatcher { @@ -53,6 +55,7 @@ impl TestDispatcher { allow_parking: false, waiting_backtrace: None, deprioritized_task_labels: Default::default(), + block_on_ticks: 0..=1000, }; TestDispatcher { @@ -82,8 +85,8 @@ impl TestDispatcher { } pub fn simulate_random_delay(&self) -> impl 'static + Send + Future { - pub struct YieldNow { - count: usize, + struct YieldNow { + pub(crate) count: usize, } impl Future for YieldNow { @@ -142,6 +145,16 @@ impl TestDispatcher { pub fn rng(&self) -> StdRng { self.state.lock().random.clone() } + + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.state.lock().block_on_ticks = range; + } + + pub fn gen_block_on_ticks(&self) -> usize { + let mut lock = self.state.lock(); + let block_on_ticks = lock.block_on_ticks.clone(); + lock.random.gen_range(block_on_ticks) + } } impl Clone for TestDispatcher { From a40a5fb212b2cc2a1838bdc7d50822e7514f77ce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:24:20 +0100 Subject: [PATCH 15/16] Revert "editor tests: Reintroduce block_on_ticks." This reverts commit aed11ee8cbeb25fca0a089b3d1b921c41a60d3e2. --- crates/editor2/src/display_map/wrap_map.rs | 608 +++++++++---------- crates/gpui2/src/executor.rs | 41 +- crates/gpui2/src/platform/test/dispatcher.rs | 17 +- 3 files changed, 313 insertions(+), 353 deletions(-) diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index c8025c7da9..5aeecbae97 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1026,337 +1026,337 @@ fn consolidate_wrap_edits(edits: &mut Vec) { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, - MultiBuffer, - }; - use gpui::test::observe; - use rand::prelude::*; - use settings::SettingsStore; - use smol::stream::StreamExt; - use std::{cmp, env, num::NonZeroU32}; - use text::Rope; +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{ +// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, +// MultiBuffer, +// }; +// use gpui::test::observe; +// use rand::prelude::*; +// use settings::SettingsStore; +// use smol::stream::StreamExt; +// use std::{cmp, env, num::NonZeroU32}; +// use text::Rope; - #[gpui::test(iterations = 100)] - async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { - init_test(cx); +// #[gpui::test(iterations = 100)] +// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { +// init_test(cx); - cx.background_executor.set_block_on_ticks(0..=50); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); +// cx.foreground().set_block_on_ticks(0..=50); +// let operations = env::var("OPERATIONS") +// .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) +// .unwrap_or(10); - let font_cache = cx.read(|cx| cx.font_cache().clone()); - let font_system = cx.platform().fonts(); - let mut wrap_width = if rng.gen_bool(0.1) { - None - } else { - Some(rng.gen_range(0.0..=1000.0)) - }; - let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); - let family_id = font_cache - .load_family(&["Helvetica"], &Default::default()) - .unwrap(); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; +// let font_cache = cx.font_cache().clone(); +// let font_system = cx.platform().fonts(); +// let mut wrap_width = if rng.gen_bool(0.1) { +// None +// } else { +// Some(rng.gen_range(0.0..=1000.0)) +// }; +// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); +// let family_id = font_cache +// .load_family(&["Helvetica"], &Default::default()) +// .unwrap(); +// let font_id = font_cache +// .select_font(family_id, &Default::default()) +// .unwrap(); +// let font_size = 14.0; - log::info!("Tab size: {}", tab_size); - log::info!("Wrap width: {:?}", wrap_width); +// log::info!("Tab size: {}", tab_size); +// log::info!("Wrap width: {:?}", wrap_width); - let buffer = cx.update(|cx| { - if rng.gen() { - MultiBuffer::build_random(&mut rng, cx) - } else { - let len = rng.gen_range(0..10); - let text = util::RandomCharIter::new(&mut rng) - .take(len) - .collect::(); - MultiBuffer::build_simple(&text, cx) - } - }); - let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); - log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); - log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); - log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); - let tabs_snapshot = tab_map.set_max_expansion_column(32); - log::info!("TabMap text: {:?}", tabs_snapshot.text()); +// let buffer = cx.update(|cx| { +// if rng.gen() { +// MultiBuffer::build_random(&mut rng, cx) +// } else { +// let len = rng.gen_range(0..10); +// let text = util::RandomCharIter::new(&mut rng) +// .take(len) +// .collect::(); +// MultiBuffer::build_simple(&text, cx) +// } +// }); +// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); +// log::info!("Buffer text: {:?}", buffer_snapshot.text()); +// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); +// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); +// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); +// log::info!("FoldMap text: {:?}", fold_snapshot.text()); +// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); +// let tabs_snapshot = tab_map.set_max_expansion_column(32); +// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); - let unwrapped_text = tabs_snapshot.text(); - let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); +// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system); +// let unwrapped_text = tabs_snapshot.text(); +// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - let (wrap_map, _) = - cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); - let mut notifications = observe(&wrap_map, cx); +// let (wrap_map, _) = +// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); +// let mut notifications = observe(&wrap_map, cx); - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } - let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { - assert!(!map.is_rewrapping()); - map.sync(tabs_snapshot.clone(), Vec::new(), cx) - }); +// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| { +// assert!(!map.is_rewrapping()); +// map.sync(tabs_snapshot.clone(), Vec::new(), cx) +// }); - let actual_text = initial_snapshot.text(); - assert_eq!( - actual_text, expected_text, - "unwrapped text is: {:?}", - unwrapped_text - ); - log::info!("Wrapped text: {:?}", actual_text); +// let actual_text = initial_snapshot.text(); +// assert_eq!( +// actual_text, expected_text, +// "unwrapped text is: {:?}", +// unwrapped_text +// ); +// log::info!("Wrapped text: {:?}", actual_text); - let mut next_inlay_id = 0; - let mut edits = Vec::new(); - for _i in 0..operations { - log::info!("{} ==============================================", _i); +// let mut next_inlay_id = 0; +// let mut edits = Vec::new(); +// for _i in 0..operations { +// log::info!("{} ==============================================", _i); - let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { - 0..=19 => { - wrap_width = if rng.gen_bool(0.2) { - None - } else { - Some(rng.gen_range(0.0..=1000.0)) - }; - log::info!("Setting wrap width to {:?}", wrap_width); - wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); - } - 20..=39 => { - for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { - let (tabs_snapshot, tab_edits) = - tab_map.sync(fold_snapshot, fold_edits, tab_size); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); - } - } - 40..=59 => { - let (inlay_snapshot, inlay_edits) = - inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); - let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); - let (tabs_snapshot, tab_edits) = - tab_map.sync(fold_snapshot, fold_edits, tab_size); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); - } - _ => { - buffer.update(cx, |buffer, cx| { - let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); - buffer.randomly_mutate(&mut rng, edit_count, cx); - buffer_snapshot = buffer.snapshot(cx); - buffer_edits.extend(subscription.consume()); - }); - } - } +// let mut buffer_edits = Vec::new(); +// match rng.gen_range(0..=100) { +// 0..=19 => { +// wrap_width = if rng.gen_bool(0.2) { +// None +// } else { +// Some(rng.gen_range(0.0..=1000.0)) +// }; +// log::info!("Setting wrap width to {:?}", wrap_width); +// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); +// } +// 20..=39 => { +// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { +// let (tabs_snapshot, tab_edits) = +// tab_map.sync(fold_snapshot, fold_edits, tab_size); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); +// } +// } +// 40..=59 => { +// let (inlay_snapshot, inlay_edits) = +// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); +// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); +// let (tabs_snapshot, tab_edits) = +// tab_map.sync(fold_snapshot, fold_edits, tab_size); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); +// } +// _ => { +// buffer.update(cx, |buffer, cx| { +// let subscription = buffer.subscribe(); +// let edit_count = rng.gen_range(1..=5); +// buffer.randomly_mutate(&mut rng, edit_count, cx); +// buffer_snapshot = buffer.snapshot(cx); +// buffer_edits.extend(subscription.consume()); +// }); +// } +// } - log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (inlay_snapshot, inlay_edits) = - inlay_map.sync(buffer_snapshot.clone(), buffer_edits); - log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); - log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); - log::info!("TabMap text: {:?}", tabs_snapshot.text()); +// log::info!("Buffer text: {:?}", buffer_snapshot.text()); +// let (inlay_snapshot, inlay_edits) = +// inlay_map.sync(buffer_snapshot.clone(), buffer_edits); +// log::info!("InlayMap text: {:?}", inlay_snapshot.text()); +// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); +// log::info!("FoldMap text: {:?}", fold_snapshot.text()); +// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); +// log::info!("TabMap text: {:?}", tabs_snapshot.text()); - let unwrapped_text = tabs_snapshot.text(); - let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - let (mut snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); - snapshot.check_invariants(); - snapshot.verify_chunks(&mut rng); - edits.push((snapshot, wrap_edits)); +// let unwrapped_text = tabs_snapshot.text(); +// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); +// let (mut snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx)); +// snapshot.check_invariants(); +// snapshot.verify_chunks(&mut rng); +// edits.push((snapshot, wrap_edits)); - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { - log::info!("Waiting for wrapping to finish"); - while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } - wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); - } +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { +// log::info!("Waiting for wrapping to finish"); +// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } +// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); +// } - if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - let (mut wrapped_snapshot, wrap_edits) = - wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); - let actual_text = wrapped_snapshot.text(); - let actual_longest_row = wrapped_snapshot.longest_row(); - log::info!("Wrapping finished: {:?}", actual_text); - wrapped_snapshot.check_invariants(); - wrapped_snapshot.verify_chunks(&mut rng); - edits.push((wrapped_snapshot.clone(), wrap_edits)); - assert_eq!( - actual_text, expected_text, - "unwrapped text is: {:?}", - unwrapped_text - ); +// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// let (mut wrapped_snapshot, wrap_edits) = +// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); +// let actual_text = wrapped_snapshot.text(); +// let actual_longest_row = wrapped_snapshot.longest_row(); +// log::info!("Wrapping finished: {:?}", actual_text); +// wrapped_snapshot.check_invariants(); +// wrapped_snapshot.verify_chunks(&mut rng); +// edits.push((wrapped_snapshot.clone(), wrap_edits)); +// assert_eq!( +// actual_text, expected_text, +// "unwrapped text is: {:?}", +// unwrapped_text +// ); - let mut summary = TextSummary::default(); - for (ix, item) in wrapped_snapshot - .transforms - .items(&()) - .into_iter() - .enumerate() - { - summary += &item.summary.output; - log::info!("{} summary: {:?}", ix, item.summary.output,); - } +// let mut summary = TextSummary::default(); +// for (ix, item) in wrapped_snapshot +// .transforms +// .items(&()) +// .into_iter() +// .enumerate() +// { +// summary += &item.summary.output; +// log::info!("{} summary: {:?}", ix, item.summary.output,); +// } - if tab_size.get() == 1 - || !wrapped_snapshot - .tab_snapshot - .fold_snapshot - .text() - .contains('\t') - { - let mut expected_longest_rows = Vec::new(); - let mut longest_line_len = -1; - for (row, line) in expected_text.split('\n').enumerate() { - let line_char_count = line.chars().count() as isize; - if line_char_count > longest_line_len { - expected_longest_rows.clear(); - longest_line_len = line_char_count; - } - if line_char_count >= longest_line_len { - expected_longest_rows.push(row as u32); - } - } +// if tab_size.get() == 1 +// || !wrapped_snapshot +// .tab_snapshot +// .fold_snapshot +// .text() +// .contains('\t') +// { +// let mut expected_longest_rows = Vec::new(); +// let mut longest_line_len = -1; +// for (row, line) in expected_text.split('\n').enumerate() { +// let line_char_count = line.chars().count() as isize; +// if line_char_count > longest_line_len { +// expected_longest_rows.clear(); +// longest_line_len = line_char_count; +// } +// if line_char_count >= longest_line_len { +// expected_longest_rows.push(row as u32); +// } +// } - assert!( - expected_longest_rows.contains(&actual_longest_row), - "incorrect longest row {}. expected {:?} with length {}", - actual_longest_row, - expected_longest_rows, - longest_line_len, - ) - } - } - } +// assert!( +// expected_longest_rows.contains(&actual_longest_row), +// "incorrect longest row {}. expected {:?} with length {}", +// actual_longest_row, +// expected_longest_rows, +// longest_line_len, +// ) +// } +// } +// } - let mut initial_text = Rope::from(initial_snapshot.text().as_str()); - for (snapshot, patch) in edits { - let snapshot_text = Rope::from(snapshot.text().as_str()); - for edit in &patch { - let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = initial_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old.len() as u32, 0), - initial_text.max_point(), - )); - let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); - let new_end = snapshot_text.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - snapshot_text.max_point(), - )); - let new_text = snapshot_text - .chunks_in_range(new_start..new_end) - .collect::(); +// let mut initial_text = Rope::from(initial_snapshot.text().as_str()); +// for (snapshot, patch) in edits { +// let snapshot_text = Rope::from(snapshot.text().as_str()); +// for edit in &patch { +// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); +// let old_end = initial_text.point_to_offset(cmp::min( +// Point::new(edit.new.start + edit.old.len() as u32, 0), +// initial_text.max_point(), +// )); +// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); +// let new_end = snapshot_text.point_to_offset(cmp::min( +// Point::new(edit.new.end, 0), +// snapshot_text.max_point(), +// )); +// let new_text = snapshot_text +// .chunks_in_range(new_start..new_end) +// .collect::(); - initial_text.replace(old_start..old_end, &new_text); - } - assert_eq!(initial_text.to_string(), snapshot_text.to_string()); - } +// initial_text.replace(old_start..old_end, &new_text); +// } +// assert_eq!(initial_text.to_string(), snapshot_text.to_string()); +// } - if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - log::info!("Waiting for wrapping to finish"); - while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { - notifications.next().await.unwrap(); - } - } - wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); - } +// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// log::info!("Waiting for wrapping to finish"); +// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) { +// notifications.next().await.unwrap(); +// } +// } +// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); +// } - fn init_test(cx: &mut gpui::TestAppContext) { - cx.foreground_executor().forbid_parking(); - cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); - }); - } +// fn init_test(cx: &mut gpui::TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init((), cx); +// }); +// } - fn wrap_text( - unwrapped_text: &str, - wrap_width: Option, - line_wrapper: &mut LineWrapper, - ) -> String { - if let Some(wrap_width) = wrap_width { - let mut wrapped_text = String::new(); - for (row, line) in unwrapped_text.split('\n').enumerate() { - if row > 0 { - wrapped_text.push('\n') - } +// fn wrap_text( +// unwrapped_text: &str, +// wrap_width: Option, +// line_wrapper: &mut LineWrapper, +// ) -> String { +// if let Some(wrap_width) = wrap_width { +// let mut wrapped_text = String::new(); +// for (row, line) in unwrapped_text.split('\n').enumerate() { +// if row > 0 { +// wrapped_text.push('\n') +// } - let mut prev_ix = 0; - for boundary in line_wrapper.wrap_line(line, wrap_width) { - wrapped_text.push_str(&line[prev_ix..boundary.ix]); - wrapped_text.push('\n'); - wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); - prev_ix = boundary.ix; - } - wrapped_text.push_str(&line[prev_ix..]); - } - wrapped_text - } else { - unwrapped_text.to_string() - } - } +// let mut prev_ix = 0; +// for boundary in line_wrapper.wrap_line(line, wrap_width) { +// wrapped_text.push_str(&line[prev_ix..boundary.ix]); +// wrapped_text.push('\n'); +// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); +// prev_ix = boundary.ix; +// } +// wrapped_text.push_str(&line[prev_ix..]); +// } +// wrapped_text +// } else { +// unwrapped_text.to_string() +// } +// } - impl WrapSnapshot { - pub fn text(&self) -> String { - self.text_chunks(0).collect() - } +// impl WrapSnapshot { +// pub fn text(&self) -> String { +// self.text_chunks(0).collect() +// } - pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks( - wrap_row..self.max_point().row() + 1, - false, - Highlights::default(), - ) - .map(|h| h.text) - } +// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { +// self.chunks( +// wrap_row..self.max_point().row() + 1, +// false, +// Highlights::default(), +// ) +// .map(|h| h.text) +// } - fn verify_chunks(&mut self, rng: &mut impl Rng) { - for _ in 0..5 { - let mut end_row = rng.gen_range(0..=self.max_point().row()); - let start_row = rng.gen_range(0..=end_row); - end_row += 1; +// fn verify_chunks(&mut self, rng: &mut impl Rng) { +// for _ in 0..5 { +// let mut end_row = rng.gen_range(0..=self.max_point().row()); +// let start_row = rng.gen_range(0..=end_row); +// end_row += 1; - let mut expected_text = self.text_chunks(start_row).collect::(); - if expected_text.ends_with('\n') { - expected_text.push('\n'); - } - let mut expected_text = expected_text - .lines() - .take((end_row - start_row) as usize) - .collect::>() - .join("\n"); - if end_row <= self.max_point().row() { - expected_text.push('\n'); - } +// let mut expected_text = self.text_chunks(start_row).collect::(); +// if expected_text.ends_with('\n') { +// expected_text.push('\n'); +// } +// let mut expected_text = expected_text +// .lines() +// .take((end_row - start_row) as usize) +// .collect::>() +// .join("\n"); +// if end_row <= self.max_point().row() { +// expected_text.push('\n'); +// } - let actual_text = self - .chunks(start_row..end_row, true, Highlights::default()) - .map(|c| c.text) - .collect::(); - assert_eq!( - expected_text, - actual_text, - "chunks != highlighted_chunks for rows {:?}", - start_row..end_row - ); - } - } - } -} +// let actual_text = self +// .chunks(start_row..end_row, true, Highlights::default()) +// .map(|c| c.text) +// .collect::(); +// assert_eq!( +// expected_text, +// actual_text, +// "chunks != highlighted_chunks for rows {:?}", +// start_row..end_row +// ); +// } +// } +// } +// } diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index e446a0cb1e..cf138a90db 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -128,19 +128,11 @@ impl BackgroundExecutor { #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn block_test(&self, future: impl Future) -> R { - if let Ok(value) = self.block_internal(false, future, usize::MAX) { - value - } else { - unreachable!() - } + self.block_internal(false, future) } pub fn block(&self, future: impl Future) -> R { - if let Ok(value) = self.block_internal(true, future, usize::MAX) { - value - } else { - unreachable!() - } + self.block_internal(true, future) } #[track_caller] @@ -148,8 +140,7 @@ impl BackgroundExecutor { &self, background_only: bool, future: impl Future, - mut max_ticks: usize, - ) -> Result { + ) -> R { pin_mut!(future); let unparker = self.dispatcher.unparker(); let awoken = Arc::new(AtomicBool::new(false)); @@ -165,13 +156,8 @@ impl BackgroundExecutor { loop { match future.as_mut().poll(&mut cx) { - Poll::Ready(result) => return Ok(result), + Poll::Ready(result) => return result, Poll::Pending => { - if max_ticks == 0 { - return Err(()); - } - max_ticks -= 1; - if !self.dispatcher.tick(background_only) { if awoken.swap(false, SeqCst) { continue; @@ -206,24 +192,16 @@ impl BackgroundExecutor { return Err(future); } - let max_ticks = if cfg!(any(test, feature = "test-support")) { - self.dispatcher - .as_test() - .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks()) - } else { - usize::MAX - }; let mut timer = self.timer(duration).fuse(); - let timeout = async { futures::select_biased! { value = future => Ok(value), _ = timer => Err(()), } }; - match self.block_internal(true, timeout, max_ticks) { - Ok(Ok(value)) => Ok(value), - _ => Err(future), + match self.block(timeout) { + Ok(value) => Ok(value), + Err(_) => Err(future), } } @@ -303,11 +281,6 @@ impl BackgroundExecutor { pub fn is_main_thread(&self) -> bool { self.dispatcher.is_main_thread() } - - #[cfg(any(test, feature = "test-support"))] - pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { - self.dispatcher.as_test().unwrap().set_block_on_ticks(range); - } } impl ForegroundExecutor { diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 9023627d1e..e77c1c0529 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -7,7 +7,6 @@ use parking_lot::Mutex; use rand::prelude::*; use std::{ future::Future, - ops::RangeInclusive, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -37,7 +36,6 @@ struct TestDispatcherState { allow_parking: bool, waiting_backtrace: Option, deprioritized_task_labels: HashSet, - block_on_ticks: RangeInclusive, } impl TestDispatcher { @@ -55,7 +53,6 @@ impl TestDispatcher { allow_parking: false, waiting_backtrace: None, deprioritized_task_labels: Default::default(), - block_on_ticks: 0..=1000, }; TestDispatcher { @@ -85,8 +82,8 @@ impl TestDispatcher { } pub fn simulate_random_delay(&self) -> impl 'static + Send + Future { - struct YieldNow { - pub(crate) count: usize, + pub struct YieldNow { + count: usize, } impl Future for YieldNow { @@ -145,16 +142,6 @@ impl TestDispatcher { pub fn rng(&self) -> StdRng { self.state.lock().random.clone() } - - pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { - self.state.lock().block_on_ticks = range; - } - - pub fn gen_block_on_ticks(&self) -> usize { - let mut lock = self.state.lock(); - let block_on_ticks = lock.block_on_ticks.clone(); - lock.random.gen_range(block_on_ticks) - } } impl Clone for TestDispatcher { From 85d72f63c1042cc16ac39853e04890fc110925cc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 1 Dec 2023 12:18:35 -0500 Subject: [PATCH 16/16] Add Tinted button style placeholder and document ButtonLike (#3476) - Adds a placeholder `ButtonStyle::Tinted`. - Note: Using this now will just give you a block of `gpui::red()` - Documents ButtonLike and ButtonStyle to hopefully help make choosing a button style easier. Release Notes: - N/A --- .../ui2/src/components/button/button_like.rs | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 29864a895d..3c48eee378 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -5,18 +5,44 @@ use crate::h_stack; use crate::prelude::*; pub trait ButtonCommon: Clickable + Disableable { + /// A unique element id to help identify the button. fn id(&self) -> &ElementId; + /// The visual style of the button. + /// + /// Mosty commonly will be `ButtonStyle::Subtle`, or `ButtonStyle::Filled` + /// for an emphasized button. fn style(self, style: ButtonStyle) -> Self; + /// The size of the button. + /// + /// Most buttons will use the default size. + /// + /// ButtonSize can also be used to help build non-button elements + /// that are consistently sized with buttons. fn size(self, size: ButtonSize) -> Self; + /// The tooltip that shows when a user hovers over the button. + /// + /// Nearly all interactable elements should have a tooltip. Some example + /// exceptions might a scroll bar, or a slider. fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self; } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum ButtonStyle { - #[default] + /// A filled button with a solid background color. Provides emphasis versus + /// the more common subtle button. Filled, - // Tinted, + /// 🚧 Under construction 🚧 + /// + /// Used to emphasize a button in some way, like a selected state, or a semantic + /// coloring like an error or success button. + Tinted, + /// The default button style, used for most buttons. Has a transparent background, + /// but has a background color to indicate states like hover and active. + #[default] Subtle, + /// Used for buttons that only change forground color on hover and active states. + /// + /// TODO: Better docs for this. Transparent, } @@ -40,6 +66,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Tinted => ButtonLikeStyles { + background: gpui::red(), + border_color: gpui::red(), + label_color: gpui::red(), + icon_color: gpui::red(), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: transparent_black(), @@ -63,6 +95,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Tinted => ButtonLikeStyles { + background: gpui::red(), + border_color: gpui::red(), + label_color: gpui::red(), + icon_color: gpui::red(), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, border_color: transparent_black(), @@ -88,6 +126,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Tinted => ButtonLikeStyles { + background: gpui::red(), + border_color: gpui::red(), + label_color: gpui::red(), + icon_color: gpui::red(), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_active, border_color: transparent_black(), @@ -114,6 +158,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Tinted => ButtonLikeStyles { + background: gpui::red(), + border_color: gpui::red(), + label_color: gpui::red(), + icon_color: gpui::red(), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: cx.theme().colors().border_focused, @@ -137,6 +187,12 @@ impl ButtonStyle { label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, + ButtonStyle::Tinted => ButtonLikeStyles { + background: gpui::red(), + border_color: gpui::red(), + label_color: gpui::red(), + icon_color: gpui::red(), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_disabled, border_color: cx.theme().colors().border_disabled, @@ -153,6 +209,8 @@ impl ButtonStyle { } } +/// ButtonSize can also be used to help build non-button elements +/// that are consistently sized with buttons. #[derive(Default, PartialEq, Clone, Copy)] pub enum ButtonSize { #[default] @@ -171,6 +229,11 @@ impl ButtonSize { } } +/// A button-like element that can be used to create a custom button when +/// prebuilt buttons are not sufficient. Use this sparingly, as it is +/// unconstrained and may make the UI feel less consistent. +/// +/// This is also used to build the prebuilt buttons. #[derive(IntoElement)] pub struct ButtonLike { id: ElementId,