(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/form.rs b/crates/ui/src/form.rs
new file mode 100644
index 00000000..89ac383a
--- /dev/null
+++ b/crates/ui/src/form.rs
@@ -0,0 +1,28 @@
+use gpui::{App, Div, IntoElement, ParentElement, SharedString, Styled, div};
+use gpui_kit::component::{ActiveTheme, h_flex, v_flex};
+
+/// A settings row: the label and description on the left, the control on the right.
+pub fn settings_row(
+ label: impl Into,
+ description: impl Into,
+ control: impl IntoElement,
+ cx: &App,
+) -> Div {
+ h_flex()
+ .gap_3()
+ .items_center()
+ .justify_between()
+ .child(
+ v_flex()
+ .flex_1()
+ .min_w_0()
+ .child(div().text_sm().child(label.into()))
+ .child(
+ div()
+ .text_xs()
+ .text_color(cx.theme().muted_foreground)
+ .child(description.into()),
+ ),
+ )
+ .child(control)
+}
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
index 90fa8029..20b9effb 100644
--- a/crates/ui/src/icon.rs
+++ b/crates/ui/src/icon.rs
@@ -1,274 +1,67 @@
-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())
- }
-}
+use gpui::SharedString;
+use gpui_kit::assets::IconNamed;
+
+/// Coop icons kept from before the gpui-kit migration; absent from the kit bundle.
+#[derive(Clone, Copy)]
+pub enum IconName {
+ ArrowDownCircle,
+ ArrowLeft,
+ Book,
+ CaretDown,
+ CaretRight,
+ CheckCircle,
+ Close,
+ Compass,
+ Door,
+ Emoji,
+ Group,
+ Input,
+ Invite,
+ Link,
+ Lock,
+ Message,
+ PaperPlaneFill,
+ Plus,
+ PlusChat,
+ Profile,
+ Relay,
+ Search,
+ Settings,
+ Settings2,
+ Upload,
+ Warning,
+}
+
+impl IconNamed for IconName {
+ fn path(self) -> SharedString {
+ match self {
+ Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
+ Self::ArrowLeft => "icons/arrow-left.svg",
+ Self::Book => "icons/book.svg",
+ Self::CaretDown => "icons/caret-down.svg",
+ Self::CaretRight => "icons/caret-right.svg",
+ Self::CheckCircle => "icons/check-circle.svg",
+ Self::Close => "icons/close.svg",
+ Self::Compass => "icons/compass.svg",
+ Self::Door => "icons/door.svg",
+ Self::Emoji => "icons/emoji.svg",
+ Self::Group => "icons/group.svg",
+ Self::Input => "icons/input.svg",
+ Self::Invite => "icons/invite.svg",
+ Self::Link => "icons/link.svg",
+ Self::Lock => "icons/lock.svg",
+ Self::Message => "icons/message.svg",
+ Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
+ Self::Plus => "icons/plus.svg",
+ Self::PlusChat => "icons/plus-chat.svg",
+ Self::Profile => "icons/profile.svg",
+ Self::Relay => "icons/relay.svg",
+ Self::Search => "icons/search.svg",
+ Self::Settings => "icons/settings.svg",
+ Self::Settings2 => "icons/settings2.svg",
+ Self::Upload => "icons/upload.svg",
+ Self::Warning => "icons/warning.svg",
+ }
+ .into()
+ }
+}
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/ui/src/lib.rs b/crates/ui/src/lib.rs
index 026a5675..acec50ca 100644
--- a/crates/ui/src/lib.rs
+++ b/crates/ui/src/lib.rs
@@ -1,50 +1,13 @@
-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 avatar::Avatar;
+pub use form::*;
+pub use icon::IconName;
+pub use message::*;
+pub use text::*;
+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 form;
+pub mod icon;
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 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 text;
+pub mod window_drag;
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