(panel: Entity) -> Self {
- Self {
- id: PanelId::from(panel.entity_id()),
- panel: Arc::new(panel),
- }
- }
-
- /// Downcast a base handle back to the coop handle.
- pub(crate) fn of(panel: &Arc) -> Option<&Self> {
- panel.as_any().downcast_ref::()
- }
-
- pub(crate) fn panel(&self) -> &Arc {
- &self.panel
- }
-}
-
-impl gpui_base::dock::PanelView for PanelHandle {
- fn panel_name(&self, _: &App) -> &'static str {
- "CoopPanel"
- }
-
- fn panel_id(&self, _: &App) -> PanelId {
- self.id
- }
-
- fn closable(&self, cx: &App) -> bool {
- self.panel.closable(cx)
- }
-
- fn zoomable(&self, cx: &App) -> bool {
- self.panel.zoomable(cx)
- }
-
- fn visible(&self, cx: &App) -> bool {
- self.panel.visible(cx)
- }
-
- fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) {
- self.panel.set_active(active, window, cx);
- }
-
- fn set_zoomed(&self, zoomed: bool, _: &mut Window, cx: &mut App) {
- self.panel.set_zoomed(zoomed, cx);
- }
-
- fn on_added_to(
- &self,
- _group: gpui::WeakEntity,
- _: &mut Window,
- _: &mut App,
- ) {
- }
-
- fn on_removed(&self, window: &mut Window, cx: &mut App) {
- self.panel.on_removed(window, cx);
- }
-
- fn view(&self) -> AnyView {
- self.panel.view()
- }
-
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.panel.focus_handle(cx)
- }
-
- fn dump(&self, cx: &App) -> PanelState {
- PanelState::new(self.panel_name(cx))
- }
-
- fn as_any(&self) -> &dyn Any {
- self
- }
-}
diff --git a/crates/ui/src/group_box.rs b/crates/ui/src/group_box.rs
deleted file mode 100644
index 407b6d4f..00000000
--- a/crates/ui/src/group_box.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use gpui::prelude::FluentBuilder;
-use gpui::{
- AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
- StyleRefinement, Styled, Window, div, px,
-};
-use smallvec::SmallVec;
-use theme::{ActiveTheme, TextStyle, Typeset as _};
-
-use crate::{StyledExt as _, v_flex};
-
-/// The variant of the GroupBox.
-#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
-pub enum GroupBoxVariant {
- #[default]
- Normal,
- Fill,
-}
-
-/// Trait to add GroupBox variant methods to elements.
-pub trait GroupBoxVariants: Sized {
- /// Set the variant of the [`GroupBox`].
- #[must_use]
- fn with_variant(self, variant: GroupBoxVariant) -> Self;
-
- /// Set to use [`GroupBoxVariant::Fill`] to GroupBox.
- #[must_use]
- fn fill(self) -> Self {
- self.with_variant(GroupBoxVariant::Fill)
- }
-}
-
-/// A styled container with an optional title that groups related content together.
-#[derive(IntoElement)]
-pub struct GroupBox {
- id: Option,
- variant: GroupBoxVariant,
- style: StyleRefinement,
- title: Option,
- children: SmallVec<[AnyElement; 1]>,
-}
-
-impl GroupBox {
- /// Create a new GroupBox.
- pub fn new() -> Self {
- Self {
- id: None,
- variant: GroupBoxVariant::default(),
- style: StyleRefinement::default(),
- title: None,
- children: SmallVec::new(),
- }
- }
-
- /// Set the id of the group box, default is None.
- #[must_use]
- pub fn id(mut self, id: impl Into) -> Self {
- self.id = Some(id.into());
- self
- }
-
- /// Set the title of the group box, default is None.
- #[must_use]
- pub fn title(mut self, title: impl IntoElement) -> Self {
- self.title = Some(title.into_any_element());
- self
- }
-}
-
-impl Default for GroupBox {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl ParentElement for GroupBox {
- fn extend(&mut self, elements: impl IntoIterator- ) {
- self.children.extend(elements);
- }
-}
-
-impl Styled for GroupBox {
- fn style(&mut self) -> &mut StyleRefinement {
- &mut self.style
- }
-}
-
-impl GroupBoxVariants for GroupBox {
- fn with_variant(mut self, variant: GroupBoxVariant) -> Self {
- self.variant = variant;
- self
- }
-}
-
-impl RenderOnce for GroupBox {
- fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
- let (bg, has_paddings) = match self.variant {
- GroupBoxVariant::Normal => (None, false),
- GroupBoxVariant::Fill => (Some(cx.theme().surface), true),
- };
-
- v_flex()
- .id(self.id.unwrap_or("group-box".into()))
- .w_full()
- .when(has_paddings, |this| this.gap_1p5())
- .when(!has_paddings, |this| this.gap_2())
- .refine_style(&self.style)
- .when_some(self.title, |this, title| {
- this.child(
- div()
- .text_color(cx.theme().text_muted)
- .text_style(TextStyle::Subheadline)
- .font_semibold()
- .child(title),
- )
- })
- .child(
- v_flex()
- .when_some(bg, |this, bg| this.bg(bg))
- .text_color(cx.theme().text)
- .when(has_paddings, |this| this.p_2())
- .gap_4()
- .rounded(px(theme::panel_radius()))
- .children(self.children),
- )
- }
-}
diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs
deleted file mode 100644
index 90fa8029..00000000
--- a/crates/ui/src/icon.rs
+++ /dev/null
@@ -1,274 +0,0 @@
-use gpui::prelude::FluentBuilder as _;
-use gpui::{
- AnyElement, App, Context, Hsla, IntoElement, Render, RenderOnce, SharedString, StyleRefinement,
- Styled, Svg, Window, svg,
-};
-use theme::ActiveTheme;
-
-use crate::{Sizable, Size};
-
-impl From for Icon {
- fn from(value: IconName) -> Self {
- Icon::build(value)
- }
-}
-
-#[derive(IntoElement, Clone)]
-pub enum IconName {
- ArrowLeft,
- ArrowRight,
- ArrowDownCircle,
- Book,
- Ban,
- ChevronDown,
- CaretDown,
- CaretRight,
- Check,
- CheckCircle,
- Close,
- CloseCircle,
- CloseCircleFill,
- Compass,
- Copy,
- Door,
- Ellipsis,
- Emoji,
- Eye,
- Input,
- Info,
- Invite,
- Inbox,
- Link,
- Loader,
- Lock,
- Login,
- Logout,
- Message,
- Moon,
- Plus,
- PlusChat,
- PlusCircle,
- Profile,
- Relay,
- Reply,
- Search,
- Settings,
- Settings2,
- Sun,
- Group,
- Upload,
- Edit,
- PanelLeft,
- PanelLeftOpen,
- PanelRight,
- PanelRightOpen,
- PanelBottom,
- PanelBottomOpen,
- PaperPlaneFill,
- Warning,
- WindowClose,
- WindowMaximize,
- WindowMinimize,
- WindowRestore,
-}
-
-impl IconName {
- fn path(self) -> SharedString {
- match self {
- Self::ArrowLeft => "icons/arrow-left.svg",
- Self::ArrowRight => "icons/arrow-right.svg",
- Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
- Self::Book => "icons/book.svg",
- Self::Ban => "icons/ban.svg",
- Self::ChevronDown => "icons/chevron-down.svg",
- Self::CaretDown => "icons/caret-down.svg",
- Self::CaretRight => "icons/caret-right.svg",
- Self::Check => "icons/check.svg",
- Self::CheckCircle => "icons/check-circle.svg",
- Self::Close => "icons/close.svg",
- Self::CloseCircle => "icons/close-circle.svg",
- Self::CloseCircleFill => "icons/close-circle-fill.svg",
- Self::Compass => "icons/compass.svg",
- Self::Copy => "icons/copy.svg",
- Self::Door => "icons/door.svg",
- Self::Ellipsis => "icons/ellipsis.svg",
- Self::Emoji => "icons/emoji.svg",
- Self::Eye => "icons/eye.svg",
- Self::Input => "icons/input.svg",
- Self::Info => "icons/info.svg",
- Self::Invite => "icons/invite.svg",
- Self::Inbox => "icons/inbox.svg",
- Self::Link => "icons/link.svg",
- Self::Loader => "icons/loader.svg",
- Self::Lock => "icons/lock.svg",
- Self::Login => "icons/login.svg",
- Self::Logout => "icons/logout.svg",
- Self::Message => "icons/message.svg",
- Self::Moon => "icons/moon.svg",
- Self::Plus => "icons/plus.svg",
- Self::PlusChat => "icons/plus-chat.svg",
- Self::PlusCircle => "icons/plus-circle.svg",
- Self::Profile => "icons/profile.svg",
- Self::Relay => "icons/relay.svg",
- Self::Reply => "icons/reply.svg",
- Self::Search => "icons/search.svg",
- Self::Settings => "icons/settings.svg",
- Self::Settings2 => "icons/settings2.svg",
- Self::Sun => "icons/sun.svg",
- Self::Upload => "icons/upload.svg",
- Self::Group => "icons/group.svg",
- Self::PanelLeft => "icons/panel-left.svg",
- Self::PanelLeftOpen => "icons/panel-left-open.svg",
- Self::PanelRight => "icons/panel-right.svg",
- Self::PanelRightOpen => "icons/panel-right-open.svg",
- Self::PanelBottom => "icons/panel-bottom.svg",
- Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
- Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
- Self::Edit => "icons/edit.svg",
- Self::Warning => "icons/warning.svg",
- Self::WindowClose => "icons/window-close.svg",
- Self::WindowMaximize => "icons/window-maximize.svg",
- Self::WindowMinimize => "icons/window-minimize.svg",
- Self::WindowRestore => "icons/window-restore.svg",
- }
- .into()
- }
-}
-
-impl From for AnyElement {
- fn from(val: IconName) -> Self {
- Icon::build(val).into_any_element()
- }
-}
-
-impl RenderOnce for IconName {
- fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
- Icon::build(self)
- }
-}
-
-#[derive(IntoElement)]
-pub struct Icon {
- base: Svg,
- style: StyleRefinement,
- path: SharedString,
- text_color: Option,
- size: Option,
-}
-
-impl Default for Icon {
- fn default() -> Self {
- Self {
- base: svg().flex_none().size_4(),
- style: StyleRefinement::default(),
- path: "".into(),
- text_color: None,
- size: None,
- }
- }
-}
-
-impl Clone for Icon {
- fn clone(&self) -> Self {
- let mut this = Self::default().path(self.path.clone());
- this.style = self.style.clone();
- this.size = self.size;
- this.text_color = self.text_color;
- this
- }
-}
-
-impl Icon {
- pub fn new(icon: impl Into) -> Self {
- icon.into()
- }
-
- fn build(name: IconName) -> Self {
- Self::default().path(name.path())
- }
-
- /// Sets the icon path, relative to the assets bundle (for example `icons/foo.svg`).
- pub fn path(mut self, path: impl Into) -> Self {
- self.path = path.into();
- self
- }
-
- pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
- self.base = self.base.with_transformation(transformation);
- self
- }
-
- pub fn empty() -> Self {
- Self::default()
- }
-}
-
-impl Styled for Icon {
- fn style(&mut self) -> &mut StyleRefinement {
- &mut self.style
- }
-
- fn text_color(mut self, color: impl Into) -> Self {
- self.text_color = Some(color.into());
- self
- }
-}
-
-impl Sizable for Icon {
- fn with_size(mut self, size: impl Into) -> Self {
- self.size = Some(size.into());
- self
- }
-}
-
-impl RenderOnce for Icon {
- fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
- let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
- let text_size = window.text_style().font_size.to_pixels(window.rem_size());
- let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
-
- let mut base = self.base;
- *base.style() = self.style;
-
- base.flex_shrink_0()
- .text_color(text_color)
- .when(!has_base_size, |this| this.size(text_size))
- .when_some(self.size, |this, size| match size {
- Size::Size(px) => this.size(px),
- Size::XSmall => this.size_3(),
- Size::Small => this.size_4(),
- Size::Medium => this.size_5(),
- Size::Large => this.size_6(),
- })
- .path(self.path)
- }
-}
-
-impl From for AnyElement {
- fn from(val: Icon) -> Self {
- val.into_any_element()
- }
-}
-
-impl Render for Icon {
- fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let text_color = self.text_color.unwrap_or_else(|| cx.theme().text_muted);
- let text_size = window.text_style().font_size.to_pixels(window.rem_size());
- let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
-
- let mut base = svg().flex_none();
- *base.style() = self.style.clone();
-
- base.flex_shrink_0()
- .text_color(text_color)
- .when(!has_base_size, |this| this.size(text_size))
- .when_some(self.size, |this, size| match size {
- Size::Size(px) => this.size(px),
- Size::XSmall => this.size_3(),
- Size::Small => this.size_4(),
- Size::Medium => this.size_5(),
- Size::Large => this.size_6(),
- })
- .path(self.path.clone())
- }
-}
diff --git a/crates/ui/src/indicator.rs b/crates/ui/src/indicator.rs
deleted file mode 100644
index 27a2da21..00000000
--- a/crates/ui/src/indicator.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use gpui::prelude::FluentBuilder as _;
-use gpui::{
- App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _, Transformation, Window, div,
- percentage,
-};
-use instant::Duration;
-
-use crate::motion::{EASE_IN_OUT, MotionSpec, Painter, pulse_delta};
-use crate::{Icon, IconName, Sizable, Size};
-
-#[derive(IntoElement)]
-pub struct Indicator {
- size: Size,
- icon: Icon,
- speed: Duration,
- color: Option,
-}
-
-impl Default for Indicator {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl Indicator {
- pub fn new() -> Self {
- Self {
- size: Size::Small,
- speed: Duration::from_secs(1),
- icon: Icon::new(IconName::Loader),
- color: None,
- }
- }
-
- pub fn color(mut self, color: Hsla) -> Self {
- self.color = Some(color);
- self
- }
-}
-
-impl Sizable for Indicator {
- fn with_size(mut self, size: impl Into) -> Self {
- self.size = size.into();
- self
- }
-}
-
-impl RenderOnce for Indicator {
- fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
- let spec = MotionSpec::new(self.speed.as_millis().max(1) as u64, EASE_IN_OUT);
- let phase = pulse_delta(&spec, Painter::from(window.current_view()), cx);
- div().child(
- self.icon
- .with_size(self.size)
- .when_some(self.color, |this, color| this.text_color(color))
- .transform(Transformation::rotate(percentage(phase))),
- )
- }
-}
diff --git a/crates/ui/src/input/clear_button.rs b/crates/ui/src/input/clear_button.rs
deleted file mode 100644
index f220318f..00000000
--- a/crates/ui/src/input/clear_button.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use gpui::{App, Styled};
-use theme::ActiveTheme;
-
-use crate::button::{Button, ButtonVariants as _};
-use crate::{Icon, IconName, Sizable as _};
-
-#[inline]
-pub(crate) fn clear_button(cx: &App) -> Button {
- Button::new("clean")
- .icon(Icon::new(IconName::CloseCircle))
- .ghost()
- .xsmall()
- .tab_stop(false)
- .text_color(cx.theme().text_faint)
-}
diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs
deleted file mode 100644
index 57911b1f..00000000
--- a/crates/ui/src/input/input.rs
+++ /dev/null
@@ -1,277 +0,0 @@
-use gpui::prelude::FluentBuilder as _;
-use gpui::{
- App, DefiniteLength, Edges, Entity, Hsla, InteractiveElement as _, IntoElement, MouseButton,
- ParentElement as _, Pixels, Rems, RenderOnce, StyleRefinement, Styled, TextAlign, Window, div,
- px,
-};
-use gpui_base::InputBase;
-use gpui_base::input::{InputBaseState, InputEditorStyle, InputMode, InputModeKind, TextareaMode};
-use theme::{ActiveTheme, button_radius};
-
-use crate::indicator::Indicator;
-use crate::input::clear_button;
-use crate::{Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
-
-#[cfg(any(target_os = "linux", target_os = "freebsd"))]
-fn nearest_caret_offset(
- value: &str,
- position: gpui::Point,
- mut caret_bounds: impl FnMut(usize) -> Option>,
-) -> Option {
- value
- .char_indices()
- .map(|(offset, _)| offset)
- .chain(std::iter::once(value.len()))
- .filter_map(|offset| {
- let bounds = caret_bounds(offset)?;
- let vertical_distance = if position.y < bounds.top() {
- bounds.top() - position.y
- } else if position.y > bounds.bottom() {
- position.y - bounds.bottom()
- } else {
- px(0.)
- };
- Some((
- offset,
- vertical_distance,
- (position.x - bounds.left()).abs(),
- ))
- })
- .min_by(|left, right| left.1.cmp(&right.1).then_with(|| left.2.cmp(&right.2)))
- .map(|(offset, _, _)| offset)
-}
-
-#[cfg(any(target_os = "linux", target_os = "freebsd"))]
-fn paste_from_primary(
- state: &Entity>,
- position: gpui::Point,
- window: &mut Window,
- cx: &mut App,
-) {
- if !state.read(cx).is_editable() {
- return;
- }
- // Read before changing focus or selection: those can change PRIMARY.
- let Some(text) = cx.read_from_primary().and_then(|item| item.text()) else {
- return;
- };
- state.update(cx, |state, cx| {
- let Some(offset) = nearest_caret_offset(&state.value(), position, |offset| {
- state.range_to_bounds(&(offset..offset))
- }) else {
- return;
- };
- let text = if state.is_multi_line() {
- text
- } else {
- text.replace(['\r', '\n'], "")
- };
- state.focus(window, cx);
- state.set_selected_range(offset..offset, cx);
- state.insert(text, window, cx);
- });
- cx.stop_propagation();
-}
-
-/// The background of an input frame, which reads muted while the input is disabled.
-fn input_background(disabled: bool, cx: &App) -> Hsla {
- if disabled {
- cx.theme().surface
- } else {
- cx.theme().input_bg
- }
-}
-
-/// The colors base paints input text with, read from the coop theme.
-fn input_editor_style(cx: &App) -> InputEditorStyle {
- let theme = cx.theme();
- InputEditorStyle {
- foreground: theme.text,
- muted_foreground: theme.text_faint,
- background: theme.input_bg,
- border: theme.border_faint,
- selection: theme.selection,
- caret: theme.caret,
- ..InputEditorStyle::default()
- }
-}
-
-/// The input's own padding, resolved to pixels.
-fn input_paddings(size: Size, style: &StyleRefinement, window: &Window) -> Edges {
- let mut probe = div().input_px(size).input_py(size).refine_style(style);
- let padding = probe.style().padding.clone();
- let base_size = window.text_style().font_size;
- let rem_size = window.rem_size();
- let resolve = |value: Option| {
- value
- .map(|value| value.to_pixels(base_size, rem_size))
- .unwrap_or(px(0.))
- };
-
- Edges {
- left: resolve(padding.left),
- right: resolve(padding.right),
- top: resolve(padding.top),
- bottom: resolve(padding.bottom),
- }
-}
-
-/// A text input element bound to an [`InputState`] or a [`TextareaState`].
-#[derive(IntoElement)]
-pub struct Input {
- state: Entity>,
- style: StyleRefinement,
- size: Size,
- appearance: bool,
- cleanable: bool,
- disabled: bool,
-}
-
-/// A styled multi-line text input.
-pub type Textarea = Input;
-
-impl Sizable for Input {
- fn with_size(mut self, size: impl Into) -> Self {
- self.size = size.into();
- self
- }
-}
-
-impl Input {
- /// Create a new [`Input`] element bind to the given state.
- pub fn new(state: &Entity>) -> Self {
- Self {
- state: state.clone(),
- size: Size::default(),
- style: StyleRefinement::default(),
- appearance: true,
- cleanable: false,
- disabled: false,
- }
- }
-
- /// Set the appearance of the input field, if false the input field will no border, background.
- pub fn appearance(mut self, appearance: bool) -> Self {
- self.appearance = appearance;
- self
- }
-
- /// Set whether to show the clear button when the input field is not empty, default is false.
- pub fn cleanable(mut self, cleanable: bool) -> Self {
- self.cleanable = cleanable;
- self
- }
-
- /// Set to disable the input field.
- pub fn disabled(mut self, disabled: bool) -> Self {
- self.disabled = disabled;
- self
- }
-}
-
-impl Styled for Input {
- fn style(&mut self) -> &mut StyleRefinement {
- &mut self.style
- }
-}
-
-impl RenderOnce for Input {
- fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
- const LINE_HEIGHT: Rems = Rems(1.25);
- let text_align = self.style.text.text_align.unwrap_or(TextAlign::Left);
-
- let multi_line = self.state.read(cx).is_multi_line();
- let editor_paddings = if multi_line {
- input_paddings(self.size, &self.style, window)
- } else {
- Edges::default()
- };
- self.state.update(cx, |state, cx| {
- state.set_editor_style(input_editor_style(cx));
- state.set_editor_paddings(editor_paddings);
- state.set_disabled(self.disabled, cx);
- if state.is_single_line() {
- state.set_text_align(text_align, cx);
- }
- });
-
- let state = self.state.read(cx);
- let presentation = state.presentation();
- let disabled = presentation.is_disabled();
- let loading = presentation.is_loading();
- let text_is_empty = state.text().len() == 0;
-
- let gap_x = match self.size {
- Size::Small => px(4.),
- Size::Large => px(8.),
- _ => px(6.),
- };
-
- let background = input_background(disabled, cx);
- let show_clear_button =
- self.cleanable && state.is_editable() && !loading && !text_is_empty && !multi_line;
- let has_suffix = loading || show_clear_button;
-
- let state_entity = self.state.clone();
-
- let input = InputBase::new(("input", self.state.entity_id()))
- .flex()
- .size_full()
- .line_height(LINE_HEIGHT)
- .when(!multi_line, |this| {
- this.input_px(self.size).input_py(self.size)
- })
- .input_h(self.size)
- .when(!disabled, |this| this.cursor_text())
- .on_mouse_down(MouseButton::Left, {
- let state_entity = state_entity.clone();
- move |_, window, cx| state_entity.update(cx, |state, cx| state.focus(window, cx))
- });
-
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- let input = input.on_mouse_down(MouseButton::Middle, {
- let state_entity = state_entity.clone();
- move |event, window, cx| paste_from_primary(&state_entity, event.position, window, cx)
- });
-
- input
- .items_center()
- .when(multi_line, |this| this.h_auto())
- .when(self.appearance, |this| {
- this.bg(background)
- .when(self.disabled, |this| this.opacity(0.5))
- .rounded(px(button_radius()))
- })
- .tab_index(0)
- .gap(gap_x)
- .refine_style(&self.style)
- .when(!multi_line, |this| this.child(state_entity.clone()))
- .when(multi_line, |this| {
- this.child(
- v_flex()
- .size_full()
- .child(div().relative().flex_1().child(state_entity.clone())),
- )
- })
- .when(has_suffix, |this| {
- this.pr_2().child(
- h_flex()
- .id("suffix")
- .gap(gap_x)
- .items_center()
- .when(loading, |this| this.child(Indicator::new()))
- .when(show_clear_button, |this| {
- this.child(clear_button(cx).on_click({
- let state = state_entity.clone();
- move |_, window, cx| {
- state.update(cx, |state, cx| {
- state.clean(window, cx);
- state.focus(window, cx);
- })
- }
- }))
- }),
- )
- })
- }
-}
diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs
deleted file mode 100644
index 077215f6..00000000
--- a/crates/ui/src/input/mod.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-mod clear_button;
-#[allow(clippy::module_inception)]
-mod input;
-
-pub(crate) use clear_button::*;
-pub use gpui_base::input::{InputEvent, InputState, TextareaState};
-pub use input::*;
diff --git a/crates/ui/src/kbd.rs b/crates/ui/src/kbd.rs
deleted file mode 100644
index 80b04f77..00000000
--- a/crates/ui/src/kbd.rs
+++ /dev/null
@@ -1,270 +0,0 @@
-use gpui::{
- Action, AsKeystroke, IntoElement, KeyContext, Keystroke, ParentElement as _, RenderOnce,
- StyleRefinement, Styled, Window, div,
-};
-use theme::{ActiveTheme, TextStyle, Typeset as _};
-
-use crate::StyledExt;
-
-/// A key binding tag
-#[derive(IntoElement, Clone, Debug)]
-pub(crate) struct Kbd {
- style: StyleRefinement,
- stroke: Keystroke,
-}
-
-impl Kbd {
- fn new(stroke: Keystroke) -> Self {
- Self {
- style: StyleRefinement::default(),
- stroke,
- }
- }
-
- /// Return the first keybinding for the given action and context.
- pub(crate) fn binding_for_action(
- action: &dyn Action,
- context: Option<&str>,
- window: &Window,
- ) -> Option {
- let key_context = context.and_then(|context| KeyContext::parse(context).ok());
- let binding = match key_context {
- Some(context) => {
- window.highest_precedence_binding_for_action_in_context(action, context)
- }
- None => window.highest_precedence_binding_for_action(action),
- }?;
-
- binding
- .keystrokes()
- .first()
- .map(|key| Self::new(key.as_keystroke().clone()))
- }
-
- /// Return the platform specific keybinding string for a keystroke.
- fn format(key: &Keystroke) -> String {
- #[cfg(target_os = "macos")]
- const DIVIDER: &str = "";
- #[cfg(not(target_os = "macos"))]
- const DIVIDER: &str = "+";
-
- let mut parts = vec![];
-
- if key.modifiers.control {
- #[cfg(target_os = "macos")]
- parts.push("โ");
-
- #[cfg(not(target_os = "macos"))]
- parts.push("Ctrl");
- }
-
- if key.modifiers.alt {
- #[cfg(target_os = "macos")]
- parts.push("โฅ");
-
- #[cfg(not(target_os = "macos"))]
- parts.push("Alt");
- }
-
- if key.modifiers.shift {
- #[cfg(target_os = "macos")]
- parts.push("โง");
-
- #[cfg(not(target_os = "macos"))]
- parts.push("Shift");
- }
-
- if key.modifiers.platform {
- #[cfg(target_os = "macos")]
- parts.push("โ");
-
- #[cfg(not(target_os = "macos"))]
- parts.push("Win");
- }
-
- let mut keys = String::new();
- let key_str = key.key.as_str();
-
- match key_str {
- #[cfg(target_os = "macos")]
- "ctrl" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "ctrl" => keys.push_str("Ctrl"),
- #[cfg(target_os = "macos")]
- "alt" => keys.push('โฅ'),
- #[cfg(not(target_os = "macos"))]
- "alt" => keys.push_str("Alt"),
- #[cfg(target_os = "macos")]
- "shift" => keys.push('โง'),
- #[cfg(not(target_os = "macos"))]
- "shift" => keys.push_str("Shift"),
- #[cfg(target_os = "macos")]
- "cmd" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "cmd" => keys.push_str("Win"),
- #[cfg(target_os = "macos")]
- "space" => keys.push_str("Space"),
- #[cfg(target_os = "macos")]
- "backspace" => keys.push('โซ'),
- #[cfg(not(target_os = "macos"))]
- "backspace" => keys.push_str("Backspace"),
- #[cfg(target_os = "macos")]
- "delete" => keys.push('โซ'),
- #[cfg(not(target_os = "macos"))]
- "delete" => keys.push_str("Delete"),
- #[cfg(target_os = "macos")]
- "escape" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "escape" => keys.push_str("Esc"),
- #[cfg(target_os = "macos")]
- "enter" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "enter" => keys.push_str("Enter"),
- "pagedown" => keys.push_str("Page Down"),
- "pageup" => keys.push_str("Page Up"),
- #[cfg(target_os = "macos")]
- "left" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "left" => keys.push_str("Left"),
- #[cfg(target_os = "macos")]
- "right" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "right" => keys.push_str("Right"),
- #[cfg(target_os = "macos")]
- "up" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "up" => keys.push_str("Up"),
- #[cfg(target_os = "macos")]
- "down" => keys.push('โ'),
- #[cfg(not(target_os = "macos"))]
- "down" => keys.push_str("Down"),
- _ => {
- if key_str.len() == 1 {
- keys.push_str(&key_str.to_uppercase());
- } else {
- let mut chars = key_str.chars();
- if let Some(first_char) = chars.next() {
- keys.push_str(&format!(
- "{}{}",
- first_char.to_uppercase(),
- chars.collect::()
- ));
- } else {
- keys.push_str(key_str);
- }
- }
- }
- }
-
- parts.push(&keys);
- parts.join(DIVIDER)
- }
-}
-
-impl Styled for Kbd {
- fn style(&mut self) -> &mut StyleRefinement {
- &mut self.style
- }
-}
-
-impl RenderOnce for Kbd {
- fn render(self, _: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
- div()
- .border_1()
- .border_color(cx.theme().border_faint)
- .text_color(cx.theme().text_muted)
- .bg(cx.theme().surface_raised)
- .py_0p5()
- .px_1()
- .min_w_5()
- .text_center()
- .rounded_sm()
- .text_style(TextStyle::Caption)
- .whitespace_normal()
- .flex_shrink_0()
- .refine_style(&self.style)
- .child(Self::format(&self.stroke))
- .into_any_element()
- }
-}
-
-#[cfg(test)]
-mod tests {
- #[test]
- fn test_format() {
- use gpui::Keystroke;
-
- use super::Kbd;
-
- if cfg!(target_os = "macos") {
- assert_eq!(Kbd::format(&Keystroke::parse("cmd-a").unwrap()), "โA");
- assert_eq!(Kbd::format(&Keystroke::parse("cmd--").unwrap()), "โ-");
- assert_eq!(Kbd::format(&Keystroke::parse("cmd-+").unwrap()), "โ+");
- assert_eq!(Kbd::format(&Keystroke::parse("cmd-enter").unwrap()), "โโ");
- assert_eq!(
- Kbd::format(&Keystroke::parse("secondary-f12").unwrap()),
- "โF12"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("shift-pagedown").unwrap()),
- "โงPage Down"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("shift-pageup").unwrap()),
- "โงPage Up"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("shift-space").unwrap()),
- "โงSpace"
- );
- assert_eq!(Kbd::format(&Keystroke::parse("cmd-ctrl-a").unwrap()), "โโA");
- assert_eq!(
- Kbd::format(&Keystroke::parse("cmd-alt-backspace").unwrap()),
- "โฅโโซ"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("shift-delete").unwrap()),
- "โงโซ"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("cmd-ctrl-shift-a").unwrap()),
- "โโงโA"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("cmd-ctrl-shift-alt-a").unwrap()),
- "โโฅโงโA"
- );
- } else {
- assert_eq!(Kbd::format(&Keystroke::parse("a").unwrap()), "A");
- assert_eq!(Kbd::format(&Keystroke::parse("ctrl-a").unwrap()), "Ctrl+A");
- assert_eq!(
- Kbd::format(&Keystroke::parse("shift-space").unwrap()),
- "Shift+Space"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("ctrl-alt-a").unwrap()),
- "Ctrl+Alt+A"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("ctrl-alt-shift-a").unwrap()),
- "Ctrl+Alt+Shift+A"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("ctrl-alt-shift-win-a").unwrap()),
- "Ctrl+Alt+Shift+Win+A"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("ctrl-shift-backspace").unwrap()),
- "Ctrl+Shift+Backspace"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("alt-delete").unwrap()),
- "Alt+Delete"
- );
- assert_eq!(
- Kbd::format(&Keystroke::parse("alt-tab").unwrap()),
- "Alt+Tab"
- );
- }
- }
-}
diff --git a/crates/theme/src/layout.rs b/crates/ui/src/layout.rs
similarity index 74%
rename from crates/theme/src/layout.rs
rename to crates/ui/src/layout.rs
index a2633922..17ee3931 100644
--- a/crates/theme/src/layout.rs
+++ b/crates/ui/src/layout.rs
@@ -1,17 +1,13 @@
-//! Layout constants, ported from bezel
-//! (`crates/theme/src/theme/layout.rs`, MIT License, Copyright (c) 2026 bezel
-//! contributors). Numbers drive layout, colors are paint: these live as plain
-//! numbers and never depend on which color is painted.
+//! Layout constants, ported from bezel. Numbers drive layout, colors are
+//! paint: these live as plain numbers and never depend on which color is
+//! painted.
use std::sync::atomic::{AtomicU32, Ordering};
-/// The branded base radius behind every radius accessor, as raw `f32` bits.
-static BASE: AtomicU32 = AtomicU32::new(BASE_RADIUS.to_bits());
+use gpui::{Pixels, px};
-/// Point the radius accessors at a base. Called when a palette is installed.
-pub(crate) fn set_base_radius(radius: f32) {
- BASE.store(radius.to_bits(), Ordering::Relaxed);
-}
+/// The base radius behind every radius accessor, as raw `f32` bits.
+static BASE: AtomicU32 = AtomicU32::new(BASE_RADIUS.to_bits());
/// The gap between siblings. Measured on macOS 26: `NSStackView().spacing`,
/// visual format's `-`, and `constraint(equalToSystemSpacingAfter:multiplier: 1)`
@@ -85,3 +81,21 @@ fn radius(ratio: f32) -> f32 {
pub const fn inset_radius(outer: f32, inset: f32) -> f32 {
if outer > inset { outer - inset } else { 0.0 }
}
+
+/// Window border radius for platforms with client-side decorations.
+pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
+
+/// Window shadow size for platforms with client-side decorations.
+pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
+
+/// Window border size for platforms with client-side decorations.
+pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
+
+/// Space left of the sidebar header for the macOS traffic lights.
+pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
+
+/// Workspace tabbar height.
+pub const TABBAR_HEIGHT: Pixels = px(36.0);
+
+/// Default sidebar width.
+pub const SIDEBAR_WIDTH: Pixels = px(240.);
diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs
index 026a5675..2804bba2 100644
--- a/crates/ui/src/lib.rs
+++ b/crates/ui/src/lib.rs
@@ -1,50 +1,15 @@
-pub(crate) use gpui_base::{ElementExt, InteractiveElementExt};
-pub use icon::*;
-pub use root::Root;
-pub use styled::*;
-pub use title_bar::{TRAFFIC_LIGHT_PADDING, title_bar_drag_handlers};
-pub use window_ext::*;
+pub use layout::{
+ BASE_RADIUS, BUTTON_HEIGHT, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
+ CLIENT_SIDE_DECORATION_SHADOW, CONTENT_MARGIN, CONTROL_HEIGHT_SMALL, EDGE, SIDEBAR_WIDTH,
+ SPACE, TABBAR_HEIGHT, TRAFFIC_LIGHT_PADDING, bubble_radius, button_radius, control_radius,
+ inset_radius, panel_radius, surface_radius,
+};
+pub use styled::{background, divider};
+pub use typography::{DEFAULT_BASE_TEXT_SIZE, Metrics, TextStyle, Typeset, base_text_size};
+pub use window_drag::title_bar_drag_handlers;
-pub mod avatar;
-pub mod button;
-pub mod dialog;
-pub mod dock;
-pub mod group_box;
-pub mod indicator;
-pub mod input;
-pub mod markdown;
-pub mod menu;
-pub mod message;
-pub mod nav;
-pub mod nav_item;
-pub mod notification;
-pub mod scroll;
-pub mod switch;
-pub mod tab;
-pub mod tooltip;
-
-mod icon;
-mod kbd;
+pub mod layout;
pub mod motion;
-mod popover;
-mod resizable;
-mod root;
-mod styled;
-mod title_bar;
-mod window_ext;
-
-/// Initialize the UI module.
-///
-/// This must be called before using any of the UI components.
-/// You can initialize the UI module at your application's entry point.
-pub fn init(cx: &mut gpui::App) {
- gpui_base::init(cx);
- #[cfg(not(target_os = "macos"))]
- cx.bind_keys([gpui::KeyBinding::new(
- "shift-insert",
- gpui_base::input::Paste,
- Some("Input"),
- )]);
- theme::sync_base(cx);
- menu::init(cx);
-}
+pub mod styled;
+pub mod typography;
+pub mod window_drag;
diff --git a/crates/ui/src/markdown.rs b/crates/ui/src/markdown.rs
deleted file mode 100644
index 7de5f93d..00000000
--- a/crates/ui/src/markdown.rs
+++ /dev/null
@@ -1,146 +0,0 @@
-use std::ops::Range;
-
-use gpui::{
- AnyElement, App, ElementId, HighlightStyle, IntoElement, SharedString, SharedUri,
- StyleRefinement, Styled as _, Window,
-};
-use gpui_base::{TextView, TextViewStyle};
-use theme::ActiveTheme as _;
-
-/// A span of the source content, by byte range, that renders as `text` instead.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct InlineReplacement {
- pub range: Range,
- pub text: SharedString,
- /// When set, the span renders as this image (NIP-30 custom emoji)
- pub image: Option,
-}
-
-impl InlineReplacement {
- pub fn new(range: Range, text: impl Into) -> Self {
- Self {
- range,
- text: text.into(),
- image: None,
- }
- }
-
- pub fn image(
- range: Range,
- alt: impl Into,
- url: impl Into,
- ) -> Self {
- Self {
- range,
- text: alt.into(),
- image: Some(url.into()),
- }
- }
-}
-
-/// Message content prepared for rendering as a GPUI Kit text view.
-#[derive(Default)]
-pub struct RenderedText {
- text: SharedString,
-}
-
-impl RenderedText {
- pub fn new(content: &str, replacements: &[InlineReplacement]) -> Self {
- let mut text = String::with_capacity(content.len());
- let mut last = 0;
-
- for replacement in replacements {
- let range = &replacement.range;
- if range.start < last
- || range.end > content.len()
- || range.start >= range.end
- || !content.is_char_boundary(range.start)
- || !content.is_char_boundary(range.end)
- {
- continue;
- }
-
- text.push_str(&content[last..range.start]);
- match &replacement.image {
- Some(url) => text.push_str(&format!(
- "",
- escape_link_text(&replacement.text)
- )),
- None => text.push_str(&format!(
- "[{}](<{}>)",
- escape_link_text(&replacement.text),
- &content[range.start..range.end]
- )),
- }
- last = range.end;
- }
- text.push_str(&content[last..]);
-
- Self {
- text: SharedString::from(text),
- }
- }
-
- /// Render the message as a GPUI Kit text view.
- pub fn element(&self, id: ElementId, _window: &Window, cx: &App) -> AnyElement {
- let theme = cx.theme();
- let code_block = StyleRefinement::default().font_family(theme.font_mono.clone());
- let muted_heading = theme.text_muted;
-
- TextView::markdown(id, self.text.clone())
- .style(
- TextViewStyle::default()
- .with_foreground(theme.text)
- .with_muted_foreground(theme.text_muted)
- // Deeply nested headings read as secondary text.
- .with_heading(move |level| {
- if level >= 4 {
- StyleRefinement::default().text_color(muted_heading)
- } else {
- StyleRefinement::default()
- }
- })
- .with_link(theme.accent)
- .with_code_background(theme.code_wash)
- .with_inline_code(HighlightStyle {
- color: Some(theme.code_text),
- ..Default::default()
- })
- .with_border(theme.border)
- .with_code_block(code_block)
- .with_dark(theme.is_dark()),
- )
- .on_link_click(|url, _, _, cx| {
- if is_web_url(url) {
- cx.open_url(url);
- } else if let Some(bech32) = nevent_bech32(url) {
- cx.open_url(&format!("https://njump.me/{bech32}"));
- }
- })
- .into_any_element()
- }
-}
-
-/// Escapes the characters that would terminate a markdown link label.
-fn escape_link_text(text: &str) -> String {
- let mut escaped = String::with_capacity(text.len());
- for character in text.chars() {
- if matches!(character, '[' | ']' | '\\') {
- escaped.push('\\');
- }
- escaped.push(character);
- }
- escaped
-}
-
-/// Matches `http://` and `https://` URLs. Only these are treated as clickable links.
-fn is_web_url(url: &str) -> bool {
- let url = url.to_ascii_lowercase();
- url.starts_with("http://") || url.starts_with("https://")
-}
-
-/// Extracts the bech32 from a NIP-19 event reference (`nevent1...` or `nostr:nevent1...`).
-fn nevent_bech32(url: &str) -> Option<&str> {
- let bech32 = url.strip_prefix("nostr:").unwrap_or(url);
- bech32.starts_with("nevent1").then_some(bech32)
-}
diff --git a/crates/ui/src/menu/dropdown_menu.rs b/crates/ui/src/menu/dropdown_menu.rs
deleted file mode 100644
index c3a2f05e..00000000
--- a/crates/ui/src/menu/dropdown_menu.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-use std::rc::Rc;
-
-use gpui::{
- Anchor, AnyElement, Context, DismissEvent, ElementId, Entity, Focusable, InteractiveElement,
- IntoElement, MouseButton, RenderOnce, SharedString, StyleRefinement, Styled, Window,
-};
-
-use crate::Selectable;
-use crate::avatar::Avatar;
-use crate::button::Button;
-use crate::menu::PopupMenu;
-use crate::nav::Nav;
-use crate::nav_item::NavItem;
-use crate::popover::{Popover, PopoverState};
-
-/// Builds the items of a popup menu on each render.
-type MenuBuilder = dyn Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu;
-
-/// A dropdown menu trait for buttons and other interactive elements
-pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
- /// Create a dropdown menu with the given items, anchored to the TopRight corner
- fn dropdown_menu(
- self,
- f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static,
- ) -> DropdownMenuPopover {
- self.dropdown_menu_with_anchor(Anchor::TopRight, f)
- }
-
- /// Create a dropdown menu with the given items, anchored to the given corner
- fn dropdown_menu_with_anchor(
- mut self,
- anchor: impl Into,
- f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static,
- ) -> DropdownMenuPopover {
- let style = self.style().clone();
- let id = self.interactivity().element_id.clone();
-
- DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
- }
-}
-
-impl DropdownMenu for Button {}
-
-impl DropdownMenu for Avatar {}
-
-impl DropdownMenu for Nav {}
-
-impl DropdownMenu for NavItem {}
-
-#[derive(IntoElement)]
-pub struct DropdownMenuPopover {
- id: ElementId,
- style: StyleRefinement,
- anchor: Anchor,
- trigger: T,
- builder: Rc,
-}
-
-impl DropdownMenuPopover
-where
- T: Selectable + IntoElement + 'static,
-{
- fn new(
- id: ElementId,
- anchor: impl Into,
- trigger: T,
- builder: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static,
- ) -> Self {
- Self {
- id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
- style: StyleRefinement::default(),
- anchor: anchor.into(),
- trigger,
- builder: Rc::new(builder),
- }
- }
-
- /// Set the style refinement for the dropdown menu trigger.
- fn trigger_style(mut self, style: StyleRefinement) -> Self {
- self.style = style;
- self
- }
-}
-
-/// Opens a [`PopupMenu`] when its child is right-clicked, keeping the child's own click handler.
-#[derive(IntoElement)]
-pub struct ContextMenu {
- id: ElementId,
- child: AnyElement,
- builder: Rc,
-}
-
-impl ContextMenu {
- pub fn new(
- id: impl Into,
- child: impl IntoElement,
- builder: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static,
- ) -> Self {
- Self {
- id: id.into(),
- child: child.into_any_element(),
- builder: Rc::new(builder),
- }
- }
-}
-
-#[derive(Default)]
-struct MenuState {
- menu: Option>,
-}
-
-/// Builds the menu once and reuses it until dismissed, preserving focus and selection state.
-fn cached_menu(
- menu_state: &Entity,
- builder: Rc,
- window: &mut Window,
- cx: &mut Context,
-) -> Entity {
- if let Some(menu) = menu_state.read(cx).menu.clone() {
- return menu;
- }
-
- let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
- builder(menu, window, cx)
- });
- menu_state.update(cx, |state, _| {
- state.menu = Some(menu.clone());
- });
- menu.focus_handle(cx).focus(window, cx);
-
- let popover_state = cx.entity();
- window
- .subscribe(&menu, cx, {
- let menu_state = menu_state.clone();
- move |_, _: &DismissEvent, window, cx| {
- popover_state.update(cx, |state, cx| state.dismiss(window, cx));
- menu_state.update(cx, |state, _| {
- state.menu = None;
- });
- }
- })
- .detach();
-
- menu
-}
-
-impl RenderOnce for DropdownMenuPopover
-where
- T: Selectable + IntoElement + 'static,
-{
- fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
- let builder = self.builder.clone();
- let menu_state = window.use_keyed_state(self.id.clone(), cx, |_, _| MenuState::default());
-
- Popover::new(SharedString::from(format!("popover:{}", self.id)))
- .appearance(false)
- .overlay_closable(false)
- .trigger(self.trigger)
- .trigger_style(self.style)
- .anchor(self.anchor)
- .content(move |_, window, cx| cached_menu(&menu_state, builder.clone(), window, cx))
- }
-}
-
-impl RenderOnce for ContextMenu {
- fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
- let builder = self.builder.clone();
- let menu_state = window.use_keyed_state(self.id.clone(), cx, |_, _| MenuState::default());
-
- Popover::new(SharedString::from(format!("context-menu:{}", self.id)))
- .appearance(false)
- .overlay_closable(false)
- .anchor(Anchor::TopLeft)
- .mouse_button(MouseButton::Right)
- .trigger_with(move |_open, _window, _cx| self.child)
- .content(move |_, window, cx| cached_menu(&menu_state, builder.clone(), window, cx))
- }
-}
diff --git a/crates/ui/src/menu/menu_item.rs b/crates/ui/src/menu/menu_item.rs
deleted file mode 100644
index 3b15dbfe..00000000
--- a/crates/ui/src/menu/menu_item.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use gpui::prelude::FluentBuilder as _;
-use gpui::{
- AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, MouseButton,
- ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
- Styled, Window,
-};
-use smallvec::SmallVec;
-use theme::{ActiveTheme, TextStyle, Typeset as _};
-
-use crate::{Disableable, StyledExt, h_flex};
-
-#[derive(IntoElement)]
-pub(crate) struct MenuItemElement {
- id: ElementId,
- group_name: SharedString,
- style: StyleRefinement,
- disabled: bool,
- selected: bool,
- #[allow(clippy::type_complexity)]
- on_click: Option>,
- #[allow(clippy::type_complexity)]
- on_hover: Option>,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl MenuItemElement {
- /// Create a new MenuItem with the given ID and group name.
- pub(crate) fn new(id: impl Into, group_name: impl Into) -> Self {
- let id: ElementId = id.into();
- Self {
- id: id.clone(),
- group_name: group_name.into(),
- style: StyleRefinement::default(),
- disabled: false,
- selected: false,
- on_click: None,
- on_hover: None,
- children: SmallVec::new(),
- }
- }
-
- /// Set the selected style of the item.
- pub(crate) fn selected(mut self, selected: bool) -> Self {
- self.selected = selected;
- self
- }
-
- /// Set a handler for when the MenuItem is clicked.
- pub(crate) fn on_click(
- mut self,
- handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
- ) -> Self {
- self.on_click = Some(Box::new(handler));
- self
- }
-
- /// Set a handler for when the mouse enters the MenuItem.
- pub(crate) fn on_hover(
- mut self,
- handler: impl Fn(&bool, &mut Window, &mut App) + 'static,
- ) -> Self {
- self.on_hover = Some(Box::new(handler));
- self
- }
-}
-
-impl Disableable for MenuItemElement {
- fn disabled(mut self, disabled: bool) -> Self {
- self.disabled = disabled;
- self
- }
-}
-
-impl Styled for MenuItemElement {
- fn style(&mut self) -> &mut gpui::StyleRefinement {
- &mut self.style
- }
-}
-
-impl ParentElement for MenuItemElement {
- fn extend(&mut self, elements: impl IntoIterator
- ) {
- self.children.extend(elements);
- }
-}
-
-impl RenderOnce for MenuItemElement {
- fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
- h_flex()
- .id(self.id)
- .group(&self.group_name)
- .gap_x_1()
- .p_1()
- .text_style(TextStyle::Callout)
- .text_color(cx.theme().text)
- .relative()
- .items_center()
- .justify_between()
- .refine_style(&self.style)
- .when_some(self.on_hover, |this, on_hover| {
- this.on_hover(move |hovered, window, cx| (on_hover)(hovered, window, cx))
- })
- .when(!self.disabled, |this| {
- this.group_hover(self.group_name, |this| {
- this.bg(cx.theme().element_hover)
- .text_color(cx.theme().text)
- })
- .when(self.selected, |this| {
- this.bg(cx.theme().element_active)
- .text_color(cx.theme().text)
- })
- .when_some(self.on_click, |this, on_click| {
- this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
- cx.stop_propagation();
- })
- .on_click(on_click)
- })
- })
- .when(self.disabled, |this| this.text_color(cx.theme().text_faint))
- .children(self.children)
- }
-}
diff --git a/crates/ui/src/menu/mod.rs b/crates/ui/src/menu/mod.rs
deleted file mode 100644
index b65aecab..00000000
--- a/crates/ui/src/menu/mod.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use gpui::App;
-
-mod dropdown_menu;
-mod menu_item;
-mod popup_menu;
-
-pub use dropdown_menu::{ContextMenu, DropdownMenu};
-pub use popup_menu::{PopupMenu, PopupMenuItem};
-
-pub(crate) fn init(cx: &mut App) {
- popup_menu::init(cx);
-}
diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs
deleted file mode 100644
index a8d0ea13..00000000
--- a/crates/ui/src/menu/popup_menu.rs
+++ /dev/null
@@ -1,839 +0,0 @@
-use std::rc::Rc;
-
-use gpui::prelude::FluentBuilder;
-use gpui::{
- Action, Anchor, App, AppContext, Axis, Bounds, ClickEvent, Context, DismissEvent, Edges,
- Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
- MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Styled, WeakEntity, Window,
- anchored, div, px, rems,
-};
-use gpui_base::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
-use theme::{ActiveTheme, Side, TextStyle, Typeset as _};
-
-use crate::kbd::Kbd;
-use crate::menu::menu_item::MenuItemElement;
-use crate::{Disableable, ElementExt, Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
-
-const CONTEXT: &str = "PopupMenu";
-const MAX_WIDTH: Pixels = px(500.);
-
-pub(crate) fn init(cx: &mut App) {
- cx.bind_keys([
- KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
- KeyBinding::new("escape", Cancel, Some(CONTEXT)),
- KeyBinding::new("up", SelectUp, Some(CONTEXT)),
- KeyBinding::new("down", SelectDown, Some(CONTEXT)),
- KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
- KeyBinding::new("right", SelectRight, Some(CONTEXT)),
- ]);
-}
-
-/// A menu item in a popup menu.
-pub enum PopupMenuItem {
- /// A menu separator item.
- Separator,
- /// A non-interactive label item.
- Label(SharedString),
- /// A standard menu item.
- Item {
- icon: Option,
- label: SharedString,
- disabled: bool,
- checked: bool,
- action: Option>,
- #[allow(clippy::type_complexity)]
- handler: Option>,
- },
- /// A submenu item that opens another popup menu.
- Submenu {
- icon: Option