feat(desktop): add foundational GPUI primitives
Co-authored-by: Maze <mazeincoding@users.noreply.github.com>
This commit is contained in:
parent
4d8c49ed07
commit
400f097bec
|
|
@ -0,0 +1,69 @@
|
|||
use gpui::{
|
||||
App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px,
|
||||
transparent_black,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum BadgeVariant {
|
||||
#[default]
|
||||
Default,
|
||||
Secondary,
|
||||
Outline,
|
||||
Destructive,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct Badge {
|
||||
label: SharedString,
|
||||
variant: BadgeVariant,
|
||||
}
|
||||
|
||||
impl Badge {
|
||||
pub(crate) fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
variant: BadgeVariant::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn variant(mut self, variant: BadgeVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Badge {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let colors = window.theme().colors;
|
||||
let transparent = transparent_black();
|
||||
let (background, foreground, border) = match self.variant {
|
||||
BadgeVariant::Default => (colors.primary, colors.primary_foreground, transparent),
|
||||
BadgeVariant::Secondary => (colors.secondary, colors.secondary_foreground, transparent),
|
||||
BadgeVariant::Outline => (transparent, colors.foreground, colors.border),
|
||||
BadgeVariant::Destructive => (
|
||||
colors.destructive.opacity(0.1),
|
||||
colors.destructive,
|
||||
transparent,
|
||||
),
|
||||
};
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.h(px(20.0))
|
||||
.px(px(6.0))
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.bg(background)
|
||||
.text_color(foreground)
|
||||
.text_size(px(10.0))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.whitespace_nowrap()
|
||||
.child(self.label)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
use gpui::{
|
||||
App, ClickEvent, ElementId, FontWeight, IntoElement, MouseButton, RenderOnce, SharedString,
|
||||
Window, div, prelude::*, px, transparent_black,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum ButtonVariant {
|
||||
#[default]
|
||||
Default,
|
||||
Outline,
|
||||
Secondary,
|
||||
Ghost,
|
||||
Destructive,
|
||||
Link,
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum ButtonSize {
|
||||
#[default]
|
||||
Default,
|
||||
XSmall,
|
||||
Small,
|
||||
Large,
|
||||
Icon,
|
||||
IconXSmall,
|
||||
IconSmall,
|
||||
IconLarge,
|
||||
}
|
||||
|
||||
impl ButtonSize {
|
||||
fn height(self) -> f32 {
|
||||
match self {
|
||||
Self::XSmall | Self::IconXSmall => 20.0,
|
||||
Self::Small | Self::IconSmall => 24.0,
|
||||
Self::Default | Self::Icon => 28.0,
|
||||
Self::Large | Self::IconLarge => 32.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_padding(self) -> f32 {
|
||||
match self {
|
||||
Self::XSmall | Self::Small | Self::Default => 8.0,
|
||||
Self::Large => 10.0,
|
||||
Self::Icon | Self::IconXSmall | Self::IconSmall | Self::IconLarge => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_icon(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Icon | Self::IconXSmall | Self::IconSmall | Self::IconLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct Button {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
variant: ButtonVariant,
|
||||
size: ButtonSize,
|
||||
disabled: bool,
|
||||
full_width: bool,
|
||||
on_click: Option<ClickHandler>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub(crate) fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
variant: ButtonVariant::default(),
|
||||
size: ButtonSize::default(),
|
||||
disabled: false,
|
||||
full_width: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn size(mut self, size: ButtonSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn full_width(mut self) -> Self {
|
||||
self.full_width = true;
|
||||
self
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let theme = window.theme();
|
||||
let colors = theme.colors;
|
||||
let transparent = transparent_black();
|
||||
|
||||
let (background, foreground, border, hover, active) = match self.variant {
|
||||
ButtonVariant::Default => (
|
||||
colors.primary,
|
||||
colors.primary_foreground,
|
||||
transparent,
|
||||
colors.primary.opacity(0.8),
|
||||
colors.primary.opacity(0.7),
|
||||
),
|
||||
ButtonVariant::Outline => (
|
||||
transparent,
|
||||
colors.foreground,
|
||||
colors.border,
|
||||
colors.input.opacity(0.5),
|
||||
colors.input,
|
||||
),
|
||||
ButtonVariant::Secondary => (
|
||||
colors.secondary,
|
||||
colors.secondary_foreground,
|
||||
transparent,
|
||||
colors.secondary.opacity(0.8),
|
||||
colors.secondary.opacity(0.65),
|
||||
),
|
||||
ButtonVariant::Ghost => (
|
||||
transparent,
|
||||
colors.foreground,
|
||||
transparent,
|
||||
colors.muted,
|
||||
colors.muted.opacity(0.75),
|
||||
),
|
||||
ButtonVariant::Destructive => (
|
||||
colors.destructive.opacity(0.1),
|
||||
colors.destructive,
|
||||
transparent,
|
||||
colors.destructive.opacity(0.2),
|
||||
colors.destructive.opacity(0.3),
|
||||
),
|
||||
ButtonVariant::Link => (
|
||||
transparent,
|
||||
colors.primary,
|
||||
transparent,
|
||||
transparent,
|
||||
transparent,
|
||||
),
|
||||
};
|
||||
|
||||
let height = px(self.size.height());
|
||||
let mut button = div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h(height)
|
||||
.when(self.size.is_icon(), |this| this.w(height))
|
||||
.when(!self.size.is_icon(), |this| {
|
||||
this.px(px(self.size.horizontal_padding()))
|
||||
})
|
||||
.when(self.full_width, |this| this.w_full())
|
||||
.rounded(theme.radius)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.bg(background)
|
||||
.text_color(foreground)
|
||||
.text_xs()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.whitespace_nowrap()
|
||||
.tab_index(0)
|
||||
.focus(|style| style.border_color(colors.ring))
|
||||
.child(self.label);
|
||||
|
||||
if self.disabled {
|
||||
button = button.opacity(0.5).cursor_not_allowed();
|
||||
} else {
|
||||
button = button
|
||||
.cursor_pointer()
|
||||
.hover(move |style| {
|
||||
let style = style.bg(hover);
|
||||
if self.variant == ButtonVariant::Link {
|
||||
style.underline()
|
||||
} else {
|
||||
style
|
||||
}
|
||||
})
|
||||
.active(move |style| style.bg(active))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
handler(event, window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, Entity, IntoElement, MouseButton, Pixels, Point, Render,
|
||||
SharedString, Window, anchored, deferred, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
type SelectHandler = Rc<dyn Fn(&mut Window, &mut App) + 'static>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum ContextMenuItemVariant {
|
||||
#[default]
|
||||
Default,
|
||||
Destructive,
|
||||
}
|
||||
pub(crate) enum ContextMenuItem {
|
||||
Entry {
|
||||
label: SharedString,
|
||||
shortcut: Option<SharedString>,
|
||||
checked: Option<bool>,
|
||||
disabled: bool,
|
||||
variant: ContextMenuItemVariant,
|
||||
on_select: SelectHandler,
|
||||
},
|
||||
Label(SharedString),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub(crate) fn entry(label: impl Into<SharedString>) -> Self {
|
||||
Self::Entry {
|
||||
label: label.into(),
|
||||
shortcut: None,
|
||||
checked: None,
|
||||
disabled: false,
|
||||
variant: ContextMenuItemVariant::Default,
|
||||
on_select: Rc::new(|_, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn label(label: impl Into<SharedString>) -> Self {
|
||||
Self::Label(label.into())
|
||||
}
|
||||
|
||||
pub(crate) fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
pub(crate) fn shortcut(mut self, shortcut: impl Into<SharedString>) -> Self {
|
||||
if let Self::Entry {
|
||||
shortcut: item_shortcut,
|
||||
..
|
||||
} = &mut self
|
||||
{
|
||||
*item_shortcut = Some(shortcut.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn checked(mut self, checked: bool) -> Self {
|
||||
if let Self::Entry {
|
||||
checked: item_checked,
|
||||
..
|
||||
} = &mut self
|
||||
{
|
||||
*item_checked = Some(checked);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
if let Self::Entry {
|
||||
disabled: item_disabled,
|
||||
..
|
||||
} = &mut self
|
||||
{
|
||||
*item_disabled = disabled;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn variant(mut self, variant: ContextMenuItemVariant) -> Self {
|
||||
if let Self::Entry {
|
||||
variant: item_variant,
|
||||
..
|
||||
} = &mut self
|
||||
{
|
||||
*item_variant = variant;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn on_select(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
if let Self::Entry {
|
||||
on_select: item_handler,
|
||||
..
|
||||
} = &mut self
|
||||
{
|
||||
*item_handler = Rc::new(handler);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
position: Option<Point<Pixels>>,
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
pub(crate) fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
|
||||
Self {
|
||||
items: items.into_iter().collect(),
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn open(&mut self, position: Point<Pixels>, cx: &mut gpui::Context<Self>) {
|
||||
self.position = Some(position);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn close(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
self.position = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn activate(&mut self, index: usize, window: &mut Window, cx: &mut gpui::Context<Self>) {
|
||||
let Some(ContextMenuItem::Entry {
|
||||
disabled,
|
||||
on_select,
|
||||
..
|
||||
}) = self.items.get(index)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let on_select = Rc::clone(on_select);
|
||||
self.close(cx);
|
||||
on_select(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextMenu {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
let Some(position) = self.position else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let theme = window.theme();
|
||||
let colors = theme.colors;
|
||||
let items = self
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| -> AnyElement {
|
||||
match item {
|
||||
ContextMenuItem::Entry {
|
||||
label,
|
||||
shortcut,
|
||||
checked,
|
||||
disabled,
|
||||
variant,
|
||||
..
|
||||
} => {
|
||||
let foreground = if *variant == ContextMenuItemVariant::Destructive {
|
||||
colors.destructive
|
||||
} else {
|
||||
colors.popover_foreground
|
||||
};
|
||||
let hover = if *variant == ContextMenuItemVariant::Destructive {
|
||||
colors.destructive.opacity(0.1)
|
||||
} else {
|
||||
colors.accent
|
||||
};
|
||||
|
||||
div()
|
||||
.id(("context-menu-item", index))
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.min_h(px(28.0))
|
||||
.px(px(8.0))
|
||||
.py(px(4.0))
|
||||
.rounded(px(5.0))
|
||||
.text_xs()
|
||||
.text_color(foreground)
|
||||
.when(*disabled, |this| this.opacity(0.5).cursor_not_allowed())
|
||||
.when(!*disabled, |this| {
|
||||
this.cursor_default()
|
||||
.hover(move |style| style.bg(hover))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.activate(index, window, cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w(px(12.0))
|
||||
.text_center()
|
||||
.child(if checked == &Some(true) { "✓" } else { "" }),
|
||||
)
|
||||
.child(div().flex_1().child(label.clone()))
|
||||
.when_some(shortcut.clone(), |this, shortcut| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(colors.muted_foreground)
|
||||
.child(shortcut),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::Label(label) => div()
|
||||
.px(px(8.0))
|
||||
.py(px(6.0))
|
||||
.text_xs()
|
||||
.text_color(colors.muted_foreground)
|
||||
.child(label.clone())
|
||||
.into_any_element(),
|
||||
ContextMenuItem::Separator => div()
|
||||
.h(px(1.0))
|
||||
.mx(px(-4.0))
|
||||
.my(px(4.0))
|
||||
.bg(colors.border.opacity(0.5))
|
||||
.into_any_element(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
deferred(
|
||||
anchored()
|
||||
.position(position)
|
||||
.snap_to_window_with_margin(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.id("context-menu-content")
|
||||
.occlude()
|
||||
.min_w(px(160.0))
|
||||
.max_h(window.viewport_size().height - px(16.0))
|
||||
.overflow_y_scroll()
|
||||
.p(px(4.0))
|
||||
.rounded(theme.radius)
|
||||
.bg(colors.popover)
|
||||
.text_color(colors.popover_foreground)
|
||||
.border_1()
|
||||
.border_color(colors.foreground.opacity(0.1))
|
||||
.shadow_md()
|
||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| this.close(cx)))
|
||||
.children(items),
|
||||
),
|
||||
)
|
||||
.with_priority(1)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn context_menu_trigger(
|
||||
id: impl Into<ElementId>,
|
||||
child: impl IntoElement,
|
||||
menu: Entity<ContextMenu>,
|
||||
) -> impl IntoElement {
|
||||
div().id(id).size_full().child(child).on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
menu.update(cx, |menu, cx| menu.open(event.position, cx));
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
use gpui::{App, FontWeight, IntoElement, RenderOnce, SharedString, Window, div, prelude::*};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct Label {
|
||||
text: SharedString,
|
||||
muted: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub(crate) fn new(text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
muted: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn muted(mut self) -> Self {
|
||||
self.muted = true;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Label {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let colors = window.theme().colors;
|
||||
|
||||
div()
|
||||
.text_xs()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(if self.muted {
|
||||
colors.muted_foreground
|
||||
} else {
|
||||
colors.foreground
|
||||
})
|
||||
.when(self.disabled, |this| this.opacity(0.5))
|
||||
.child(self.text)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
mod badge;
|
||||
mod button;
|
||||
mod context_menu;
|
||||
mod label;
|
||||
mod resizable;
|
||||
mod separator;
|
||||
|
||||
pub(crate) use badge::{Badge, BadgeVariant};
|
||||
pub(crate) use button::{Button, ButtonSize, ButtonVariant};
|
||||
pub(crate) use context_menu::{
|
||||
ContextMenu, ContextMenuItem, ContextMenuItemVariant, context_menu_trigger,
|
||||
};
|
||||
pub(crate) use label::Label;
|
||||
pub(crate) use resizable::{Orientation, ResizablePanelGroup};
|
||||
pub(crate) use separator::Separator;
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
use gpui::{
|
||||
AnyView, AppContext, ClickEvent, Context, DragMoveEvent, Entity, IntoElement, Render, Window,
|
||||
div, prelude::*, px, relative,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) enum Orientation {
|
||||
#[default]
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ResizeDrag;
|
||||
|
||||
fn clamp_fraction(fraction: f32, minimum_fraction: f32) -> f32 {
|
||||
fraction.clamp(minimum_fraction, 1.0 - minimum_fraction)
|
||||
}
|
||||
|
||||
pub(crate) struct ResizablePanelGroup {
|
||||
orientation: Orientation,
|
||||
first: AnyView,
|
||||
second: AnyView,
|
||||
initial_fraction: f32,
|
||||
fraction: f32,
|
||||
minimum_fraction: f32,
|
||||
}
|
||||
|
||||
impl ResizablePanelGroup {
|
||||
pub(crate) fn new<A, B>(orientation: Orientation, first: Entity<A>, second: Entity<B>) -> Self
|
||||
where
|
||||
A: Render,
|
||||
B: Render,
|
||||
{
|
||||
Self {
|
||||
orientation,
|
||||
first: first.into(),
|
||||
second: second.into(),
|
||||
initial_fraction: 0.5,
|
||||
fraction: 0.5,
|
||||
minimum_fraction: 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn initial_fraction(mut self, fraction: f32) -> Self {
|
||||
let fraction = self.clamp_fraction(fraction);
|
||||
self.initial_fraction = fraction;
|
||||
self.fraction = fraction;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn minimum_fraction(mut self, fraction: f32) -> Self {
|
||||
self.minimum_fraction = fraction.clamp(0.0, 0.49);
|
||||
self.initial_fraction = self.clamp_fraction(self.initial_fraction);
|
||||
self.fraction = self.clamp_fraction(self.fraction);
|
||||
self
|
||||
}
|
||||
|
||||
fn clamp_fraction(&self, fraction: f32) -> f32 {
|
||||
clamp_fraction(fraction, self.minimum_fraction)
|
||||
}
|
||||
|
||||
fn resize(
|
||||
&mut self,
|
||||
event: &DragMoveEvent<ResizeDrag>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let bounds = event.bounds;
|
||||
let fraction = match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
(event.event.position.x - bounds.left()) / (bounds.right() - bounds.left())
|
||||
}
|
||||
Orientation::Vertical => {
|
||||
(event.event.position.y - bounds.top()) / (bounds.bottom() - bounds.top())
|
||||
}
|
||||
};
|
||||
|
||||
self.fraction = self.clamp_fraction(fraction);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn reset(&mut self, cx: &mut Context<Self>) {
|
||||
self.fraction = self.initial_fraction;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ResizablePanelGroup {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = window.theme().colors;
|
||||
let orientation = self.orientation;
|
||||
let first_fraction = self.fraction;
|
||||
|
||||
let handle = div()
|
||||
.id("resizable-handle")
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when(orientation == Orientation::Horizontal, |this| {
|
||||
this.w(px(5.0)).h_full().cursor_col_resize().child(
|
||||
div()
|
||||
.w(px(1.0))
|
||||
.h_full()
|
||||
.bg(colors.border)
|
||||
.group_hover("resizable", |style| style.bg(colors.ring)),
|
||||
)
|
||||
})
|
||||
.when(orientation == Orientation::Vertical, |this| {
|
||||
this.h(px(5.0)).w_full().cursor_row_resize().child(
|
||||
div()
|
||||
.h(px(1.0))
|
||||
.w_full()
|
||||
.bg(colors.border)
|
||||
.group_hover("resizable", |style| style.bg(colors.ring)),
|
||||
)
|
||||
})
|
||||
.group("resizable")
|
||||
.on_click(cx.listener(|this, event: &ClickEvent, _, cx| {
|
||||
if event.click_count() >= 2 {
|
||||
this.reset(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.on_drag(ResizeDrag, |_, _, _, cx| cx.new(|_| gpui::Empty));
|
||||
|
||||
div()
|
||||
.id("resizable-panel-group")
|
||||
.flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.when(orientation == Orientation::Vertical, |this| this.flex_col())
|
||||
.on_drag_move::<ResizeDrag>(cx.listener(Self::resize))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.when(orientation == Orientation::Horizontal, |this| {
|
||||
this.w(relative(first_fraction)).h_full()
|
||||
})
|
||||
.when(orientation == Orientation::Vertical, |this| {
|
||||
this.h(relative(first_fraction)).w_full()
|
||||
})
|
||||
.child(self.first.clone()),
|
||||
)
|
||||
.child(handle)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.child(self.second.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::clamp_fraction;
|
||||
|
||||
#[test]
|
||||
fn panel_fraction_respects_both_minimums() {
|
||||
assert_eq!(clamp_fraction(-1.0, 0.2), 0.2);
|
||||
assert_eq!(clamp_fraction(0.45, 0.2), 0.45);
|
||||
assert_eq!(clamp_fraction(2.0, 0.2), 0.8);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
use gpui::{App, IntoElement, RenderOnce, Window, div, prelude::*, px};
|
||||
|
||||
use crate::{components::Orientation, theme::ActiveTheme};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct Separator {
|
||||
orientation: Orientation,
|
||||
}
|
||||
|
||||
impl Separator {
|
||||
pub(crate) fn horizontal() -> Self {
|
||||
Self {
|
||||
orientation: Orientation::Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn vertical() -> Self {
|
||||
Self {
|
||||
orientation: Orientation::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Separator {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let color = window.theme().colors.border.opacity(0.5);
|
||||
|
||||
div()
|
||||
.flex_none()
|
||||
.bg(color)
|
||||
.map(|this| match self.orientation {
|
||||
Orientation::Horizontal => this.w_full().h(px(1.0)),
|
||||
Orientation::Vertical => this.h_full().w(px(1.0)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use gpui::{
|
|||
WindowOptions,
|
||||
};
|
||||
|
||||
mod components;
|
||||
mod panels;
|
||||
mod shell;
|
||||
mod theme;
|
||||
|
|
|
|||
Loading…
Reference in New Issue