feat: add new theme (#61)
Reviewed-on: #61
This commit was merged in pull request #61.
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
/// A cubic bezier function like CSS `cubic-bezier`.
|
||||
///
|
||||
/// Builder:
|
||||
///
|
||||
/// https://cubic-bezier.com
|
||||
pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> impl Fn(f32) -> f32 {
|
||||
move |t: f32| {
|
||||
let one_t = 1.0 - t;
|
||||
let one_t2 = one_t * one_t;
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
|
||||
// The Bezier curve function for x and y, where x0 = 0, y0 = 0, x3 = 1, y3 = 1
|
||||
let _x = 3.0 * x1 * one_t2 * t + 3.0 * x2 * one_t * t2 + t3;
|
||||
|
||||
3.0 * y1 * one_t2 * t + 3.0 * y2 * one_t * t2 + t3
|
||||
}
|
||||
}
|
||||
+6
-25
@@ -1,4 +1,3 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AbsoluteLength, AnyElement, App, Bounds, Div, Hsla, ImageSource, InteractiveElement,
|
||||
Interactivity, IntoElement, ObjectFit, ParentElement, PathBuilder, Pixels, Point, RenderOnce,
|
||||
@@ -24,7 +23,7 @@ const FALLBACK_SEED: &str = "coop";
|
||||
const CIRCLE_SEGMENTS: usize = 32;
|
||||
|
||||
/// Returns the size of the avatar based on the given [`Size`].
|
||||
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
match size {
|
||||
Size::Large => px(64.).into(),
|
||||
Size::Medium => px(32.).into(),
|
||||
@@ -36,7 +35,7 @@ pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
|
||||
/// A deterministic, offline pixel-art avatar derived from a seed.
|
||||
#[derive(IntoElement)]
|
||||
pub(crate) struct PixelAvatar {
|
||||
struct PixelAvatar {
|
||||
seed: u64,
|
||||
size: Size,
|
||||
style: StyleRefinement,
|
||||
@@ -44,7 +43,7 @@ pub(crate) struct PixelAvatar {
|
||||
|
||||
impl PixelAvatar {
|
||||
/// Creates a pixel avatar from `seed`.
|
||||
pub fn new(seed: impl AsRef<str>) -> Self {
|
||||
fn new(seed: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
seed: fnv1a(seed.as_ref().as_bytes()),
|
||||
size: Size::Medium,
|
||||
@@ -188,8 +187,7 @@ fn paint_polygons<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximates the circle of `radius` around `center` as a convex polygon,
|
||||
/// wound so that its interior is on the left of every directed edge.
|
||||
/// Approximates the circle of `radius` around `center` as a convex polygon wound left-interior.
|
||||
fn circle_polygon(center: Point<Pixels>, radius: f32) -> Vec<Point<Pixels>> {
|
||||
let center_x = center.x.as_f32();
|
||||
let center_y = center.y.as_f32();
|
||||
@@ -354,11 +352,9 @@ fn generated_avatar(seed: Option<&str>, size: Pixels) -> AnyElement {
|
||||
pub struct Avatar {
|
||||
base: Div,
|
||||
picture: Option<ImageSource>,
|
||||
grayscale: bool,
|
||||
seed: Option<String>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
@@ -377,11 +373,9 @@ impl Avatar {
|
||||
Avatar {
|
||||
base: div(),
|
||||
picture,
|
||||
grayscale: false,
|
||||
seed: None,
|
||||
style: StyleRefinement::default(),
|
||||
size: Size::Medium,
|
||||
border_color: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
@@ -424,27 +418,17 @@ impl InteractiveElement for Avatar {
|
||||
}
|
||||
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let border_width = if self.border_color.is_some() {
|
||||
px(2.)
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let image_size = avatar_size(self.size).to_pixels(window.rem_size());
|
||||
let container_size = image_size + border_width * 2.;
|
||||
|
||||
let content = match self.picture {
|
||||
Some(picture) => {
|
||||
let seed = self.seed;
|
||||
let grayscale = self.grayscale;
|
||||
|
||||
img(picture)
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.object_fit(ObjectFit::Cover)
|
||||
.grayscale(grayscale)
|
||||
.bg(cx.theme().ghost_element_background)
|
||||
.with_fallback(move || generated_avatar(seed.as_deref(), image_size))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -453,12 +437,9 @@ impl RenderOnce for Avatar {
|
||||
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.size(container_size)
|
||||
.size(image_size)
|
||||
.rounded_full()
|
||||
.overflow_hidden()
|
||||
.when_some(self.border_color, |this, color| {
|
||||
this.border(border_width).border_color(color)
|
||||
})
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
|
||||
+109
-112
@@ -4,10 +4,13 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div, relative,
|
||||
Styled, Window, div, px, relative,
|
||||
};
|
||||
use gpui_base::Button as BaseButton;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{
|
||||
ActiveTheme, TextStyle, Typeset as _, bubble_radius, button_radius, plate_active, plate_hover,
|
||||
wash,
|
||||
};
|
||||
|
||||
use crate::indicator::Indicator;
|
||||
use crate::tooltip::Tooltip;
|
||||
@@ -24,10 +27,10 @@ pub struct ButtonCustomVariant {
|
||||
impl ButtonCustomVariant {
|
||||
pub fn new(_window: &Window, cx: &App) -> Self {
|
||||
Self {
|
||||
color: cx.theme().element_background,
|
||||
foreground: cx.theme().element_foreground,
|
||||
hover: cx.theme().element_hover,
|
||||
active: cx.theme().element_active,
|
||||
color: cx.theme().solid,
|
||||
foreground: cx.theme().on_solid,
|
||||
hover: plate_hover(cx.theme().solid),
|
||||
active: plate_active(cx.theme().solid),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,47 +73,49 @@ pub enum ButtonVariant {
|
||||
pub trait ButtonVariants: Sized {
|
||||
fn with_variant(self, variant: ButtonVariant) -> Self;
|
||||
|
||||
/// With the primary style for the Button.
|
||||
fn primary(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Primary)
|
||||
}
|
||||
|
||||
/// With the secondary style for the Button.
|
||||
fn secondary(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Secondary)
|
||||
}
|
||||
|
||||
/// With the danger style for the Button.
|
||||
fn danger(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Danger)
|
||||
}
|
||||
|
||||
/// With the warning style for the Button.
|
||||
fn warning(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Warning)
|
||||
}
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost { alt: false })
|
||||
}
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost_alt(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost { alt: true })
|
||||
}
|
||||
|
||||
/// With the transparent style for the Button.
|
||||
fn transparent(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Transparent)
|
||||
}
|
||||
|
||||
/// With the custom style for the Button.
|
||||
fn custom(self, style: ButtonCustomVariant) -> Self {
|
||||
self.with_variant(ButtonVariant::Custom(style))
|
||||
}
|
||||
}
|
||||
|
||||
/// Which side of the label the icon sits on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum IconPosition {
|
||||
/// Left of the label.
|
||||
#[default]
|
||||
Start,
|
||||
/// Right of the label.
|
||||
End,
|
||||
}
|
||||
|
||||
/// A Button element.
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -127,13 +132,10 @@ pub struct Button {
|
||||
rounded: bool,
|
||||
compact: bool,
|
||||
caret: bool,
|
||||
indicator: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
tab_index: isize,
|
||||
tab_stop: bool,
|
||||
|
||||
pub(crate) selected: bool,
|
||||
selected: bool,
|
||||
icon_position: IconPosition,
|
||||
}
|
||||
|
||||
impl From<Button> for AnyElement {
|
||||
@@ -151,22 +153,19 @@ impl Button {
|
||||
variant: ButtonVariant::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
indicator: false,
|
||||
compact: false,
|
||||
caret: false,
|
||||
rounded: false,
|
||||
size: Size::Medium,
|
||||
tooltip: None,
|
||||
on_click: None,
|
||||
on_hover: None,
|
||||
loading: false,
|
||||
children: Vec::new(),
|
||||
tab_index: 0,
|
||||
tab_stop: true,
|
||||
icon_position: IconPosition::Start,
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the button rounded.
|
||||
pub fn rounded(mut self) -> Self {
|
||||
self.rounded = true;
|
||||
self
|
||||
@@ -178,19 +177,23 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon of the button, if the Button have no label, the button well in Icon Button mode.
|
||||
/// Set the icon of the button, if the Button has no label, the button will be in Icon Button mode.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tooltip of the button.
|
||||
/// Set which side of the label the icon sits on (default [`IconPosition::Start`]).
|
||||
pub fn icon_position(mut self, position: IconPosition) -> Self {
|
||||
self.icon_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
||||
self.tooltip = Some(tooltip.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the loading indicator.
|
||||
pub fn loading(mut self, loading: bool) -> Self {
|
||||
self.loading = loading;
|
||||
self
|
||||
@@ -202,13 +205,11 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the caret indicator.
|
||||
pub fn caret(mut self) -> Self {
|
||||
self.caret = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add click handler.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
@@ -217,9 +218,7 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab stop of the button, if true, the button will be focusable by tab key.
|
||||
///
|
||||
/// Default is true.
|
||||
/// Set whether the button can be focused with the tab key (default true).
|
||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
self.tab_stop = tab_stop;
|
||||
self
|
||||
@@ -229,11 +228,6 @@ impl Button {
|
||||
fn clickable(&self) -> bool {
|
||||
!(self.disabled || self.loading) && self.on_click.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hoverable(&self) -> bool {
|
||||
!(self.disabled || self.loading) && self.on_hover.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
@@ -288,24 +282,23 @@ impl InteractiveElement for Button {
|
||||
|
||||
impl RenderOnce for Button {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let style: ButtonVariant = self.variant;
|
||||
let style = self.variant;
|
||||
let clickable = self.clickable();
|
||||
let hoverable = self.hoverable();
|
||||
let normal_style = style.normal(cx);
|
||||
let icon_size = match self.size {
|
||||
Size::Size(v) => Size::Size(v * 0.75),
|
||||
Size::Large => Size::Medium,
|
||||
_ => self.size,
|
||||
};
|
||||
let icon_start = self.icon_position == IconPosition::Start;
|
||||
|
||||
self.base
|
||||
.tab_index(self.tab_index)
|
||||
.tab_index(0)
|
||||
.tab_stop(self.tab_stop)
|
||||
.disabled(self.disabled)
|
||||
.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, window, cx| {
|
||||
// Stop handle any click event when disabled.
|
||||
// To avoid handle dropdown menu open when button is disabled.
|
||||
// Stop clicks when disabled, so a parent (e.g. dropdown menu) doesn't handle them.
|
||||
if !clickable {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
@@ -322,9 +315,16 @@ impl RenderOnce for Button {
|
||||
.cursor_default()
|
||||
.overflow_hidden()
|
||||
.map(|this| match self.rounded {
|
||||
false => this.rounded(cx.theme().radius),
|
||||
true => this.rounded_full(),
|
||||
false => this.rounded(px(button_radius())),
|
||||
true => this.rounded(px(bubble_radius())),
|
||||
})
|
||||
.when_some(
|
||||
match style {
|
||||
ButtonVariant::Secondary => Some(cx.theme().border),
|
||||
_ => None,
|
||||
},
|
||||
|this, border_color| this.border_1().border_color(border_color),
|
||||
)
|
||||
.when(!self.compact, |this| {
|
||||
if self.label.is_none() && self.children.is_empty() {
|
||||
// Icon Button
|
||||
@@ -337,7 +337,7 @@ impl RenderOnce for Button {
|
||||
}
|
||||
} else {
|
||||
// Normal Button
|
||||
match self.size {
|
||||
let this = match self.size {
|
||||
Size::Size(size) => this.px(size * 0.2),
|
||||
Size::XSmall => {
|
||||
if self.icon.is_some() {
|
||||
@@ -367,12 +367,21 @@ impl RenderOnce for Button {
|
||||
this.h_10().px_3()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !icon_start && self.icon.is_some() {
|
||||
match self.size {
|
||||
Size::XSmall | Size::Small => this.pl_2p5().pr_2(),
|
||||
Size::Medium | Size::Large => this.pl_3p5().pr_3(),
|
||||
Size::Size(_) => this,
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
||||
// Stop handle any click event when disabled.
|
||||
// To avoid handle dropdown menu open when button is disabled.
|
||||
// Stop clicks when disabled, so a parent (e.g. dropdown menu) doesn't handle them.
|
||||
if self.disabled {
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
@@ -380,30 +389,35 @@ impl RenderOnce for Button {
|
||||
// Avoid focus on mouse down.
|
||||
window.prevent_default();
|
||||
})
|
||||
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
|
||||
this.on_hover(move |hovered, window, cx| {
|
||||
(on_hover)(hovered, window, cx);
|
||||
})
|
||||
})
|
||||
.child({
|
||||
h_flex()
|
||||
.id("label")
|
||||
.justify_center()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs().gap_1(),
|
||||
Size::Small => this.text_sm().gap_1p5(),
|
||||
_ => this.text_sm().gap_2(),
|
||||
Size::XSmall => this.text_style(TextStyle::Caption).gap_1(),
|
||||
Size::Small => this.text_style(TextStyle::Callout).gap_1p5(),
|
||||
_ => this.text_style(TextStyle::Callout).gap(px(theme::SPACE)),
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
.when(!self.loading && icon_start, |this| {
|
||||
this.when_some(self.icon.clone(), |this, icon| {
|
||||
this.child(icon.with_size(icon_size))
|
||||
})
|
||||
})
|
||||
.when(self.loading, |this| this.child(Indicator::new()))
|
||||
.when(self.loading && icon_start, |this| {
|
||||
this.child(Indicator::new())
|
||||
})
|
||||
.when_some(self.label, |this, label| {
|
||||
this.child(div().flex_none().line_height(relative(1.)).child(label))
|
||||
})
|
||||
.children(self.children)
|
||||
.when(!self.loading && !icon_start, |this| {
|
||||
this.when_some(self.icon.clone(), |this, icon| {
|
||||
this.child(icon.with_size(icon_size))
|
||||
})
|
||||
})
|
||||
.when(self.loading && !icon_start, |this| {
|
||||
this.child(Indicator::new())
|
||||
})
|
||||
.when(self.caret, |this| {
|
||||
this.justify_between().gap_0p5().child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
@@ -413,17 +427,6 @@ impl RenderOnce for Button {
|
||||
})
|
||||
})
|
||||
.text_color(normal_style.fg)
|
||||
.when(self.indicator && !self.disabled, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_px()
|
||||
.right_px()
|
||||
.size_1()
|
||||
.rounded_full()
|
||||
.bg(gpui::green()),
|
||||
)
|
||||
})
|
||||
.when(!self.disabled && !self.selected, |this| {
|
||||
this.bg(normal_style.bg)
|
||||
.hover(|this| {
|
||||
@@ -470,15 +473,15 @@ impl ButtonVariant {
|
||||
|
||||
fn bg_color(&self, cx: &App) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Primary => cx.theme().element_background,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_background,
|
||||
ButtonVariant::Danger => cx.theme().danger_background,
|
||||
ButtonVariant::Warning => cx.theme().warning_background,
|
||||
ButtonVariant::Primary => cx.theme().solid,
|
||||
ButtonVariant::Secondary => cx.theme().surface_raised,
|
||||
ButtonVariant::Danger => cx.theme().danger_strong,
|
||||
ButtonVariant::Warning => cx.theme().warning_muted,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().ghost_element_background_alt
|
||||
cx.theme().surface_raised
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
gpui::transparent_black()
|
||||
}
|
||||
}
|
||||
ButtonVariant::Custom(colors) => colors.color,
|
||||
@@ -488,11 +491,15 @@ impl ButtonVariant {
|
||||
|
||||
fn text_color(&self, cx: &App) -> Hsla {
|
||||
match self {
|
||||
ButtonVariant::Primary => cx.theme().element_foreground,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Primary => cx.theme().on_solid,
|
||||
ButtonVariant::Secondary => cx.theme().text,
|
||||
// Bezel's destructive pairing: the red plate carries white ink in
|
||||
// both appearances, not the appearance-flipping on_accent.
|
||||
ButtonVariant::Danger => gpui::white(),
|
||||
// warning_muted is a translucent amber wash, so the label is the
|
||||
// amber token itself — dark amber in light, bright in dark.
|
||||
ButtonVariant::Warning => cx.theme().warning,
|
||||
ButtonVariant::Transparent => cx.theme().text_faint,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().text
|
||||
@@ -506,19 +513,19 @@ impl ButtonVariant {
|
||||
|
||||
fn hovered(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_hover,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||
ButtonVariant::Warning => cx.theme().warning_hover,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_hover,
|
||||
ButtonVariant::Primary => plate_hover(cx.theme().solid),
|
||||
ButtonVariant::Secondary => cx.theme().surface_raised_hover,
|
||||
ButtonVariant::Danger => plate_hover(cx.theme().danger_strong),
|
||||
ButtonVariant::Warning => plate_hover(cx.theme().warning_muted),
|
||||
ButtonVariant::Ghost { .. } => cx.theme().element_hover,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.hover,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Secondary => cx.theme().text,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Transparent => cx.theme().text_faint,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
@@ -527,18 +534,18 @@ impl ButtonVariant {
|
||||
|
||||
fn active(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_active,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||
ButtonVariant::Danger => cx.theme().danger_active,
|
||||
ButtonVariant::Warning => cx.theme().warning_active,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_active,
|
||||
ButtonVariant::Primary => plate_active(cx.theme().solid),
|
||||
ButtonVariant::Secondary => plate_active(cx.theme().surface_raised),
|
||||
ButtonVariant::Danger => plate_active(cx.theme().danger_strong),
|
||||
ButtonVariant::Warning => plate_active(cx.theme().warning_muted),
|
||||
ButtonVariant::Ghost { .. } => cx.theme().element_active,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Secondary => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_faint,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
@@ -547,18 +554,18 @@ impl ButtonVariant {
|
||||
|
||||
fn selected(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Primary => cx.theme().element_selected,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||
ButtonVariant::Danger => cx.theme().danger_selected,
|
||||
ButtonVariant::Warning => cx.theme().warning_selected,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_selected,
|
||||
ButtonVariant::Primary => cx.theme().element_active,
|
||||
ButtonVariant::Secondary => cx.theme().element_active,
|
||||
ButtonVariant::Danger => plate_active(cx.theme().danger_strong),
|
||||
ButtonVariant::Warning => plate_active(cx.theme().warning_muted),
|
||||
ButtonVariant::Ghost { .. } => cx.theme().element_active,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Secondary => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_faint,
|
||||
_ => self.text_color(cx),
|
||||
};
|
||||
|
||||
@@ -566,18 +573,8 @@ impl ButtonVariant {
|
||||
}
|
||||
|
||||
fn disabled(&self, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Danger => cx.theme().danger_disabled,
|
||||
ButtonVariant::Warning => cx.theme().warning_disabled,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_disabled,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_disabled,
|
||||
_ => cx.theme().element_disabled,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Primary => cx.theme().text_muted,
|
||||
_ => cx.theme().text_muted,
|
||||
};
|
||||
let bg = wash(0.08);
|
||||
let fg = cx.theme().text_muted;
|
||||
|
||||
ButtonVariantStyle { bg, fg }
|
||||
}
|
||||
|
||||
+34
-90
@@ -2,20 +2,17 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||
div, hsla, point, px,
|
||||
AnimationExt as _, AnyElement, App, ClickEvent, Div, FocusHandle, IntoElement, ParentElement,
|
||||
Pixels, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, px, transparent_black,
|
||||
};
|
||||
use gpui_base::{DialogBackdrop, DialogPopup, DialogTitle};
|
||||
use instant::Duration;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, SCRIM_ALPHA_DARK, scrim, surface_radius, surface_shadows};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::motion::DIALOG_IN;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{IconName, Root, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
type OnClose = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
type OnOk = Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>>;
|
||||
type OnCancel = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||
type RenderButtonFn = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
|
||||
@@ -70,7 +67,6 @@ pub struct Dialog {
|
||||
width: Pixels,
|
||||
max_width: Option<Pixels>,
|
||||
|
||||
on_close: OnClose,
|
||||
on_ok: OnOk,
|
||||
on_cancel: OnCancel,
|
||||
|
||||
@@ -80,10 +76,10 @@ pub struct Dialog {
|
||||
show_close: bool,
|
||||
button_props: DialogButtonProps,
|
||||
|
||||
/// This will be change when open the dialog, the focus handle is create when open the dialog.
|
||||
pub focus_handle: FocusHandle,
|
||||
pub layer_ix: usize,
|
||||
pub overlay_visible: bool,
|
||||
/// The focus handle created when the dialog is opened, owned by the `Root`.
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
pub(crate) layer_ix: usize,
|
||||
pub(crate) overlay_visible: bool,
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
@@ -100,7 +96,6 @@ impl Dialog {
|
||||
keyboard: true,
|
||||
layer_ix: 0,
|
||||
overlay_visible: false,
|
||||
on_close: Rc::new(|_, _, _| {}),
|
||||
on_ok: None,
|
||||
on_cancel: Rc::new(|_, _, _| true),
|
||||
button_props: DialogButtonProps::default(),
|
||||
@@ -115,15 +110,8 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the footer of the dialog.
|
||||
///
|
||||
/// The `footer` is a function that takes two `RenderButtonFn` and a `WindowContext` and returns a list of `AnyElement`.
|
||||
///
|
||||
/// - First `RenderButtonFn` is the render function for the OK button.
|
||||
/// - Second `RenderButtonFn` is the render function for the CANCEL button.
|
||||
///
|
||||
/// When you set the footer, the footer will be placed default footer buttons.
|
||||
pub fn footer<E, F>(mut self, footer: F) -> Self
|
||||
/// Sets a custom footer, replacing the default footer buttons.
|
||||
fn footer<E, F>(mut self, footer: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(RenderButtonFn, RenderButtonFn, &mut Window, &mut App) -> Vec<E> + 'static,
|
||||
@@ -150,20 +138,7 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the dialog is closed.
|
||||
///
|
||||
/// Called after [`Self::on_ok`] or [`Self::on_cancel`] callback.
|
||||
pub fn on_close(
|
||||
mut self,
|
||||
on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_close = Rc::new(on_close);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the dialog is has been confirmed.
|
||||
///
|
||||
/// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
|
||||
/// Sets the confirm callback, returning `false` keeps the dialog open.
|
||||
pub fn on_ok(
|
||||
mut self,
|
||||
on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -172,9 +147,7 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the callback for when the dialog is has been canceled.
|
||||
///
|
||||
/// The callback should return `true` to close the dialog, if return `false` the dialog will not be closed.
|
||||
/// Sets the cancel callback, returning `false` keeps the dialog open.
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -189,15 +162,13 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the dialog, defaults to 480px.
|
||||
/// Sets the width of the dialog, defaults to 380px.
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overlay closable of the dialog, defaults to `true`.
|
||||
///
|
||||
/// When the overlay is clicked, the dialog will be closed.
|
||||
/// Sets whether clicking the overlay closes the dialog, defaults to `true`.
|
||||
pub fn overlay_closable(mut self, overlay_closable: bool) -> Self {
|
||||
self.overlay_closable = overlay_closable;
|
||||
self
|
||||
@@ -209,7 +180,7 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_overlay(&self) -> bool {
|
||||
pub(crate) fn has_overlay(&self) -> bool {
|
||||
self.overlay
|
||||
}
|
||||
}
|
||||
@@ -232,13 +203,11 @@ impl RenderOnce for Dialog {
|
||||
let is_topmost = layer_ix + 1 == Root::read(window, cx).active_dialogs.len();
|
||||
let has_footer = self.footer.is_some();
|
||||
|
||||
let on_close = self.on_close.clone();
|
||||
let on_ok = self.on_ok.clone();
|
||||
let on_cancel = self.on_cancel.clone();
|
||||
|
||||
let render_ok: RenderButtonFn = Box::new({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
let ok_variant = self.button_props.ok_variant;
|
||||
let ok_text = self.button_props.ok_text.unwrap_or_else(|| "OK".into());
|
||||
|
||||
@@ -249,7 +218,6 @@ impl RenderOnce for Dialog {
|
||||
.font_semibold()
|
||||
.on_click({
|
||||
let on_ok = on_ok.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
move |_, window, cx| {
|
||||
if let Some(on_ok) = &on_ok
|
||||
@@ -258,7 +226,6 @@ impl RenderOnce for Dialog {
|
||||
return;
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_dialog(cx);
|
||||
}
|
||||
})
|
||||
@@ -268,7 +235,6 @@ impl RenderOnce for Dialog {
|
||||
|
||||
let render_cancel: RenderButtonFn = Box::new({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
let cancel_variant = self.button_props.cancel_variant;
|
||||
let cancel_text = self
|
||||
.button_props
|
||||
@@ -281,13 +247,11 @@ impl RenderOnce for Dialog {
|
||||
.with_variant(cancel_variant)
|
||||
.on_click({
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
move |_, window, cx| {
|
||||
if !on_cancel(&ClickEvent::default(), window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_dialog(cx);
|
||||
}
|
||||
})
|
||||
@@ -306,25 +270,24 @@ impl RenderOnce for Dialog {
|
||||
padding_right = pr.to_pixels(self.width.into(), window.rem_size());
|
||||
}
|
||||
|
||||
let animation = Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.32, 0.72, 0., 1.));
|
||||
|
||||
let backdrop = DialogBackdrop::new()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.when(self.overlay_visible, |this| this.bg(cx.theme().overlay))
|
||||
.with_animation("fade-in", animation.clone(), move |this, delta| {
|
||||
.when(self.overlay_visible, |this| {
|
||||
this.bg(scrim(SCRIM_ALPHA_DARK))
|
||||
})
|
||||
.with_animation("fade-in", DIALOG_IN.animation(), move |this, delta| {
|
||||
this.opacity(delta)
|
||||
});
|
||||
|
||||
let card = DialogPopup::new()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.bg(cx.theme().background)
|
||||
.bg(cx.theme().surface_dialog)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border.alpha(0.4))
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_xl())
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(px(surface_radius()))
|
||||
.when(cx.theme().shadow, |this| this.shadow(surface_shadows()))
|
||||
.min_h_24()
|
||||
.refine_style(&self.style)
|
||||
.relative()
|
||||
@@ -349,7 +312,6 @@ impl RenderOnce for Dialog {
|
||||
})
|
||||
.when(self.show_close, |this| {
|
||||
let on_cancel = on_cancel.clone();
|
||||
let on_close = on_close.clone();
|
||||
|
||||
this.child(
|
||||
div()
|
||||
@@ -364,14 +326,13 @@ impl RenderOnce for Dialog {
|
||||
.icon(IconName::CloseCircleFill)
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.foreground(cx.theme().icon_muted)
|
||||
.color(cx.theme().ghost_element_background)
|
||||
.hover(cx.theme().ghost_element_background)
|
||||
.active(cx.theme().ghost_element_background),
|
||||
.foreground(cx.theme().text_faint)
|
||||
.color(transparent_black())
|
||||
.hover(cx.theme().element_hover)
|
||||
.active(cx.theme().element_active),
|
||||
)
|
||||
.on_click(move |_, window, cx| {
|
||||
on_cancel(&ClickEvent::default(), window, cx);
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_dialog(cx);
|
||||
}),
|
||||
),
|
||||
@@ -407,26 +368,12 @@ impl RenderOnce for Dialog {
|
||||
.children(footer(render_ok, render_cancel, window, cx)),
|
||||
)
|
||||
})
|
||||
.with_animation("slide-down", animation, move |this, delta| {
|
||||
// Settle at the target offset; only the entrance is animated.
|
||||
let y_offset = px(30.) * (delta - 1.);
|
||||
// This is equivalent to `shadow_xl` with an extra opacity.
|
||||
let shadow = vec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1 * delta),
|
||||
offset: point(px(0.), px(20.)),
|
||||
blur_radius: px(25.),
|
||||
spread_radius: px(-5.),
|
||||
inset: false,
|
||||
},
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1 * delta),
|
||||
offset: point(px(0.), px(8.)),
|
||||
blur_radius: px(10.),
|
||||
spread_radius: px(-6.),
|
||||
inset: false,
|
||||
},
|
||||
];
|
||||
.with_animation("slide-down", DIALOG_IN.animation(), move |this, delta| {
|
||||
let y_offset = px(2.) * (delta - 1.);
|
||||
let mut shadow = surface_shadows();
|
||||
for shadow in &mut shadow {
|
||||
shadow.color.a *= delta;
|
||||
}
|
||||
this.top(y_offset).shadow(shadow)
|
||||
});
|
||||
|
||||
@@ -442,9 +389,6 @@ impl RenderOnce for Dialog {
|
||||
None => has_footer,
|
||||
})
|
||||
.on_cancel(move |event, window, cx| on_cancel(event, window, cx))
|
||||
.on_close(move |event, window, cx| {
|
||||
on_close(event, window, cx);
|
||||
window.close_dialog(cx);
|
||||
})
|
||||
.on_close(move |_, window, cx| window.close_dialog(cx))
|
||||
}
|
||||
}
|
||||
|
||||
+17
-16
@@ -17,7 +17,7 @@ use gpui_base::dock::{
|
||||
PaneRef, PanelId, TabGroupContext, TabGroupRenderer,
|
||||
};
|
||||
use gpui_base::{HandleEdge, Placement, ResizeHandleContext};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT, TextStyle, Typeset as _, button_radius, panel_radius};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::resizable::{resize_handle, resize_handle_appearance};
|
||||
@@ -83,9 +83,11 @@ pub fn add_panel_to(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let _ = dock.update(cx, |area, cx| {
|
||||
if let Err(error) = dock.update(cx, |area, cx| {
|
||||
add_panel(area, panel, placement, window, cx);
|
||||
});
|
||||
}) {
|
||||
log::warn!("Failed to add panel to dock: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The panel in any region of `area` whose logical id is `key`.
|
||||
@@ -346,12 +348,12 @@ impl Render for DragPreview {
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_sm()
|
||||
.rounded(px(button_radius()))
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.text_ellipsis()
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
.bg(cx.theme().background)
|
||||
.bg(cx.theme().bg)
|
||||
.child(panel_title(&self.panel, cx))
|
||||
}
|
||||
}
|
||||
@@ -446,7 +448,7 @@ impl TabGroupSkin {
|
||||
.flex_grow_1()
|
||||
.min_w_16()
|
||||
.when(droppable, |this| {
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().surface_background))
|
||||
this.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().drop_target))
|
||||
.on_drop({
|
||||
let group = TabGroupContext::clone(group);
|
||||
move |drag: &DragPanel, window, cx| {
|
||||
@@ -465,7 +467,7 @@ impl TabGroupSkin {
|
||||
let bar = TabBar::new("tab-bar")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.bg(cx.theme().panel_background)
|
||||
.bg(cx.theme().bg)
|
||||
.when(needs_traffic_light_padding, |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
@@ -537,7 +539,7 @@ impl TabGroupSkin {
|
||||
this.rounded_l_none()
|
||||
.border_l_2()
|
||||
.border_r_0()
|
||||
.border_color(cx.theme().border)
|
||||
.border_color(cx.theme().drop_line)
|
||||
})
|
||||
.on_drop({
|
||||
let group = TabGroupContext::clone(group);
|
||||
@@ -567,7 +569,7 @@ impl TabGroupSkin {
|
||||
h_flex()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.w_full()
|
||||
.bg(cx.theme().panel_background)
|
||||
.bg(cx.theme().bg)
|
||||
.child(bar.flex_1())
|
||||
.child(window_controls())
|
||||
.into_any_element()
|
||||
@@ -670,8 +672,7 @@ impl TabGroupRenderer for TabGroupSkin {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
// The left dock's only panel draws bare, so its content can own the
|
||||
// window's top-left corner instead of a tab bar doing so.
|
||||
// The plain left group draws bare so its content can own the window's top-left corner.
|
||||
if self.is_plain_left_group(group, cx) {
|
||||
return Empty.into_any_element();
|
||||
}
|
||||
@@ -717,7 +718,7 @@ impl TabGroupRenderer for TabGroupSkin {
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.bg(cx.theme().panel_background)
|
||||
.bg(cx.theme().bg)
|
||||
.overflow_hidden()
|
||||
.child(panel.cached(StyleRefinement::default().v_flex().size_full())),
|
||||
)
|
||||
@@ -758,10 +759,10 @@ impl TabGroupRenderer for TabGroupSkin {
|
||||
.top(top)
|
||||
.w(width)
|
||||
.h(height)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.rounded(px(panel_radius()))
|
||||
.border_1()
|
||||
.border_color(cx.theme().element_disabled)
|
||||
.bg(cx.theme().drop_target_background)
|
||||
.border_color(cx.theme().drop_line)
|
||||
.bg(cx.theme().drop_target)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,9 @@ use gpui::{
|
||||
use gpui_base::dock::{PanelId, PanelState};
|
||||
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
LayoutChanged,
|
||||
}
|
||||
pub enum PanelEvent {}
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||
@@ -31,44 +26,29 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return true if the panel is zoomable, default is `false`.
|
||||
/// Whether the panel is zoomable, default is `true`.
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Return false to hide panel, true to show panel, default is `true`.
|
||||
///
|
||||
/// This method called in Panel render, we should make sure it is fast.
|
||||
fn visible(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Set active state of the panel.
|
||||
///
|
||||
/// This method will be called when the panel is active or inactive.
|
||||
///
|
||||
/// The last_active_panel and current_active_panel will be touched when the panel is active.
|
||||
fn set_active(&self, _active: bool, _window: &mut Window, _cx: &mut App) {}
|
||||
|
||||
/// Set zoomed state of the panel.
|
||||
///
|
||||
/// This method will be called when the panel is zoomed or unzoomed.
|
||||
///
|
||||
/// Only current Panel will touch this method.
|
||||
fn set_zoomed(&self, _zoomed: bool, _cx: &mut App) {}
|
||||
|
||||
/// The addition popup menu of the panel, default is `None`.
|
||||
fn popup_menu(&self, this: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
this
|
||||
}
|
||||
|
||||
/// The addition toolbar buttons of the panel used to show in the right of the title bar, default is `None`.
|
||||
/// Toolbar buttons shown at the right of the title bar.
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PanelView: 'static + Send + Sync {
|
||||
pub(crate) trait PanelView: 'static + Send + Sync {
|
||||
fn panel_id(&self, cx: &App) -> SharedString;
|
||||
fn title(&self, cx: &App) -> AnyElement;
|
||||
fn closable(&self, cx: &App) -> bool;
|
||||
@@ -76,8 +56,6 @@ pub trait PanelView: 'static + Send + Sync {
|
||||
fn visible(&self, cx: &App) -> bool;
|
||||
fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
|
||||
fn set_zoomed(&self, zoomed: bool, cx: &mut App);
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu;
|
||||
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button>;
|
||||
fn view(&self) -> AnyView;
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle;
|
||||
}
|
||||
@@ -115,14 +93,6 @@ impl<T: Panel> PanelView for Entity<T> {
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu {
|
||||
self.read(cx).popup_menu(menu, cx)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button> {
|
||||
self.read(cx).toolbar_buttons(window, cx)
|
||||
}
|
||||
|
||||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
@@ -132,24 +102,6 @@ impl<T: Panel> PanelView for Entity<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn PanelView> for AnyView {
|
||||
fn from(handle: &dyn PanelView) -> Self {
|
||||
handle.view()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Panel> From<&dyn PanelView> for Entity<T> {
|
||||
fn from(value: &dyn PanelView) -> Self {
|
||||
value.view().downcast::<T>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for dyn PanelView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.view() == other.view()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PanelHandle {
|
||||
id: PanelId,
|
||||
@@ -164,13 +116,12 @@ impl PanelHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Recover the coop handle behind one of base's.
|
||||
pub fn of(panel: &Arc<dyn gpui_base::dock::PanelView>) -> Option<&Self> {
|
||||
/// Downcast a base handle back to the coop handle.
|
||||
pub(crate) fn of(panel: &Arc<dyn gpui_base::dock::PanelView>) -> Option<&Self> {
|
||||
panel.as_any().downcast_ref::<Self>()
|
||||
}
|
||||
|
||||
/// The coop panel behind this handle.
|
||||
pub fn panel(&self) -> &Arc<dyn PanelView> {
|
||||
pub(crate) fn panel(&self) -> &Arc<dyn PanelView> {
|
||||
&self.panel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce,
|
||||
StyleRefinement, Styled, Window, div, relative,
|
||||
StyleRefinement, Styled, Window, div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{StyledExt as _, v_flex};
|
||||
|
||||
@@ -22,12 +22,6 @@ pub trait GroupBoxVariants: Sized {
|
||||
#[must_use]
|
||||
fn with_variant(self, variant: GroupBoxVariant) -> Self;
|
||||
|
||||
/// Set to use [`GroupBoxVariant::Normal`] to GroupBox.
|
||||
#[must_use]
|
||||
fn normal(self) -> Self {
|
||||
self.with_variant(GroupBoxVariant::Normal)
|
||||
}
|
||||
|
||||
/// Set to use [`GroupBoxVariant::Fill`] to GroupBox.
|
||||
#[must_use]
|
||||
fn fill(self) -> Self {
|
||||
@@ -35,16 +29,13 @@ pub trait GroupBoxVariants: Sized {
|
||||
}
|
||||
}
|
||||
|
||||
/// GroupBox is a styled container element that with
|
||||
/// an optional title to groups related content together.
|
||||
/// A styled container with an optional title that groups related content together.
|
||||
#[derive(IntoElement)]
|
||||
pub struct GroupBox {
|
||||
id: Option<ElementId>,
|
||||
variant: GroupBoxVariant,
|
||||
style: StyleRefinement,
|
||||
title_style: StyleRefinement,
|
||||
title: Option<AnyElement>,
|
||||
content_style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 1]>,
|
||||
}
|
||||
|
||||
@@ -55,8 +46,6 @@ impl GroupBox {
|
||||
id: None,
|
||||
variant: GroupBoxVariant::default(),
|
||||
style: StyleRefinement::default(),
|
||||
title_style: StyleRefinement::default(),
|
||||
content_style: StyleRefinement::default(),
|
||||
title: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
@@ -106,7 +95,7 @@ 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_background), true),
|
||||
GroupBoxVariant::Fill => (Some(cx.theme().surface), true),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
@@ -119,9 +108,7 @@ impl RenderOnce for GroupBox {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.line_height(relative(1.))
|
||||
.refine_style(&self.title_style)
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_semibold()
|
||||
.child(title),
|
||||
)
|
||||
@@ -132,8 +119,7 @@ impl RenderOnce for GroupBox {
|
||||
.text_color(cx.theme().text)
|
||||
.when(has_paddings, |this| this.p_2())
|
||||
.gap_4()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.refine_style(&self.content_style)
|
||||
.rounded(px(theme::panel_radius()))
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
|
||||
+4
-46
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Hsla, IntoElement, Radians, Render, RenderOnce, SharedString,
|
||||
StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||
AnyElement, App, Context, Hsla, IntoElement, Render, RenderOnce, SharedString, StyleRefinement,
|
||||
Styled, Svg, Window, svg,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -18,13 +18,11 @@ pub enum IconName {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowDownCircle,
|
||||
Boom,
|
||||
Book,
|
||||
Ban,
|
||||
ChevronDown,
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Check,
|
||||
CheckCircle,
|
||||
Close,
|
||||
@@ -32,17 +30,14 @@ pub enum IconName {
|
||||
CloseCircleFill,
|
||||
Compass,
|
||||
Copy,
|
||||
Device,
|
||||
Door,
|
||||
Ellipsis,
|
||||
Emoji,
|
||||
Eye,
|
||||
Folder,
|
||||
Input,
|
||||
Info,
|
||||
Invite,
|
||||
Inbox,
|
||||
InboxFill,
|
||||
Link,
|
||||
Loader,
|
||||
Lock,
|
||||
@@ -54,23 +49,14 @@ pub enum IconName {
|
||||
PlusChat,
|
||||
PlusCircle,
|
||||
Profile,
|
||||
Reset,
|
||||
Relay,
|
||||
Reply,
|
||||
Refresh,
|
||||
Scan,
|
||||
Search,
|
||||
Settings,
|
||||
Settings2,
|
||||
Sun,
|
||||
Ship,
|
||||
Shield,
|
||||
Group,
|
||||
Hashtag,
|
||||
History,
|
||||
UserKey,
|
||||
Upload,
|
||||
Usb,
|
||||
Edit,
|
||||
PanelLeft,
|
||||
PanelLeftOpen,
|
||||
@@ -84,9 +70,6 @@ pub enum IconName {
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
WindowRestore,
|
||||
Fistbump,
|
||||
FistbumpFill,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
@@ -95,13 +78,11 @@ impl IconName {
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::ArrowDownCircle => "icons/arrow-down-circle.svg",
|
||||
Self::Boom => "icons/boom.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::CaretUp => "icons/caret-up.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
@@ -109,17 +90,14 @@ impl IconName {
|
||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||
Self::Compass => "icons/compass.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::Device => "icons/device.svg",
|
||||
Self::Door => "icons/door.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::Folder => "icons/folder.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::InboxFill => "icons/inbox-fill.svg",
|
||||
Self::Link => "icons/link.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Lock => "icons/lock.svg",
|
||||
@@ -131,23 +109,14 @@ impl IconName {
|
||||
Self::PlusChat => "icons/plus-chat.svg",
|
||||
Self::PlusCircle => "icons/plus-circle.svg",
|
||||
Self::Profile => "icons/profile.svg",
|
||||
Self::Reset => "icons/reset.svg",
|
||||
Self::Relay => "icons/relay.svg",
|
||||
Self::Reply => "icons/reply.svg",
|
||||
Self::Refresh => "icons/refresh.svg",
|
||||
Self::Scan => "icons/scan.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::Settings2 => "icons/settings2.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Ship => "icons/ship.svg",
|
||||
Self::Shield => "icons/shield.svg",
|
||||
Self::UserKey => "icons/user-key.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::Usb => "icons/usb.svg",
|
||||
Self::Group => "icons/group.svg",
|
||||
Self::Hashtag => "icons/hashtag.svg",
|
||||
Self::History => "icons/history.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
@@ -161,9 +130,6 @@ impl IconName {
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
Self::Fistbump => "icons/fistbump.svg",
|
||||
Self::FistbumpFill => "icons/fistbump-fill.svg",
|
||||
Self::Zoom => "icons/zoom.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -188,7 +154,6 @@ pub struct Icon {
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Option<Size>,
|
||||
rotation: Option<Radians>,
|
||||
}
|
||||
|
||||
impl Default for Icon {
|
||||
@@ -199,7 +164,6 @@ impl Default for Icon {
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: None,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +172,6 @@ impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
this.style = self.style.clone();
|
||||
this.rotation = self.rotation;
|
||||
this.size = self.size;
|
||||
this.text_color = self.text_color;
|
||||
this
|
||||
@@ -224,9 +187,7 @@ impl Icon {
|
||||
Self::default().path(name.path())
|
||||
}
|
||||
|
||||
/// Set the icon path of the Assets bundle
|
||||
///
|
||||
/// For example: `icons/foo.svg`
|
||||
/// Sets the icon path, relative to the assets bundle (for example `icons/foo.svg`).
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = path.into();
|
||||
self
|
||||
@@ -291,7 +252,7 @@ impl From<Icon> for AnyElement {
|
||||
|
||||
impl Render for Icon {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||
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();
|
||||
|
||||
@@ -309,8 +270,5 @@ impl Render for Icon {
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path.clone())
|
||||
.when_some(self.rotation, |this, rotation| {
|
||||
this.with_transformation(Transformation::rotate(rotation))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,10 +1,11 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _,
|
||||
Transformation, Window, div, ease_in_out, percentage,
|
||||
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)]
|
||||
@@ -15,6 +16,12 @@ pub struct Indicator {
|
||||
color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl Default for Indicator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -31,12 +38,6 @@ impl Indicator {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Indicator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Indicator {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
@@ -45,16 +46,14 @@ impl Sizable for Indicator {
|
||||
}
|
||||
|
||||
impl RenderOnce for Indicator {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
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))
|
||||
.with_animation(
|
||||
"circle",
|
||||
Animation::new(self.speed).repeat().with_easing(ease_in_out),
|
||||
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.transform(Transformation::rotate(percentage(phase))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ pub(crate) fn clear_button(cx: &App) -> Button {
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.tab_stop(false)
|
||||
.text_color(cx.theme().icon_muted)
|
||||
.text_color(cx.theme().text_faint)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, DefiniteLength, Edges, Entity, Hsla, InteractiveElement as _, IntoElement,
|
||||
MouseButton, ParentElement as _, Pixels, Rems, RenderOnce, StyleRefinement, Styled, TextAlign,
|
||||
Window, div, px,
|
||||
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;
|
||||
use theme::{ActiveTheme, button_radius};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::indicator::Indicator;
|
||||
use crate::input::clear_button;
|
||||
use crate::{IconName, Selectable, Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||
use crate::{Sizable, Size, StyleSized, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn primary_paste_offset(
|
||||
@@ -45,35 +44,27 @@ fn primary_paste_offset(
|
||||
/// 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_background
|
||||
cx.theme().surface
|
||||
} else {
|
||||
cx.theme().elevated_surface_background
|
||||
cx.theme().input_bg
|
||||
}
|
||||
}
|
||||
|
||||
/// The colors base paints input text with, read from the coop theme.
|
||||
///
|
||||
/// Base fills in any color left transparent from its own palette, and that
|
||||
/// palette is only a projection of this one, so every color coop paints with is
|
||||
/// named here rather than left to resolve.
|
||||
fn input_editor_style(cx: &App) -> InputEditorStyle {
|
||||
let theme = cx.theme();
|
||||
InputEditorStyle {
|
||||
foreground: theme.text,
|
||||
muted_foreground: theme.text_muted,
|
||||
background: theme.elevated_surface_background,
|
||||
border: theme.border,
|
||||
muted_foreground: theme.text_faint,
|
||||
background: theme.input_bg,
|
||||
border: theme.border_faint,
|
||||
selection: theme.selection,
|
||||
caret: theme.cursor,
|
||||
caret: theme.caret,
|
||||
..InputEditorStyle::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The input's own padding, resolved to pixels.
|
||||
///
|
||||
/// Base applies the multi-line padding itself so that the text, the gutter, and
|
||||
/// the scrollbar share one inset, and the single-line frame carries its own.
|
||||
/// Both come from the same size table, resolved through the window's rem size.
|
||||
fn input_paddings(size: Size, style: &StyleRefinement, window: &Window) -> Edges<Pixels> {
|
||||
let mut probe = div().input_px(size).input_py(size).refine_style(style);
|
||||
let padding = probe.style().padding.clone();
|
||||
@@ -94,23 +85,14 @@ fn input_paddings(size: Size, style: &StyleRefinement, window: &Window) -> Edges
|
||||
}
|
||||
|
||||
/// A text input element bound to an [`InputState`] or a [`TextareaState`].
|
||||
///
|
||||
/// The editing kind lives on the state, so `Input::new` accepts either and
|
||||
/// infers which one is rendered.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Input<M: InputModeKind = InputMode> {
|
||||
state: Entity<InputBaseState<M>>,
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
height: Option<DefiniteLength>,
|
||||
appearance: bool,
|
||||
cleanable: bool,
|
||||
mask_toggle: bool,
|
||||
disabled: bool,
|
||||
tab_index: isize,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
/// A styled multi-line text input.
|
||||
@@ -123,17 +105,6 @@ impl<M: InputModeKind> Sizable for Input<M> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: InputModeKind> Selectable for Input<M> {
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: InputModeKind> Input<M> {
|
||||
/// Create a new [`Input`] element bind to the given state.
|
||||
pub fn new(state: &Entity<InputBaseState<M>>) -> Self {
|
||||
@@ -141,28 +112,12 @@ impl<M: InputModeKind> Input<M> {
|
||||
state: state.clone(),
|
||||
size: Size::default(),
|
||||
style: StyleRefinement::default(),
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
height: None,
|
||||
appearance: true,
|
||||
cleanable: false,
|
||||
mask_toggle: false,
|
||||
disabled: false,
|
||||
tab_index: 0,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -180,18 +135,6 @@ impl<M: InputModeKind> Input<M> {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_toggle_mask_button(state: &Entity<InputBaseState<M>>) -> impl IntoElement {
|
||||
Button::new("toggle-mask")
|
||||
.icon(IconName::Eye)
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.tab_stop(false)
|
||||
.on_click({
|
||||
let state = state.clone();
|
||||
move |_, window, cx| state.update(cx, |state, cx| state.toggle_masked(window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: InputModeKind> Styled for Input<M> {
|
||||
@@ -235,10 +178,8 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
||||
let background = input_background(disabled, cx);
|
||||
let show_clear_button =
|
||||
self.cleanable && state.is_editable() && !loading && !text_is_empty && !multi_line;
|
||||
let has_suffix = self.suffix.is_some() || loading || self.mask_toggle || show_clear_button;
|
||||
let has_suffix = loading || show_clear_button;
|
||||
|
||||
let prefix = self.prefix;
|
||||
let suffix = self.suffix;
|
||||
let state_entity = self.state.clone();
|
||||
|
||||
InputBase::new(("input", self.state.entity_id()))
|
||||
@@ -294,19 +235,15 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
||||
},
|
||||
)
|
||||
.items_center()
|
||||
.when(multi_line, |this| {
|
||||
this.h_auto()
|
||||
.when_some(self.height, |this, height| this.h(height))
|
||||
})
|
||||
.when(multi_line, |this| this.h_auto())
|
||||
.when(self.appearance, |this| {
|
||||
this.bg(background)
|
||||
.when(self.disabled, |this| this.opacity(0.5))
|
||||
.rounded(cx.theme().radius)
|
||||
.rounded(px(button_radius()))
|
||||
})
|
||||
.tab_index(self.tab_index)
|
||||
.tab_index(0)
|
||||
.gap(gap_x)
|
||||
.refine_style(&self.style)
|
||||
.children(prefix)
|
||||
.when(!multi_line, |this| this.child(state_entity.clone()))
|
||||
.when(multi_line, |this| {
|
||||
this.child(
|
||||
@@ -322,9 +259,6 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
||||
.gap(gap_x)
|
||||
.items_center()
|
||||
.when(loading, |this| this.child(Indicator::new()))
|
||||
.when(self.mask_toggle, |this| {
|
||||
this.child(Self::render_toggle_mask_button(&state_entity))
|
||||
})
|
||||
.when(show_clear_button, |this| {
|
||||
this.child(clear_button(cx).on_click({
|
||||
let state = state_entity.clone();
|
||||
@@ -335,8 +269,7 @@ impl<M: InputModeKind> RenderOnce for Input<M> {
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
.children(suffix),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
+11
-38
@@ -1,29 +1,20 @@
|
||||
use gpui::{
|
||||
Action, AsKeystroke, FocusHandle, IntoElement, KeyContext, Keystroke, ParentElement as _,
|
||||
RenderOnce, StyleRefinement, Styled, Window, div, relative,
|
||||
Action, AsKeystroke, IntoElement, KeyContext, Keystroke, ParentElement as _, RenderOnce,
|
||||
StyleRefinement, Styled, Window, div,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::StyledExt;
|
||||
|
||||
/// A key binding tag
|
||||
#[derive(IntoElement, Clone, Debug)]
|
||||
pub struct Kbd {
|
||||
pub(crate) struct Kbd {
|
||||
style: StyleRefinement,
|
||||
stroke: Keystroke,
|
||||
}
|
||||
|
||||
impl From<Keystroke> for Kbd {
|
||||
fn from(stroke: Keystroke) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
stroke,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Kbd {
|
||||
pub fn new(stroke: Keystroke) -> Self {
|
||||
fn new(stroke: Keystroke) -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
stroke,
|
||||
@@ -31,7 +22,7 @@ impl Kbd {
|
||||
}
|
||||
|
||||
/// Return the first keybinding for the given action and context.
|
||||
pub fn binding_for_action(
|
||||
pub(crate) fn binding_for_action(
|
||||
action: &dyn Action,
|
||||
context: Option<&str>,
|
||||
window: &Window,
|
||||
@@ -50,25 +41,8 @@ impl Kbd {
|
||||
.map(|key| Self::new(key.as_keystroke().clone()))
|
||||
}
|
||||
|
||||
/// Return the first keybinding for the given action and focus handle.
|
||||
pub fn binding_for_action_in(
|
||||
action: &dyn Action,
|
||||
focus_handle: &FocusHandle,
|
||||
window: &Window,
|
||||
) -> Option<Self> {
|
||||
let binding = window.highest_precedence_binding_for_action_in(action, focus_handle)?;
|
||||
|
||||
binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.map(|key| Self::new(key.as_keystroke().clone()))
|
||||
}
|
||||
|
||||
/// Return the Platform specific keybinding string by KeyStroke
|
||||
///
|
||||
/// - macOS: https://support.apple.com/en-us/HT201236
|
||||
/// - Windows: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec
|
||||
pub fn format(key: &Keystroke) -> String {
|
||||
/// 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"))]
|
||||
@@ -197,16 +171,15 @@ 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)
|
||||
.border_color(cx.theme().border_faint)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.bg(cx.theme().surface_background)
|
||||
.bg(cx.theme().surface_raised)
|
||||
.py_0p5()
|
||||
.px_1()
|
||||
.min_w_5()
|
||||
.text_center()
|
||||
.rounded_sm()
|
||||
.line_height(relative(1.))
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.whitespace_normal()
|
||||
.flex_shrink_0()
|
||||
.refine_style(&self.style)
|
||||
|
||||
@@ -23,9 +23,9 @@ pub mod switch;
|
||||
pub mod tab;
|
||||
pub mod tooltip;
|
||||
|
||||
mod animation;
|
||||
mod icon;
|
||||
mod kbd;
|
||||
pub mod motion;
|
||||
mod popover;
|
||||
mod resizable;
|
||||
mod root;
|
||||
|
||||
+18
-14
@@ -1,7 +1,8 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, IntoElement, SharedString, StyleRefinement, Styled as _, Window,
|
||||
AnyElement, App, ElementId, HighlightStyle, IntoElement, SharedString, StyleRefinement,
|
||||
Styled as _, Window,
|
||||
};
|
||||
use gpui_base::{TextView, TextViewStyle};
|
||||
use theme::ActiveTheme as _;
|
||||
@@ -62,15 +63,28 @@ impl RenderedText {
|
||||
/// 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(code_font_family());
|
||||
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)
|
||||
.with_link(theme.text_accent)
|
||||
.with_code_background(theme.elevated_surface_background)
|
||||
// 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()),
|
||||
@@ -101,13 +115,3 @@ fn is_web_url(url: &str) -> bool {
|
||||
let url = url.to_ascii_lowercase();
|
||||
url.starts_with("http://") || url.starts_with("https://")
|
||||
}
|
||||
|
||||
fn code_font_family() -> &'static str {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Menlo"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Consolas"
|
||||
} else {
|
||||
"monospace"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner for the dropdown menu popover.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style refinement for the dropdown menu trigger.
|
||||
fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.style = style;
|
||||
@@ -88,13 +82,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a [`PopupMenu`] when its child is clicked with a mouse button
|
||||
/// (right by default), keeping the child's own click handler intact.
|
||||
/// Opens a [`PopupMenu`] when its child is right-clicked, keeping the child's own click handler.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextMenu {
|
||||
id: ElementId,
|
||||
anchor: Anchor,
|
||||
mouse_button: MouseButton,
|
||||
child: AnyElement,
|
||||
builder: Rc<MenuBuilder>,
|
||||
}
|
||||
@@ -107,24 +98,10 @@ impl ContextMenu {
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
anchor: Anchor::TopLeft,
|
||||
mouse_button: MouseButton::Right,
|
||||
child: child.into_any_element(),
|
||||
builder: Rc::new(builder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner of the menu, default is `Anchor::TopLeft`.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the mouse button that opens the menu, default is `MouseButton::Right`.
|
||||
pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
|
||||
self.mouse_button = mouse_button;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -132,10 +109,7 @@ struct MenuState {
|
||||
menu: Option<Entity<PopupMenu>>,
|
||||
}
|
||||
|
||||
/// Builds the menu once and reuses it until it is dismissed.
|
||||
///
|
||||
/// The popover content closure runs on every render, so rebuilding the menu
|
||||
/// entity each time would drop its focus and selection state.
|
||||
/// Builds the menu once and reuses it until dismissed, preserving focus and selection state.
|
||||
fn cached_menu(
|
||||
menu_state: &Entity<MenuState>,
|
||||
builder: Rc<MenuBuilder>,
|
||||
@@ -196,8 +170,8 @@ impl RenderOnce for ContextMenu {
|
||||
Popover::new(SharedString::from(format!("context-menu:{}", self.id)))
|
||||
.appearance(false)
|
||||
.overlay_closable(false)
|
||||
.anchor(self.anchor)
|
||||
.mouse_button(self.mouse_button)
|
||||
.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))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Disableable, StyledExt, h_flex};
|
||||
|
||||
@@ -39,18 +39,12 @@ impl MenuItemElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set ListItem as the selected item style.
|
||||
/// Set the selected style of the item.
|
||||
pub(crate) fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the disabled state of the MenuItem.
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a handler for when the MenuItem is clicked.
|
||||
pub(crate) fn on_click(
|
||||
mut self,
|
||||
@@ -61,7 +55,10 @@ impl MenuItemElement {
|
||||
}
|
||||
|
||||
/// Set a handler for when the mouse enters the MenuItem.
|
||||
pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
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
|
||||
}
|
||||
@@ -93,7 +90,7 @@ impl RenderOnce for MenuItemElement {
|
||||
.group(&self.group_name)
|
||||
.gap_x_1()
|
||||
.p_1()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
@@ -104,12 +101,12 @@ impl RenderOnce for MenuItemElement {
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.group_hover(self.group_name, |this| {
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
this.bg(cx.theme().element_hover)
|
||||
.text_color(cx.theme().text)
|
||||
})
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
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| {
|
||||
@@ -118,7 +115,7 @@ impl RenderOnce for MenuItemElement {
|
||||
.on_click(on_click)
|
||||
})
|
||||
})
|
||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
||||
.when(self.disabled, |this| this.text_color(cx.theme().text_faint))
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, Anchor, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, DismissEvent,
|
||||
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
||||
KeyBinding, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored, div, px, rems,
|
||||
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};
|
||||
use theme::{ActiveTheme, Side, TextStyle, Typeset as _};
|
||||
|
||||
use crate::kbd::Kbd;
|
||||
use crate::menu::menu_item::MenuItemElement;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{ElementExt, Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
use crate::{Disableable, ElementExt, Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "PopupMenu";
|
||||
const MAX_WIDTH: Pixels = px(500.);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||
@@ -28,7 +28,7 @@ pub fn init(cx: &mut App) {
|
||||
]);
|
||||
}
|
||||
|
||||
/// An menu item in a popup menu.
|
||||
/// A menu item in a popup menu.
|
||||
pub enum PopupMenuItem {
|
||||
/// A menu separator item.
|
||||
Separator,
|
||||
@@ -40,26 +40,11 @@ pub enum PopupMenuItem {
|
||||
label: SharedString,
|
||||
disabled: bool,
|
||||
checked: bool,
|
||||
is_link: bool,
|
||||
action: Option<Box<dyn Action>>,
|
||||
// For link item
|
||||
#[allow(clippy::type_complexity)]
|
||||
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
},
|
||||
/// A menu item with custom element render.
|
||||
ElementItem {
|
||||
icon: Option<Icon>,
|
||||
disabled: bool,
|
||||
checked: bool,
|
||||
action: Option<Box<dyn Action>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
},
|
||||
/// A submenu item that opens another popup menu.
|
||||
///
|
||||
/// NOTE: This is only supported when the parent menu is not `scrollable`.
|
||||
Submenu {
|
||||
icon: Option<Icon>,
|
||||
label: SharedString,
|
||||
@@ -80,14 +65,13 @@ impl PopupMenuItem {
|
||||
disabled: false,
|
||||
checked: false,
|
||||
action: None,
|
||||
is_link: false,
|
||||
handler: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new submenu item that opens another popup menu.
|
||||
#[inline]
|
||||
pub fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
|
||||
fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
|
||||
PopupMenuItem::Submenu {
|
||||
icon: None,
|
||||
label: label.into(),
|
||||
@@ -98,27 +82,22 @@ impl PopupMenuItem {
|
||||
|
||||
/// Create a separator menu item.
|
||||
#[inline]
|
||||
pub fn separator() -> Self {
|
||||
fn separator() -> Self {
|
||||
PopupMenuItem::Separator
|
||||
}
|
||||
|
||||
/// Creates a label menu item.
|
||||
/// Create a label menu item.
|
||||
#[inline]
|
||||
pub fn label(label: impl Into<SharedString>) -> Self {
|
||||
fn label(label: impl Into<SharedString>) -> Self {
|
||||
PopupMenuItem::Label(label.into())
|
||||
}
|
||||
|
||||
/// Set the icon for the menu item.
|
||||
///
|
||||
/// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
match &mut self {
|
||||
PopupMenuItem::Item { icon: i, .. } => {
|
||||
*i = Some(icon.into());
|
||||
}
|
||||
PopupMenuItem::ElementItem { icon: i, .. } => {
|
||||
*i = Some(icon.into());
|
||||
}
|
||||
PopupMenuItem::Submenu { icon: i, .. } => {
|
||||
*i = Some(icon.into());
|
||||
}
|
||||
@@ -128,32 +107,19 @@ impl PopupMenuItem {
|
||||
}
|
||||
|
||||
/// Set the action for the menu item.
|
||||
///
|
||||
/// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
|
||||
pub fn action(mut self, action: Box<dyn Action>) -> Self {
|
||||
match &mut self {
|
||||
PopupMenuItem::Item { action: a, .. } => {
|
||||
*a = Some(action);
|
||||
}
|
||||
PopupMenuItem::ElementItem { action: a, .. } => {
|
||||
*a = Some(action);
|
||||
}
|
||||
_ => {}
|
||||
fn action(mut self, action: Box<dyn Action>) -> Self {
|
||||
if let PopupMenuItem::Item { action: a, .. } = &mut self {
|
||||
*a = Some(action);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the disabled state for the menu item.
|
||||
///
|
||||
/// Only works for [`PopupMenuItem::Item`], [`PopupMenuItem::ElementItem`] and [`PopupMenuItem::Submenu`].
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
match &mut self {
|
||||
PopupMenuItem::Item { disabled: d, .. } => {
|
||||
*d = disabled;
|
||||
}
|
||||
PopupMenuItem::ElementItem { disabled: d, .. } => {
|
||||
*d = disabled;
|
||||
}
|
||||
PopupMenuItem::Submenu { disabled: d, .. } => {
|
||||
*d = disabled;
|
||||
}
|
||||
@@ -162,57 +128,37 @@ impl PopupMenuItem {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set checked state for the menu item.
|
||||
///
|
||||
/// NOTE: If `check_side` is [`Side::Left`], the icon will replace with a check icon.
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
match &mut self {
|
||||
PopupMenuItem::Item { checked: c, .. } => {
|
||||
*c = checked;
|
||||
}
|
||||
PopupMenuItem::ElementItem { checked: c, .. } => {
|
||||
*c = checked;
|
||||
}
|
||||
_ => {}
|
||||
/// Set the checked state for the menu item.
|
||||
fn checked(mut self, checked: bool) -> Self {
|
||||
if let PopupMenuItem::Item { checked: c, .. } = &mut self {
|
||||
*c = checked;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a click handler for the menu item.
|
||||
///
|
||||
/// Only works for [`PopupMenuItem::Item`] and [`PopupMenuItem::ElementItem`].
|
||||
pub fn on_click<F>(mut self, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
match &mut self {
|
||||
PopupMenuItem::Item { handler: h, .. } => {
|
||||
*h = Some(Rc::new(handler));
|
||||
}
|
||||
PopupMenuItem::ElementItem { handler: h, .. } => {
|
||||
*h = Some(Rc::new(handler));
|
||||
}
|
||||
_ => {}
|
||||
if let PopupMenuItem::Item { handler: h, .. } = &mut self {
|
||||
*h = Some(Rc::new(handler));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_clickable(&self) -> bool {
|
||||
!matches!(self, PopupMenuItem::Separator)
|
||||
&& matches!(
|
||||
self,
|
||||
PopupMenuItem::Item {
|
||||
disabled: false,
|
||||
..
|
||||
} | PopupMenuItem::ElementItem {
|
||||
disabled: false,
|
||||
..
|
||||
} | PopupMenuItem::Submenu {
|
||||
disabled: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
matches!(
|
||||
self,
|
||||
PopupMenuItem::Item {
|
||||
disabled: false,
|
||||
..
|
||||
} | PopupMenuItem::Submenu {
|
||||
disabled: false,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -220,14 +166,9 @@ impl PopupMenuItem {
|
||||
matches!(self, PopupMenuItem::Separator)
|
||||
}
|
||||
|
||||
fn has_left_icon(&self, check_side: Side) -> bool {
|
||||
fn has_left_icon(&self) -> bool {
|
||||
match self {
|
||||
PopupMenuItem::Item { icon, checked, .. } => {
|
||||
icon.is_some() || (check_side.is_left() && *checked)
|
||||
}
|
||||
PopupMenuItem::ElementItem { icon, checked, .. } => {
|
||||
icon.is_some() || (check_side.is_left() && *checked)
|
||||
}
|
||||
PopupMenuItem::Item { icon, checked, .. } => icon.is_some() || *checked,
|
||||
PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
@@ -235,66 +176,38 @@ impl PopupMenuItem {
|
||||
|
||||
#[inline]
|
||||
fn is_checked(&self) -> bool {
|
||||
match self {
|
||||
PopupMenuItem::Item { checked, .. } => *checked,
|
||||
PopupMenuItem::ElementItem { checked, .. } => *checked,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, PopupMenuItem::Item { checked: true, .. })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopupMenu {
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
pub(crate) menu_items: Vec<PopupMenuItem>,
|
||||
|
||||
/// The focus handle of Entity to handle actions.
|
||||
pub(crate) action_context: Option<FocusHandle>,
|
||||
|
||||
focus_handle: FocusHandle,
|
||||
menu_items: Vec<PopupMenuItem>,
|
||||
axis: Axis,
|
||||
selected_index: Option<usize>,
|
||||
min_width: Option<Pixels>,
|
||||
max_width: Option<Pixels>,
|
||||
max_height: Option<Pixels>,
|
||||
bounds: Bounds<Pixels>,
|
||||
size: Size,
|
||||
check_side: Side,
|
||||
|
||||
/// The parent menu of this menu, if this is a submenu
|
||||
parent_menu: Option<WeakEntity<Self>>,
|
||||
scrollable: bool,
|
||||
external_link_icon: bool,
|
||||
scroll_handle: ScrollHandle,
|
||||
|
||||
/// This will update on render
|
||||
submenu_anchor: (Anchor, Pixels),
|
||||
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl PopupMenu {
|
||||
pub(crate) fn new(cx: &mut App) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
action_context: None,
|
||||
parent_menu: None,
|
||||
menu_items: Vec::new(),
|
||||
selected_index: None,
|
||||
axis: Axis::Vertical,
|
||||
min_width: None,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
check_side: Side::Left,
|
||||
bounds: Bounds::default(),
|
||||
scrollable: false,
|
||||
scroll_handle: ScrollHandle::default(),
|
||||
external_link_icon: true,
|
||||
size: Size::default(),
|
||||
submenu_anchor: (Anchor::TopLeft, Pixels::ZERO),
|
||||
_subscriptions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
pub(crate) fn build(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
|
||||
@@ -302,45 +215,15 @@ impl PopupMenu {
|
||||
cx.new(|cx| f(Self::new(cx), window, cx))
|
||||
}
|
||||
|
||||
/// Set min width of the popup menu, default is 120px
|
||||
pub fn min_w(mut self, width: impl Into<Pixels>) -> Self {
|
||||
self.min_width = Some(width.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set max height of the popup menu, default is half of the window height
|
||||
pub fn max_h(mut self, height: impl Into<Pixels>) -> Self {
|
||||
self.max_height = Some(height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the axis of children to horizontal.
|
||||
pub fn horizontal(mut self) -> Self {
|
||||
self.axis = Axis::Horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the menu to be scrollable to show vertical scrollbar.
|
||||
///
|
||||
/// NOTE: If this is true, the sub-menus will cannot be support.
|
||||
pub fn scrollable(mut self, scrollable: bool) -> Self {
|
||||
self.scrollable = scrollable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add Menu Item
|
||||
pub fn menu(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
self.menu_with_disabled(label, action, false)
|
||||
}
|
||||
|
||||
/// Add Menu Item with disabled state
|
||||
pub fn menu_with_disabled(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Box<dyn Action>,
|
||||
disabled: bool,
|
||||
) -> Self {
|
||||
self.add_menu_item(label, None, action, disabled, false);
|
||||
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
||||
self.add_menu_item(label, None, action, false, false);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -399,19 +282,7 @@ impl PopupMenu {
|
||||
|
||||
/// Add a Submenu
|
||||
pub fn submenu(
|
||||
self,
|
||||
label: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Self {
|
||||
self.submenu_with_icon(None, label, window, cx, f)
|
||||
}
|
||||
|
||||
/// Add a Submenu item with icon
|
||||
pub fn submenu_with_icon(
|
||||
mut self,
|
||||
icon: Option<Icon>,
|
||||
label: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -423,9 +294,7 @@ impl PopupMenu {
|
||||
view.parent_menu = Some(parent_menu);
|
||||
});
|
||||
|
||||
self.menu_items.push(
|
||||
PopupMenuItem::submenu(label, submenu).when_some(icon, |this, icon| this.icon(icon)),
|
||||
);
|
||||
self.menu_items.push(PopupMenuItem::submenu(label, submenu));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -436,13 +305,6 @@ impl PopupMenu {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use small size, the menu item will have smaller height.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn small(mut self) -> Self {
|
||||
self.size = Size::Small;
|
||||
self
|
||||
}
|
||||
|
||||
fn add_menu_item(
|
||||
&mut self,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -461,7 +323,7 @@ impl PopupMenu {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
||||
fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
||||
if let Some(ix) = self.selected_index
|
||||
&& let Some(item) = self.menu_items.get(ix)
|
||||
{
|
||||
@@ -489,52 +351,24 @@ impl PopupMenu {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(index) = self.selected_index {
|
||||
let item = self.menu_items.get(index);
|
||||
match item {
|
||||
Some(PopupMenuItem::Item {
|
||||
handler, action, ..
|
||||
}) => {
|
||||
if let Some(handler) = handler {
|
||||
handler(&ClickEvent::default(), window, cx);
|
||||
} else if let Some(action) = action.as_ref() {
|
||||
self.dispatch_confirm_action(action.as_ref(), window, cx);
|
||||
}
|
||||
|
||||
self.dismiss(&Cancel, window, cx)
|
||||
}
|
||||
Some(PopupMenuItem::ElementItem {
|
||||
handler, action, ..
|
||||
}) => {
|
||||
if let Some(handler) = handler {
|
||||
handler(&ClickEvent::default(), window, cx);
|
||||
} else if let Some(action) = action.as_ref() {
|
||||
self.dispatch_confirm_action(action.as_ref(), window, cx);
|
||||
}
|
||||
self.dismiss(&Cancel, window, cx)
|
||||
}
|
||||
_ => {}
|
||||
if let Some(index) = self.selected_index
|
||||
&& let Some(PopupMenuItem::Item {
|
||||
handler, action, ..
|
||||
}) = self.menu_items.get(index)
|
||||
{
|
||||
if let Some(handler) = handler {
|
||||
handler(&ClickEvent::default(), window, cx);
|
||||
} else if let Some(action) = action.as_ref() {
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_confirm_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(context) = self.action_context.as_ref() {
|
||||
context.focus(window, cx);
|
||||
self.dismiss(&Cancel, window, cx)
|
||||
}
|
||||
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
if self.selected_index != Some(ix) {
|
||||
self.selected_index = Some(ix);
|
||||
self.scroll_handle.scroll_to_item(ix);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -675,6 +509,7 @@ impl PopupMenu {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::only_used_in_recursion)]
|
||||
fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.active_submenu().is_some() {
|
||||
return;
|
||||
@@ -682,20 +517,17 @@ impl PopupMenu {
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
// Focus back to the previous focused handle.
|
||||
if let Some(action_context) = self.action_context.as_ref() {
|
||||
window.focus(action_context, cx);
|
||||
}
|
||||
|
||||
let Some(parent_menu) = self.parent_menu.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Dismiss parent menu, when this menu is dismissed
|
||||
_ = parent_menu.update(cx, |view, cx| {
|
||||
if let Err(error) = parent_menu.update(cx, |view, cx| {
|
||||
view.selected_index = None;
|
||||
view.dismiss(&Cancel, window, cx);
|
||||
});
|
||||
}) {
|
||||
log::warn!("Failed to dismiss parent menu: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_dismiss(
|
||||
@@ -732,16 +564,7 @@ impl PopupMenu {
|
||||
) -> Option<Kbd> {
|
||||
let action = action?;
|
||||
|
||||
match self
|
||||
.action_context
|
||||
.as_ref()
|
||||
.and_then(|handle| Kbd::binding_for_action_in(action.as_ref(), handle, window))
|
||||
{
|
||||
Some(kbd) => Some(kbd),
|
||||
// Fallback to App level key binding
|
||||
None => Kbd::binding_for_action(action.as_ref(), None, window),
|
||||
}
|
||||
.map(|this| {
|
||||
Kbd::binding_for_action(action.as_ref(), None, window).map(|this| {
|
||||
this.p_0()
|
||||
.flex_nowrap()
|
||||
.border_0()
|
||||
@@ -754,7 +577,7 @@ impl PopupMenu {
|
||||
checked: bool,
|
||||
icon: Option<Icon>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if !has_icon {
|
||||
return None;
|
||||
@@ -763,7 +586,7 @@ impl PopupMenu {
|
||||
let icon = if let Some(icon) = icon {
|
||||
icon.clone()
|
||||
} else if checked {
|
||||
Icon::new(IconName::Check)
|
||||
Icon::new(IconName::Check).text_color(cx.theme().accent)
|
||||
} else {
|
||||
Icon::empty()
|
||||
};
|
||||
@@ -771,16 +594,10 @@ impl PopupMenu {
|
||||
Some(icon)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn max_width(&self) -> Pixels {
|
||||
self.max_width.unwrap_or(px(500.))
|
||||
}
|
||||
|
||||
/// Calculate the anchor corner and left offset for child submenu
|
||||
fn update_submenu_menu_anchor(&mut self, window: &Window) {
|
||||
let bounds = self.bounds;
|
||||
let max_width = self.max_width();
|
||||
let (anchor, left) = if max_width + bounds.origin.x > window.bounds().size.width {
|
||||
let (anchor, left) = if MAX_WIDTH + bounds.origin.x > window.bounds().size.width {
|
||||
(Anchor::TopRight, -px(16.))
|
||||
} else {
|
||||
(Anchor::TopLeft, bounds.size.width - px(8.))
|
||||
@@ -803,12 +620,7 @@ impl PopupMenu {
|
||||
cx: &mut Context<Self>,
|
||||
) -> MenuItemElement {
|
||||
let has_left_icon = options.has_left_icon;
|
||||
let is_left_check = options.check_side.is_left() && item.is_checked();
|
||||
let right_check_icon = if options.check_side.is_right() && item.is_checked() {
|
||||
Some(Icon::new(IconName::Check).xsmall())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let is_checked = item.is_checked();
|
||||
|
||||
let selected = self.selected_index == Some(ix);
|
||||
const EDGE_PADDING: Pixels = px(4.);
|
||||
@@ -817,17 +629,14 @@ impl PopupMenu {
|
||||
let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
|
||||
let group_name = format!("{}:item-{}", cx.entity().entity_id(), ix);
|
||||
|
||||
let (item_height, radius) = match self.size {
|
||||
Size::Small => (px(20.), options.radius.half()),
|
||||
_ => (px(26.), options.radius),
|
||||
};
|
||||
let item_height = px(26.);
|
||||
|
||||
let this = MenuItemElement::new(ix, &group_name)
|
||||
.relative()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.py_0()
|
||||
.px(INNER_PADDING)
|
||||
.rounded(radius)
|
||||
.rounded(options.radius)
|
||||
.items_center()
|
||||
.selected(selected)
|
||||
.on_hover(cx.listener(move |this, hovered, _, cx| {
|
||||
@@ -848,45 +657,25 @@ impl PopupMenu {
|
||||
.my_0p5()
|
||||
.mx_neg_1()
|
||||
.border_b(px(2.))
|
||||
.border_color(cx.theme().border)
|
||||
.border_color(cx.theme().border_faint)
|
||||
.disabled(true),
|
||||
PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
|
||||
h_flex().cursor_default().items_center().gap_x_1().child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(label.clone()),
|
||||
),
|
||||
),
|
||||
PopupMenuItem::ElementItem {
|
||||
render, disabled, ..
|
||||
} => this
|
||||
.when(!disabled, |this| {
|
||||
this.on_click(
|
||||
cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
|
||||
)
|
||||
})
|
||||
.disabled(*disabled)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_h(item_height)
|
||||
.items_center()
|
||||
.gap_x_2()
|
||||
.child((render)(window, cx))
|
||||
.children(right_check_icon.map(|icon| icon.ml_3())),
|
||||
),
|
||||
PopupMenuItem::Item {
|
||||
icon,
|
||||
label,
|
||||
action,
|
||||
disabled,
|
||||
is_link,
|
||||
..
|
||||
} => {
|
||||
let show_link_icon = *is_link && self.external_link_icon;
|
||||
let action = action.as_ref().map(|action| action.boxed_clone());
|
||||
let key = self.render_key_binding(action, window, cx);
|
||||
|
||||
@@ -897,10 +686,10 @@ impl PopupMenu {
|
||||
})
|
||||
.disabled(*disabled)
|
||||
.h(item_height)
|
||||
.gap_x_2()
|
||||
.gap_x(px(theme::SPACE))
|
||||
.children(Self::render_icon(
|
||||
has_left_icon,
|
||||
is_left_check,
|
||||
is_checked,
|
||||
icon.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -911,22 +700,7 @@ impl PopupMenu {
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.when(!show_link_icon, |this| this.child(label.clone()))
|
||||
.children(right_check_icon)
|
||||
.when(show_link_icon, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(label.clone())
|
||||
.child(
|
||||
Icon::new(IconName::Link)
|
||||
.xsmall()
|
||||
.text_color(cx.theme().text_muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(label.clone())
|
||||
.children(key),
|
||||
)
|
||||
}
|
||||
@@ -944,7 +718,7 @@ impl PopupMenu {
|
||||
.min_h(item_height)
|
||||
.size_full()
|
||||
.items_center()
|
||||
.gap_x_2()
|
||||
.gap_x(px(theme::SPACE))
|
||||
.children(Self::render_icon(
|
||||
has_left_icon,
|
||||
false,
|
||||
@@ -955,7 +729,7 @@ impl PopupMenu {
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(label.clone())
|
||||
@@ -1002,7 +776,6 @@ impl Focusable for PopupMenu {
|
||||
#[derive(Clone, Copy)]
|
||||
struct RenderOptions {
|
||||
has_left_icon: bool,
|
||||
check_side: Side,
|
||||
radius: Pixels,
|
||||
}
|
||||
|
||||
@@ -1013,21 +786,12 @@ impl Render for PopupMenu {
|
||||
let view = cx.entity().clone();
|
||||
let items_count = self.menu_items.len();
|
||||
|
||||
let max_width = self.max_width();
|
||||
let max_height = self.max_height.unwrap_or_else(|| {
|
||||
let window_half_height = window.window_bounds().get_bounds().size.height * 0.5;
|
||||
window_half_height.min(px(450.))
|
||||
});
|
||||
|
||||
let has_left_icon = self
|
||||
.menu_items
|
||||
.iter()
|
||||
.any(|item| item.has_left_icon(self.check_side));
|
||||
let has_left_icon = self.menu_items.iter().any(|item| item.has_left_icon());
|
||||
|
||||
let options = RenderOptions {
|
||||
has_left_icon,
|
||||
check_side: self.check_side,
|
||||
radius: cx.theme().radius.min(px(8.)),
|
||||
// Items sit p_1 (4px) inside the surface plate; keep corners concentric.
|
||||
radius: px(theme::inset_radius(theme::surface_radius(), 4.0)),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
@@ -1051,17 +815,11 @@ impl Render for PopupMenu {
|
||||
.p_1()
|
||||
.gap_y_0p5()
|
||||
.min_w(rems(8.))
|
||||
.when_some(self.min_width, |this, min_width| this.min_w(min_width))
|
||||
.max_w(max_width)
|
||||
.max_w(MAX_WIDTH)
|
||||
.map(|this| match self.axis {
|
||||
Axis::Horizontal => this.flex().flex_row().items_center(),
|
||||
Axis::Vertical => this.flex().flex_col(),
|
||||
})
|
||||
.when(self.scrollable, |this| {
|
||||
this.max_h(max_height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
})
|
||||
.children(
|
||||
self.menu_items
|
||||
.iter()
|
||||
@@ -1072,9 +830,5 @@ impl Render for PopupMenu {
|
||||
)
|
||||
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)),
|
||||
)
|
||||
.when(self.scrollable, |this| {
|
||||
// TODO: When the menu is limited by `overflow_y_scroll`, the sub-menu will cannot be displayed.
|
||||
this.vertical_scrollbar(&self.scroll_handle)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{InteractiveElementExt as _, StyledExt as _, h_flex, v_flex};
|
||||
|
||||
@@ -190,9 +190,9 @@ impl RenderOnce for MessageRow {
|
||||
.when(show_author, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.gap(px(theme::SPACE))
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text_dim)
|
||||
.when_some(author, |this, author| {
|
||||
this.child(div().font_semibold().child(author))
|
||||
})
|
||||
@@ -212,7 +212,7 @@ impl RenderOnce for MessageRow {
|
||||
.when_some(on_double_click, |this, listener| {
|
||||
this.on_double_click(listener)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().surface_background))
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,6 @@ pub struct WelcomeMessage {
|
||||
icon: Option<AnyElement>,
|
||||
title: Option<SharedString>,
|
||||
message: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl WelcomeMessage {
|
||||
@@ -237,7 +236,6 @@ impl WelcomeMessage {
|
||||
icon: None,
|
||||
title: None,
|
||||
message: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,13 +259,6 @@ impl WelcomeMessage {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Append an element below the body text.
|
||||
#[must_use]
|
||||
pub fn child(mut self, child: impl IntoElement) -> Self {
|
||||
self.children.push(child.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for WelcomeMessage {
|
||||
@@ -284,13 +275,12 @@ impl RenderOnce for WelcomeMessage {
|
||||
icon,
|
||||
title,
|
||||
message,
|
||||
children,
|
||||
} = self;
|
||||
|
||||
v_flex()
|
||||
.id(id)
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.p_3()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
@@ -305,8 +295,7 @@ impl RenderOnce for WelcomeMessage {
|
||||
.when_some(title, |this, title| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_style(TextStyle::Headline)
|
||||
.text_color(cx.theme().text)
|
||||
.child(title),
|
||||
)
|
||||
@@ -314,12 +303,11 @@ impl RenderOnce for WelcomeMessage {
|
||||
.when_some(message, |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_faint)
|
||||
.child(message),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.children(children)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
//! The motion kit, ported from bezel (`crates/motion/src/lib.rs` and its
|
||||
//! `app.rs`, MIT License, Copyright (c) 2026 bezel contributors). TranslateY
|
||||
//! rides a relative `top` inset, which taffy applies after layout, so
|
||||
//! siblings never move.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationElement, AnimationExt as _, App, Context, ElementId, EntityId, Global,
|
||||
Hsla, IntoElement, Rgba, SharedString, Styled, Window, px,
|
||||
};
|
||||
use instant::Instant;
|
||||
|
||||
/// Redraw rate for anything driven by [`pulse_delta`].
|
||||
const PULSE_FPS: f32 = 30.0;
|
||||
|
||||
/// How long a view stays on the tick list after its last pulse paint.
|
||||
const PULSE_LEASE: Duration = Duration::from_millis(300);
|
||||
|
||||
/// Redraw rate for hover fades; [`HOVER_FADE`] paints in nine steps at it.
|
||||
const HOVER_FPS: f32 = 60.0;
|
||||
|
||||
/// Floor on how long the clock sleeps between wake-ups.
|
||||
const MIN_SLEEP: Duration = Duration::from_millis(1);
|
||||
|
||||
/// One view's claim on the clock.
|
||||
struct Lease {
|
||||
/// The fastest rate anything on this view has claimed.
|
||||
period: Duration,
|
||||
/// When this view is next owed a redraw.
|
||||
due: Instant,
|
||||
/// A notify is out and the render it provoked has not renewed yet.
|
||||
in_flight: bool,
|
||||
/// When the claim lapses if nothing renews it.
|
||||
until: Instant,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PulseClock {
|
||||
epoch: Option<Instant>,
|
||||
leases: HashMap<EntityId, Lease>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl Global for PulseClock {}
|
||||
|
||||
impl PulseClock {
|
||||
fn next_wake(&self) -> Option<Instant> {
|
||||
self.leases
|
||||
.values()
|
||||
.map(|lease| lease.due.min(lease.until))
|
||||
.min()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component's line back to the view that paints it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Painter(EntityId);
|
||||
|
||||
impl Painter {
|
||||
/// The view this render belongs to; take it once where the state is built.
|
||||
pub fn of<T: 'static>(cx: &Context<T>) -> Self {
|
||||
Self(cx.entity_id())
|
||||
}
|
||||
|
||||
/// Redraw this view once.
|
||||
pub fn notify(self, cx: &mut App) {
|
||||
cx.notify(self.0);
|
||||
}
|
||||
|
||||
/// Claim `fps` redraws a second; renew from `render` so an unmounted
|
||||
/// element drops off.
|
||||
pub fn lease(self, fps: f32, until: Duration, cx: &mut App) {
|
||||
lease(self.0, fps, until, cx);
|
||||
}
|
||||
|
||||
/// Whether the clock is what asked for the render running now.
|
||||
pub fn woken(self, cx: &App) -> bool {
|
||||
cx.try_global::<PulseClock>()
|
||||
.and_then(|clock| clock.leases.get(&self.0))
|
||||
.is_some_and(|lease| lease.in_flight)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntityId> for Painter {
|
||||
fn from(entity_id: EntityId) -> Self {
|
||||
Self(entity_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Painter> for EntityId {
|
||||
fn from(painter: Painter) -> Self {
|
||||
painter.0
|
||||
}
|
||||
}
|
||||
|
||||
fn lease(view: EntityId, fps: f32, until: Duration, cx: &mut App) {
|
||||
if cx.pause_when_inactive() && cx.active_window().is_none() {
|
||||
return;
|
||||
}
|
||||
let now = cx.background_executor().now();
|
||||
let period = Duration::from_secs_f32(1.0 / fps.clamp(1.0, 240.0));
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock
|
||||
.leases
|
||||
.entry(view)
|
||||
.and_modify(|lease| {
|
||||
lease.period = lease.period.min(period);
|
||||
lease.due = lease.due.min(now + period);
|
||||
lease.in_flight = false;
|
||||
lease.until = lease.until.max(now + until);
|
||||
})
|
||||
.or_insert(Lease {
|
||||
period,
|
||||
due: now + period,
|
||||
in_flight: false,
|
||||
until: now + until,
|
||||
});
|
||||
if clock.running {
|
||||
return;
|
||||
}
|
||||
clock.running = true;
|
||||
cx.spawn(async move |cx| {
|
||||
loop {
|
||||
let sleep = cx.update(|cx| {
|
||||
let now = cx.background_executor().now();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock
|
||||
.next_wake()
|
||||
.map(|wake| wake.saturating_duration_since(now).max(MIN_SLEEP))
|
||||
});
|
||||
let Some(sleep) = sleep else { break };
|
||||
cx.background_executor().timer(sleep).await;
|
||||
let parked = cx.update(|cx| {
|
||||
let now = cx.background_executor().now();
|
||||
tick_hover_fades();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
clock.leases.retain(|_, lease| lease.until > now);
|
||||
if clock.leases.is_empty() {
|
||||
clock.running = false;
|
||||
return true;
|
||||
}
|
||||
let mut owed = Vec::new();
|
||||
for (view, lease) in clock.leases.iter_mut() {
|
||||
if lease.due > now {
|
||||
continue;
|
||||
}
|
||||
lease.due += lease.period;
|
||||
if lease.due <= now {
|
||||
lease.due = now + lease.period;
|
||||
}
|
||||
lease.in_flight = true;
|
||||
owed.push(*view);
|
||||
}
|
||||
for view in owed {
|
||||
cx.notify(view);
|
||||
}
|
||||
false
|
||||
});
|
||||
if parked {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Current phase `[0,1)` of a repeating spec; reduced motion returns a
|
||||
/// static 0 and schedules nothing.
|
||||
pub fn pulse_delta(spec: &MotionSpec, painter: Painter, cx: &mut App) -> f32 {
|
||||
if cx.reduced_motion() {
|
||||
return 0.0;
|
||||
}
|
||||
painter.lease(PULSE_FPS, PULSE_LEASE, cx);
|
||||
let now = cx.background_executor().now();
|
||||
let clock = cx.default_global::<PulseClock>();
|
||||
let epoch = *clock.epoch.get_or_insert(now);
|
||||
let period = spec.total().as_secs_f32();
|
||||
((now - epoch).as_secs_f32() / period).fract()
|
||||
}
|
||||
|
||||
/// A CSS `cubic-bezier(x1, y1, x2, y2)` timing function, evaluated exactly.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CubicBezier {
|
||||
pub x1: f32,
|
||||
pub y1: f32,
|
||||
pub x2: f32,
|
||||
pub y2: f32,
|
||||
}
|
||||
|
||||
impl CubicBezier {
|
||||
pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
|
||||
Self { x1, y1, x2, y2 }
|
||||
}
|
||||
|
||||
fn coefficients(a: f32, b: f32) -> (f32, f32, f32) {
|
||||
let c = 3.0 * a;
|
||||
let bb = 3.0 * (b - a) - c;
|
||||
let aa = 1.0 - c - bb;
|
||||
(aa, bb, c)
|
||||
}
|
||||
|
||||
fn sample_x(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.x1, self.x2);
|
||||
((a * t + b) * t + c) * t
|
||||
}
|
||||
|
||||
fn sample_y(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.y1, self.y2);
|
||||
((a * t + b) * t + c) * t
|
||||
}
|
||||
|
||||
fn sample_x_derivative(&self, t: f32) -> f32 {
|
||||
let (a, b, c) = Self::coefficients(self.x1, self.x2);
|
||||
(3.0 * a * t + 2.0 * b) * t + c
|
||||
}
|
||||
|
||||
fn solve_t_for_x(&self, x: f32) -> f32 {
|
||||
let mut t = x;
|
||||
for _ in 0..8 {
|
||||
let err = self.sample_x(t) - x;
|
||||
if err.abs() < 1e-6 {
|
||||
return t;
|
||||
}
|
||||
let d = self.sample_x_derivative(t);
|
||||
if d.abs() < 1e-6 {
|
||||
break;
|
||||
}
|
||||
t -= err / d;
|
||||
}
|
||||
let (mut lo, mut hi) = (0.0_f32, 1.0_f32);
|
||||
for _ in 0..32 {
|
||||
let mid = (lo + hi) / 2.0;
|
||||
if self.sample_x(mid) < x {
|
||||
lo = mid
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
(lo + hi) / 2.0
|
||||
}
|
||||
|
||||
/// Eased output for input progress `x ∈ [0,1]`, clamped hard — gpui's
|
||||
/// animation element asserts its delta is in `[0,1]` and aborts.
|
||||
pub fn eval(&self, x: f32) -> f32 {
|
||||
if x <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
if x >= 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
self.sample_y(self.solve_t_for_x(x)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// This curve as a gpui easing closure.
|
||||
pub fn easing(self) -> impl Fn(f32) -> f32 + 'static {
|
||||
move |x| self.eval(x)
|
||||
}
|
||||
}
|
||||
|
||||
/// The signature entrance curve — CSS `cubic-bezier(0.16, 1, 0.3, 1)`.
|
||||
pub const EASE_OUT_EXPO: CubicBezier = CubicBezier::new(0.16, 1.0, 0.3, 1.0);
|
||||
/// CSS `ease-out` — width/height transitions.
|
||||
pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.58, 1.0);
|
||||
/// CSS `ease` — quick fades, menu/dialog pops.
|
||||
pub const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
|
||||
/// Sidebar resort glide — CSS `cubic-bezier(0.22, 1, 0.36, 1)`.
|
||||
pub const EASE_RESORT: CubicBezier = CubicBezier::new(0.22, 1.0, 0.36, 1.0);
|
||||
/// CSS `ease-in-out`.
|
||||
pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
|
||||
/// Tailwind's default transition curve — CSS `cubic-bezier(0.4, 0, 0.2, 1)`.
|
||||
pub const EASE_TAILWIND: CubicBezier = CubicBezier::new(0.4, 0.0, 0.2, 1.0);
|
||||
|
||||
/// One catalog entry: duration + optional delay + curve; gpui has no native
|
||||
/// delay, so `delay_ms` holds progress at 0 until it elapses.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MotionSpec {
|
||||
pub duration_ms: u64,
|
||||
pub delay_ms: u64,
|
||||
pub curve: CubicBezier,
|
||||
}
|
||||
|
||||
impl MotionSpec {
|
||||
pub const fn new(duration_ms: u64, curve: CubicBezier) -> Self {
|
||||
Self {
|
||||
duration_ms,
|
||||
delay_ms: 0,
|
||||
curve,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_delay(mut self, delay_ms: u64) -> Self {
|
||||
self.delay_ms = delay_ms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wall-clock span of the whole timeline (delay + duration).
|
||||
pub fn total(&self) -> Duration {
|
||||
Duration::from_millis(self.delay_ms + self.duration_ms)
|
||||
}
|
||||
|
||||
/// Eased progress (0..1) for a raw timeline delta (0..1 across `total`).
|
||||
pub fn progress(&self, raw_delta: f32) -> f32 {
|
||||
let total = (self.delay_ms + self.duration_ms) as f32;
|
||||
if total <= 0.0 || self.duration_ms == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
let t =
|
||||
(raw_delta.clamp(0.0, 1.0) * total - self.delay_ms as f32) / self.duration_ms as f32;
|
||||
self.curve.eval(t.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// A oneshot gpui [`Animation`] for this spec.
|
||||
pub fn animation(&self) -> Animation {
|
||||
let spec = *self;
|
||||
Animation::new(spec.total().mul_f32(speed_scale())).with_easing(move |d| spec.progress(d))
|
||||
}
|
||||
}
|
||||
|
||||
/// Entrances: 0.5s expo-out fade + 4px rise.
|
||||
pub const FADE_IN: MotionSpec = MotionSpec::new(500, EASE_OUT_EXPO);
|
||||
/// Quick fade: 0.15s.
|
||||
pub const FADE_QUICK: MotionSpec = MotionSpec::new(150, EASE);
|
||||
/// Popover-in: 0.14s.
|
||||
pub const MENU_IN: MotionSpec = MotionSpec::new(140, EASE);
|
||||
/// Popover-out: 0.1s — quicker than the entrance.
|
||||
pub const MENU_OUT: MotionSpec = MotionSpec::new(100, EASE);
|
||||
/// Dialog-in: 0.18s.
|
||||
pub const DIALOG_IN: MotionSpec = MotionSpec::new(180, EASE);
|
||||
/// Boot-splash exit: 0.5s fade + 6px lift after a 0.15s hold.
|
||||
pub const SPLASH_OUT: MotionSpec = MotionSpec::new(500, EASE).with_delay(150);
|
||||
/// Sidebar / pane width+height transitions: 200ms ease-out.
|
||||
pub const RESIZE: MotionSpec = MotionSpec::new(200, EASE_OUT);
|
||||
/// Tab drag-reorder sliding transforms: 150ms.
|
||||
pub const TAB_SLIDE: MotionSpec = MotionSpec::new(150, EASE_OUT);
|
||||
/// Per-pane collapse: 180ms height.
|
||||
pub const COLLAPSE: MotionSpec = MotionSpec::new(180, EASE_OUT);
|
||||
/// A gliding move the reader caused rather than one they are dragging.
|
||||
pub const LAYOUT: MotionSpec = MotionSpec::new(200, EASE_OUT);
|
||||
/// Chevron rotate: 200ms.
|
||||
pub const CHEVRON: MotionSpec = MotionSpec::new(200, EASE);
|
||||
/// Scroll-to-row glide: 500ms ease-in-out.
|
||||
pub const SCROLL_GLIDE: MotionSpec = MotionSpec::new(500, EASE_IN_OUT);
|
||||
/// CSS `transition-colors` default: 150ms over [`EASE_TAILWIND`].
|
||||
pub const HOVER_FADE: MotionSpec = MotionSpec::new(150, EASE_TAILWIND);
|
||||
/// Toast slide/fade, coop's own: 250ms over [`EASE_TAILWIND`].
|
||||
pub const NOTIFICATION_IN: MotionSpec = MotionSpec::new(250, EASE_TAILWIND);
|
||||
/// Pulse loader period: 2.4s.
|
||||
pub const PULSE: MotionSpec = MotionSpec::new(2400, EASE);
|
||||
/// Spinner wave period: 750ms.
|
||||
pub const GRADIENT_SPIN: MotionSpec = MotionSpec::new(750, EASE);
|
||||
|
||||
/// Standard entrance: opacity 0→1 + translateY 4→0 over [`FADE_IN`].
|
||||
pub fn fade_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, FADE_IN.animation(), |el, t| {
|
||||
el.relative().opacity(t).top(px(4.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Quick opacity-only fade over [`FADE_QUICK`].
|
||||
pub fn fade_quick<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, FADE_QUICK.animation(), |el, t| el.opacity(t))
|
||||
}
|
||||
|
||||
/// Popover entrance: fade + translateY −2→0 over [`MENU_IN`].
|
||||
pub fn menu_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, MENU_IN.animation(), |el, t| {
|
||||
el.relative()
|
||||
.opacity(0.3 + 0.7 * t)
|
||||
.top(px(-2.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Popover exit over [`MENU_OUT`]; the caller supplies progress because
|
||||
/// `with_animation` replays from 0 on remount.
|
||||
pub fn menu_out<E>(id: impl Into<ElementId>, t: f32, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, MENU_OUT.animation(), move |el, _| {
|
||||
el.relative().opacity(1.0 - t).top(px(-2.0 * t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Dialog entrance over [`DIALOG_IN`] (fade + 2px rise).
|
||||
pub fn dialog_in<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, DIALOG_IN.animation(), |el, t| {
|
||||
el.relative().opacity(t).top(px(2.0 * (1.0 - t)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Boot-splash exit: hold 150ms, then fade out + lift 6px over 500ms.
|
||||
pub fn splash_out<E>(id: impl Into<ElementId>, element: E) -> AnimationElement<E>
|
||||
where
|
||||
E: Styled + IntoElement + 'static,
|
||||
{
|
||||
element.with_animation(id, SPLASH_OUT.animation(), |el, t| {
|
||||
el.opacity(1.0 - t).top(px(-6.0 * t))
|
||||
})
|
||||
}
|
||||
|
||||
/// Linear interpolation.
|
||||
pub fn lerp(from: f32, to: f32, t: f32) -> f32 {
|
||||
from + (to - from) * t
|
||||
}
|
||||
|
||||
/// One element's hover fade progress over [`HOVER_FADE`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FadeEntry {
|
||||
origin: f32,
|
||||
target: f32,
|
||||
started: Instant,
|
||||
seen: u64,
|
||||
}
|
||||
|
||||
impl FadeEntry {
|
||||
fn value(&self, now: Instant, duration: Duration) -> f32 {
|
||||
let elapsed = now.saturating_duration_since(self.started);
|
||||
if duration.is_zero() || elapsed >= duration {
|
||||
return self.target;
|
||||
}
|
||||
let raw = elapsed.as_secs_f32() / duration.as_secs_f32();
|
||||
lerp(self.origin, self.target, HOVER_FADE.curve.eval(raw))
|
||||
}
|
||||
|
||||
fn settled(&self, now: Instant, duration: Duration) -> bool {
|
||||
self.origin == self.target || now.saturating_duration_since(self.started) >= duration
|
||||
}
|
||||
}
|
||||
|
||||
/// Which fade: the view that paints it plus the element key — two views
|
||||
/// using the same string would trade washes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Fade {
|
||||
pub painter: Painter,
|
||||
pub key: SharedString,
|
||||
}
|
||||
|
||||
impl Fade {
|
||||
pub fn new(painter: Painter, key: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
painter,
|
||||
key: key.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-fade progress store.
|
||||
#[derive(Default)]
|
||||
pub struct HoverFades {
|
||||
pub entries: HashMap<Fade, FadeEntry>,
|
||||
frame: u64,
|
||||
}
|
||||
|
||||
impl HoverFades {
|
||||
pub fn duration() -> Duration {
|
||||
HOVER_FADE.total().mul_f32(speed_scale())
|
||||
}
|
||||
|
||||
/// Pointer entered (`hovered`) or left the element behind `fade`.
|
||||
pub fn set_at(&mut self, fade: &Fade, hovered: bool, reduced: bool, now: Instant) {
|
||||
let target = if hovered { 1.0 } else { 0.0 };
|
||||
let duration = Self::duration();
|
||||
let current = self
|
||||
.entries
|
||||
.get(fade)
|
||||
.map(|e| e.value(now, duration))
|
||||
.unwrap_or(0.0);
|
||||
if target == 0.0 && !self.entries.contains_key(fade) {
|
||||
return;
|
||||
}
|
||||
let origin = if reduced { target } else { current };
|
||||
let seen = self.frame;
|
||||
self.entries.insert(
|
||||
fade.clone(),
|
||||
FadeEntry {
|
||||
origin,
|
||||
target,
|
||||
started: now,
|
||||
seen,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Hover progress (0..1) for `fade` at `now`; stamps liveness.
|
||||
pub fn value_at(&mut self, fade: &Fade, now: Instant) -> f32 {
|
||||
let frame = self.frame;
|
||||
match self.entries.get_mut(fade) {
|
||||
Some(entry) => {
|
||||
entry.seen = frame;
|
||||
entry.value(now, Self::duration())
|
||||
}
|
||||
None => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the frame counter and prune settled or unmounted entries.
|
||||
pub fn tick_at(&mut self, now: Instant) -> bool {
|
||||
self.frame += 1;
|
||||
let frame = self.frame;
|
||||
let duration = Self::duration();
|
||||
let mut active = false;
|
||||
self.entries.retain(|_, entry| {
|
||||
if entry.seen + 1 < frame {
|
||||
return false;
|
||||
}
|
||||
let settled = entry.settled(now, duration);
|
||||
if !settled {
|
||||
active = true;
|
||||
}
|
||||
!(settled && entry.target == 0.0)
|
||||
});
|
||||
active
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static HOVER_FADES: RefCell<HoverFades> = RefCell::new(HoverFades::default());
|
||||
}
|
||||
|
||||
/// Hover progress (0..1) for `fade` this frame.
|
||||
pub fn hover_t(fade: &Fade) -> f32 {
|
||||
HOVER_FADES.with(|fades| fades.borrow_mut().value_at(fade, Instant::now()))
|
||||
}
|
||||
|
||||
/// Record a hover flip for `fade`; prefer [`hover_listener`].
|
||||
pub fn set_hover(fade: &Fade, hovered: bool, reduced: bool) {
|
||||
HOVER_FADES.with(|fades| {
|
||||
fades
|
||||
.borrow_mut()
|
||||
.set_at(fade, hovered, reduced, Instant::now())
|
||||
});
|
||||
}
|
||||
|
||||
/// An `.on_hover` listener driving the fade.
|
||||
pub fn hover_listener(fade: Fade) -> impl Fn(&bool, &mut Window, &mut App) + 'static {
|
||||
move |hovered, _window, cx| {
|
||||
set_hover(&fade, *hovered, cx.reduced_motion());
|
||||
fade.painter.lease(HOVER_FPS, HoverFades::duration(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Once-per-tick bookkeeping for the fade store, driven by the clock.
|
||||
fn tick_hover_fades() {
|
||||
HOVER_FADES.with(|fades| {
|
||||
fades.borrow_mut().tick_at(Instant::now());
|
||||
});
|
||||
}
|
||||
|
||||
/// Blend two colors by `t` the way the browser transitions them: sRGB with
|
||||
/// premultiplied alpha, so a wash fading in never passes through grey.
|
||||
pub fn mix(from: Hsla, to: Hsla, t: f32) -> Hsla {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
if t <= 0.0 {
|
||||
return from;
|
||||
}
|
||||
if t >= 1.0 {
|
||||
return to;
|
||||
}
|
||||
let (f, g) = (Rgba::from(from), Rgba::from(to));
|
||||
let a = lerp(f.a, g.a, t);
|
||||
if a <= f32::EPSILON {
|
||||
return Hsla::from(Rgba { a: 0.0, ..g });
|
||||
}
|
||||
Hsla::from(Rgba {
|
||||
r: lerp(f.r * f.a, g.r * g.a, t) / a,
|
||||
g: lerp(f.g * f.a, g.g * g.a, t) / a,
|
||||
b: lerp(f.b * f.a, g.b * g.a, t) / a,
|
||||
a,
|
||||
})
|
||||
}
|
||||
|
||||
/// The standard hover blend: rest → hover color at this fade's progress.
|
||||
pub fn hover_blend(fade: &Fade, rest: Hsla, hover: Hsla) -> Hsla {
|
||||
mix(rest, hover, hover_t(fade))
|
||||
}
|
||||
|
||||
/// Process-wide motion speed, as raw `f32` bits; an atomic mirror because the
|
||||
/// timelines are read from free functions with no `cx` in scope.
|
||||
static SPEED: AtomicU32 = AtomicU32::new(1.0f32.to_bits());
|
||||
|
||||
/// How far every timeline in the catalog is stretched. `1.0` is the designed
|
||||
/// speed.
|
||||
pub fn speed_scale() -> f32 {
|
||||
f32::from_bits(SPEED.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Stretch every catalog timeline by `scale`; clamped, and a non-finite
|
||||
/// `scale` resets to `1.0`.
|
||||
pub fn set_speed(scale: f32) {
|
||||
let scale = if scale.is_finite() {
|
||||
scale.clamp(0.01, 100.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
SPEED.store(scale.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Process-wide motion switches.
|
||||
#[derive(Clone, Copy)]
|
||||
struct MotionSettings {
|
||||
reduced_motion: bool,
|
||||
pause_when_inactive: bool,
|
||||
}
|
||||
|
||||
impl Global for MotionSettings {}
|
||||
|
||||
impl MotionSettings {
|
||||
/// Defaults: the OS flag rules reduce motion; the clock pauses when no
|
||||
/// window is active.
|
||||
fn get(cx: &App) -> Self {
|
||||
cx.try_global::<MotionSettings>().copied().unwrap_or(Self {
|
||||
reduced_motion: false,
|
||||
pause_when_inactive: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// App-level motion switches.
|
||||
pub trait AppExt {
|
||||
/// The OS reduce-motion flag ORed with the manual override.
|
||||
fn reduced_motion(&self) -> bool;
|
||||
|
||||
/// Set the manual reduce-motion override.
|
||||
fn set_reduced_motion(&mut self, reduced: bool);
|
||||
|
||||
/// Whether the leased clock claims frames while no window is active.
|
||||
fn pause_when_inactive(&self) -> bool;
|
||||
|
||||
fn set_pause_when_inactive(&mut self, pause: bool);
|
||||
}
|
||||
|
||||
impl AppExt for App {
|
||||
fn reduced_motion(&self) -> bool {
|
||||
self.reduce_motion() || MotionSettings::get(self).reduced_motion
|
||||
}
|
||||
|
||||
fn set_reduced_motion(&mut self, reduced: bool) {
|
||||
let mut settings = MotionSettings::get(self);
|
||||
settings.reduced_motion = reduced;
|
||||
self.set_global(settings);
|
||||
}
|
||||
|
||||
fn pause_when_inactive(&self) -> bool {
|
||||
MotionSettings::get(self).pause_when_inactive
|
||||
}
|
||||
|
||||
fn set_pause_when_inactive(&mut self, pause: bool) {
|
||||
let mut settings = MotionSettings::get(self);
|
||||
settings.pause_when_inactive = pause;
|
||||
self.set_global(settings);
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -4,7 +4,7 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div,
|
||||
Styled, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -44,8 +44,7 @@ impl Nav {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the row as clickable even when it has no `on_click` handler, so it
|
||||
/// still shows the hover affordance. Used by rows whose click opens a menu.
|
||||
/// Marks the row as clickable even without an `on_click` handler, keeping the hover affordance.
|
||||
pub fn clickable(mut self, clickable: bool) -> Self {
|
||||
self.clickable = clickable;
|
||||
self
|
||||
@@ -104,15 +103,21 @@ impl Styled for Nav {
|
||||
impl RenderOnce for Nav {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let clickable = self.on_click.is_some() || self.clickable;
|
||||
let text_color = if self.selected {
|
||||
cx.theme().text
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
};
|
||||
|
||||
v_flex().h_10().w_full().items_center().child(
|
||||
v_flex().h_10().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_1p5()
|
||||
.gap_1p5()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.rounded(px(theme::button_radius()))
|
||||
.text_color(text_color)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -126,10 +131,8 @@ impl RenderOnce for Nav {
|
||||
)
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().ghost_element_active)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.when(self.selected, |this| this.bg(cx.theme().element_active))
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
|
||||
+14
-20
@@ -4,9 +4,9 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||
Styled, Window, div,
|
||||
Styled, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Selectable, StyledExt, h_flex};
|
||||
|
||||
@@ -21,7 +21,6 @@ pub struct NavItem {
|
||||
label: SharedString,
|
||||
suffix: Option<AnyElement>,
|
||||
selected: bool,
|
||||
clickable: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
@@ -43,18 +42,10 @@ impl NavItem {
|
||||
label: label.into(),
|
||||
suffix: None,
|
||||
selected: false,
|
||||
clickable: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the item as clickable even when it has no `on_click` handler, so it
|
||||
/// still shows the hover affordance. Used by items whose click opens a menu.
|
||||
pub fn clickable(mut self, clickable: bool) -> Self {
|
||||
self.clickable = clickable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
@@ -94,7 +85,12 @@ impl Styled for NavItem {
|
||||
|
||||
impl RenderOnce for NavItem {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let clickable = self.on_click.is_some() || self.clickable;
|
||||
let clickable = self.on_click.is_some();
|
||||
let text_color = if self.selected {
|
||||
cx.theme().text
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
@@ -102,16 +98,16 @@ impl RenderOnce for NavItem {
|
||||
.px_2()
|
||||
.py_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_color(cx.theme().text)
|
||||
.gap(px(theme::SPACE))
|
||||
.rounded(px(theme::button_radius()))
|
||||
.text_color(text_color)
|
||||
.child(self.icon)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.child(self.label),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
@@ -119,10 +115,8 @@ impl RenderOnce for NavItem {
|
||||
})
|
||||
.when(clickable, |this| {
|
||||
this.cursor_pointer()
|
||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().ghost_element_active)
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.when(self.selected, |this| this.bg(cx.theme().element_active))
|
||||
})
|
||||
.when_some(self.on_click, |this, handler| {
|
||||
this.on_click(move |event, window, cx| handler(event, window, cx))
|
||||
|
||||
@@ -5,19 +5,19 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Window, div, px, relative,
|
||||
Anchor, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
|
||||
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||
Window, div, px, relative,
|
||||
};
|
||||
use gpui_base::{
|
||||
Toast as BaseToast, ToastManager, ToastMotion, ToastOptions, ToastStack, ToastStackState,
|
||||
ToastTransitionStatus,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::motion::NOTIFICATION_IN;
|
||||
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
|
||||
/// How often the notification lifecycle clock is sampled.
|
||||
@@ -40,20 +40,13 @@ pub enum NotificationKind {
|
||||
|
||||
impl NotificationKind {
|
||||
fn icon(&self, cx: &App) -> Icon {
|
||||
match self {
|
||||
Self::Info => Icon::new(IconName::Info)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().icon),
|
||||
Self::Success => Icon::new(IconName::CheckCircle)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().icon_accent),
|
||||
Self::Warning => Icon::new(IconName::Warning)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().text_warning),
|
||||
Self::Error => Icon::new(IconName::CloseCircle)
|
||||
.with_size(Size::Medium)
|
||||
.text_color(cx.theme().danger_foreground),
|
||||
}
|
||||
let (name, color) = match self {
|
||||
Self::Info => (IconName::Info, cx.theme().text_muted),
|
||||
Self::Success => (IconName::CheckCircle, cx.theme().success),
|
||||
Self::Warning => (IconName::Warning, cx.theme().warning),
|
||||
Self::Error => (IconName::CloseCircle, gpui::white()),
|
||||
};
|
||||
Icon::new(name).with_size(Size::Medium).text_color(color)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +71,7 @@ impl From<(TypeId, ElementId)> for NotificationId {
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
/// Then you push a notification with the same id, the previous notification will be replaced.
|
||||
///
|
||||
/// None means the notification will be added to the end of the list.
|
||||
/// Pushing a notification with the same id replaces the previous one.
|
||||
id: NotificationId,
|
||||
style: StyleRefinement,
|
||||
kind: Option<NotificationKind>,
|
||||
@@ -128,9 +118,7 @@ impl From<(NotificationKind, SharedString)> for Notification {
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification.
|
||||
///
|
||||
/// The default id is a random UUID.
|
||||
/// Creates a notification with a random UUID id.
|
||||
pub fn new() -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
@@ -177,12 +165,7 @@ impl Notification {
|
||||
.with_kind(NotificationKind::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
///
|
||||
/// ```rs
|
||||
/// struct MyNotificationKind;
|
||||
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
||||
/// ```
|
||||
/// Sets the type used to uniquely identify the notification.
|
||||
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
||||
self.id = TypeId::of::<T>().into();
|
||||
self
|
||||
@@ -194,15 +177,13 @@ impl Notification {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the notification, default is None.
|
||||
///
|
||||
/// If title is None, the notification will not have a title.
|
||||
/// Sets the title of the notification, default is None.
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
/// Sets the kind of the notification, default is `NotificationKind::Info`.
|
||||
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
|
||||
self.kind = Some(kind);
|
||||
self
|
||||
@@ -214,9 +195,7 @@ impl Notification {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the action button of the notification.
|
||||
///
|
||||
/// When an action is set, the notification will not autohide.
|
||||
/// Sets an action button; the notification will not autohide when set.
|
||||
pub fn action<F>(mut self, action: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
@@ -226,8 +205,8 @@ impl Notification {
|
||||
self
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
/// Emits a request for the notification list to dismiss this notification.
|
||||
pub(crate) fn dismiss(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissRequest);
|
||||
}
|
||||
|
||||
@@ -300,12 +279,14 @@ impl Render for Notification {
|
||||
};
|
||||
|
||||
let background = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_background,
|
||||
_ => cx.theme().surface_background,
|
||||
Some(NotificationKind::Error) => cx.theme().danger_strong,
|
||||
_ => cx.theme().surface_overlay,
|
||||
};
|
||||
|
||||
let text_color = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_foreground,
|
||||
// White on the danger plate in both appearances, per bezel's
|
||||
// destructive pairing.
|
||||
Some(NotificationKind::Error) => gpui::white(),
|
||||
_ => cx.theme().text,
|
||||
};
|
||||
|
||||
@@ -326,10 +307,12 @@ impl Render for Notification {
|
||||
.border_color(cx.theme().border)
|
||||
.bg(background)
|
||||
.text_color(text_color)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.rounded(px(theme::surface_radius()))
|
||||
.when(cx.theme().shadow, |this| {
|
||||
this.shadow(theme::surface_shadows())
|
||||
})
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.gap(px(theme::SPACE))
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.when(only_message, |this| this.items_center())
|
||||
@@ -343,12 +326,12 @@ impl Render for Notification {
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(h_flex().h_5().text_sm().font_semibold().child(title))
|
||||
this.child(h_flex().h_5().text_style(TextStyle::Headline).child(title))
|
||||
})
|
||||
.when_some(self.message.clone(), |this, message| {
|
||||
this.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_style(TextStyle::Callout)
|
||||
.when(has_title, |this| this.text_color(cx.theme().text_muted))
|
||||
.line_height(relative(1.3))
|
||||
.child(message),
|
||||
@@ -356,7 +339,7 @@ impl Render for Notification {
|
||||
})
|
||||
.when_some(content, |this, content| this.child(content))
|
||||
.when_some(action, |this, action| {
|
||||
this.gap_2().child(
|
||||
this.gap(px(theme::SPACE)).child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.w_full()
|
||||
@@ -396,15 +379,11 @@ impl Render for Notification {
|
||||
}))
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
NOTIFICATION_IN.animation(),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let opacity = 1. - delta;
|
||||
let that = this
|
||||
.shadow_none()
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none());
|
||||
let that = this.shadow_none().opacity(opacity);
|
||||
match placement {
|
||||
Anchor::TopRight | Anchor::BottomRight => {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
@@ -446,8 +425,7 @@ impl Render for Notification {
|
||||
|
||||
/// A list of notifications.
|
||||
pub(crate) struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: ToastManager<NotificationId, Entity<Notification>>,
|
||||
notifications: ToastManager<NotificationId, Entity<Notification>>,
|
||||
|
||||
/// Measured geometry and interaction state of the visible stack.
|
||||
stack_state: ToastStackState,
|
||||
@@ -455,12 +433,11 @@ pub(crate) struct NotificationList {
|
||||
/// Whether the lifecycle clock is running. The loop clears it as it exits.
|
||||
is_advancing: bool,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
impl NotificationList {
|
||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
pub(crate) fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
notifications: ToastManager::new(ToastMotion::default()),
|
||||
stack_state: ToastStackState::default(),
|
||||
@@ -469,10 +446,7 @@ impl NotificationList {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tick the toast lifecycle until the last notification is unmounted.
|
||||
///
|
||||
/// The stack expansion is sampled here because it reaches the list through
|
||||
/// no event, and an idle window should arm no timer.
|
||||
/// Ticks the toast lifecycle, including stack expansion which arrives through no event.
|
||||
fn start_advancing(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_advancing {
|
||||
return;
|
||||
@@ -520,7 +494,7 @@ impl NotificationList {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
pub(crate) fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
@@ -575,18 +549,6 @@ impl NotificationList {
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
for id in self
|
||||
.notifications
|
||||
.dismiss_all(cx.background_executor().now())
|
||||
{
|
||||
if let Some(note) = self.notifications.get(&id) {
|
||||
note.update(cx, |note, cx| note.begin_close(cx));
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
|
||||
+34
-52
@@ -6,13 +6,13 @@ use gpui::{
|
||||
MouseButton, ParentElement, RenderOnce, Stateful, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use gpui_base::Popover as BasePopover;
|
||||
pub use gpui_base::PopoverState;
|
||||
pub(crate) use gpui_base::PopoverState;
|
||||
|
||||
use crate::{Selectable, StyledExt as _, v_flex};
|
||||
|
||||
/// A popover element that can be triggered by a button or any other element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Popover {
|
||||
pub(crate) struct Popover {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
anchor: Anchor,
|
||||
@@ -26,8 +26,7 @@ pub struct Popover {
|
||||
>,
|
||||
>,
|
||||
children: Vec<AnyElement>,
|
||||
/// Style for trigger element.
|
||||
/// This is used for hotfix the trigger element style to support w_full.
|
||||
/// Style refinement applied to the trigger element.
|
||||
trigger_style: Option<StyleRefinement>,
|
||||
mouse_button: MouseButton,
|
||||
appearance: bool,
|
||||
@@ -35,8 +34,8 @@ pub struct Popover {
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
/// Create a new Popover with `view` mode.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
/// Creates a new Popover with the given id.
|
||||
pub(crate) fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
style: StyleRefinement::default(),
|
||||
@@ -51,23 +50,20 @@ impl Popover {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner of the popover, default is `Corner::TopLeft`.
|
||||
///
|
||||
/// This method is kept for backward compatibility with `Corner` type.
|
||||
/// Internally, it converts `Corner` to `Anchor`.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
/// Sets the anchor corner of the popover, default is `Anchor::TopLeft`.
|
||||
pub(crate) fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the mouse button to trigger the popover, default is `MouseButton::Left`.
|
||||
pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
|
||||
/// Sets the mouse button that triggers the popover, default is `MouseButton::Left`.
|
||||
pub(crate) fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
|
||||
self.mouse_button = mouse_button;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the trigger element of the popover.
|
||||
pub fn trigger<T>(mut self, trigger: T) -> Self
|
||||
/// Sets the trigger element, marked as selected while the popover is open.
|
||||
pub(crate) fn trigger<T>(mut self, trigger: T) -> Self
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
@@ -78,12 +74,8 @@ impl Popover {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the trigger from a builder, for elements that have no selected state.
|
||||
///
|
||||
/// [`Self::trigger`] marks the trigger as selected while the popover is
|
||||
/// open, so it cannot be used with elements whose selection carries a
|
||||
/// different meaning, such as a row that indicates the current room.
|
||||
pub fn trigger_with<F>(mut self, trigger: F) -> Self
|
||||
/// Sets the trigger from a builder, for elements that have no selected state.
|
||||
pub(crate) fn trigger_with<F>(mut self, trigger: F) -> Self
|
||||
where
|
||||
F: FnOnce(bool, &Window, &App) -> AnyElement + 'static,
|
||||
{
|
||||
@@ -91,23 +83,20 @@ impl Popover {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style for the trigger element.
|
||||
pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
/// Sets the style for the trigger element.
|
||||
pub(crate) fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.trigger_style = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether clicking outside the popover will dismiss it, default is `true`.
|
||||
pub fn overlay_closable(mut self, closable: bool) -> Self {
|
||||
/// Sets whether clicking outside the popover dismisses it, default is `true`.
|
||||
pub(crate) fn overlay_closable(mut self, closable: bool) -> Self {
|
||||
self.overlay_closable = closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content builder for content of the Popover.
|
||||
///
|
||||
/// This callback will called every time on render the popover.
|
||||
/// So, you should avoid creating new elements or entities in the content closure.
|
||||
pub fn content<F, E>(mut self, content: F) -> Self
|
||||
/// Sets the content builder, called on every render of the popover.
|
||||
pub(crate) fn content<F, E>(mut self, content: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> E + 'static,
|
||||
@@ -118,32 +107,13 @@ impl Popover {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether the popover no style, default is `false`.
|
||||
///
|
||||
/// If no style:
|
||||
///
|
||||
/// - The popover will not have a bg, border, shadow, or padding.
|
||||
/// - The click out of the popover will not dismiss it.
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
/// Sets whether the popover renders with default styling, default is `true`.
|
||||
pub(crate) fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Popover {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
pub(crate) fn render_popover_content(
|
||||
fn render_popover_content(
|
||||
anchor: Anchor,
|
||||
appearance: bool,
|
||||
_: &mut Window,
|
||||
@@ -162,6 +132,18 @@ impl Popover {
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Popover {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Popover {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let anchor = self.anchor;
|
||||
|
||||
@@ -12,15 +12,15 @@ pub(crate) fn resize_handle_appearance() -> ResizeHandleRenderer {
|
||||
Rc::new(
|
||||
|context: &ResizeHandleContext, _: &mut Window, cx: &mut App| {
|
||||
let color = if context.is_active() {
|
||||
cx.theme().border_selected
|
||||
cx.theme().border_strong
|
||||
} else {
|
||||
cx.theme().border
|
||||
cx.theme().element_hover
|
||||
};
|
||||
let axis = context.axis();
|
||||
|
||||
Some(
|
||||
div()
|
||||
.bg(cx.theme().border_variant)
|
||||
.bg(cx.theme().border_faint)
|
||||
.group_hover("handle", move |this| this.bg(color))
|
||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE))
|
||||
|
||||
+9
-27
@@ -4,14 +4,14 @@ use std::rc::Rc;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, ElementId, Entity,
|
||||
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
|
||||
Window, canvas, div, point, px, size,
|
||||
FocusHandle, HitboxBehavior, InteractiveElement, IntoElement, MouseButton, ParentElement as _,
|
||||
Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle, Window, canvas, div,
|
||||
point, px, size,
|
||||
};
|
||||
use gpui_base::TextSelectionLayer;
|
||||
use theme::{
|
||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||
CLIENT_SIDE_DECORATION_SHADOW,
|
||||
CLIENT_SIDE_DECORATION_SHADOW, base_text_size, surface_shadows,
|
||||
};
|
||||
|
||||
use crate::dialog::Dialog;
|
||||
@@ -230,19 +230,12 @@ impl Root {
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear all notifications from the notification layer.
|
||||
pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
|
||||
self.notification
|
||||
.update(cx, |view, cx| view.clear(window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Root {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let rem_size = cx.theme().font_size;
|
||||
let font_family = cx.theme().font_family.clone();
|
||||
let rem_size = px(base_text_size());
|
||||
let font_family = cx.theme().font_sans.clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
// Set the base font size
|
||||
@@ -361,19 +354,8 @@ impl Render for Root {
|
||||
.when(!tiling.right, |div| {
|
||||
div.border_r(CLIENT_SIDE_DECORATION_BORDER)
|
||||
})
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(vec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
inset: false,
|
||||
}])
|
||||
.when(!tiling.is_tiled() && cx.theme().shadow, |div| {
|
||||
div.shadow(surface_shadows())
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, _, cx| {
|
||||
@@ -381,7 +363,7 @@ impl Render for Root {
|
||||
})
|
||||
.size_full()
|
||||
.font_family(font_family)
|
||||
.bg(cx.theme().surface_background)
|
||||
.bg(cx.theme().bg)
|
||||
.text_color(cx.theme().text)
|
||||
.child(TextSelectionLayer)
|
||||
.child(self.view.clone()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod scrollable;
|
||||
|
||||
pub use gpui_base::{Scrollbar, ScrollbarAxis, ScrollbarHandle};
|
||||
pub use gpui_base::Scrollbar;
|
||||
pub use scrollable::*;
|
||||
|
||||
@@ -3,35 +3,14 @@ use std::panic::Location;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, px,
|
||||
ScrollHandle, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
|
||||
};
|
||||
use gpui_base::{Scrollbar, ScrollbarAxis, ScrollbarHandle};
|
||||
use theme::ActiveTheme as _;
|
||||
use gpui_base::{Scrollbar, ScrollbarAxis};
|
||||
|
||||
use crate::StyledExt as _;
|
||||
|
||||
/// A trait for elements that can be made scrollable with scrollbars.
|
||||
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
||||
/// Adds a scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn scrollbar<H: ScrollbarHandle + Clone>(
|
||||
self,
|
||||
scroll_handle: &H,
|
||||
axis: impl Into<ScrollbarAxis>,
|
||||
) -> Self {
|
||||
self.child(ScrollbarLayer {
|
||||
id: "scrollbar_layer".into(),
|
||||
axis: axis.into(),
|
||||
scroll_handle: scroll_handle.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a vertical scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
|
||||
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
|
||||
#[track_caller]
|
||||
fn overflow_y_scrollbar(self) -> Scrollable<Self> {
|
||||
@@ -39,72 +18,6 @@ pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Eleme
|
||||
}
|
||||
}
|
||||
|
||||
/// The scrollbar the application's theme describes: a 10px rail holding a 6px
|
||||
/// rounded thumb that grows to 8px under the pointer.
|
||||
fn scrollbar<H: ScrollbarHandle + Clone>(
|
||||
scroll_handle: &H,
|
||||
axis: ScrollbarAxis,
|
||||
cx: &App,
|
||||
) -> Scrollbar {
|
||||
let theme = cx.theme();
|
||||
let (thumb_width, thumb_radius) = if theme.scrollbar_mode.is_scrolling() {
|
||||
(px(6.), px(3.))
|
||||
} else {
|
||||
(px(8.), px(4.))
|
||||
};
|
||||
|
||||
Scrollbar::new(scroll_handle).axis(axis).styles(|styles| {
|
||||
styles
|
||||
.track(|track| {
|
||||
track
|
||||
.bg(theme.scrollbar_track_background)
|
||||
.border_color(theme.scrollbar_thumb_border)
|
||||
.width(px(10.))
|
||||
})
|
||||
.track_hover(|track| track.bg(theme.scrollbar_thumb_background).width(px(10.)))
|
||||
.thumb(|thumb| {
|
||||
thumb
|
||||
.bg(theme.scrollbar_thumb_background)
|
||||
.width(thumb_width)
|
||||
.inset(px(1.))
|
||||
.radius(thumb_radius)
|
||||
.min_length(px(48.))
|
||||
})
|
||||
.thumb_hover(|thumb| {
|
||||
thumb
|
||||
.bg(theme.scrollbar_thumb_hover_background)
|
||||
.width(px(8.))
|
||||
.inset(px(1.))
|
||||
.radius(px(4.))
|
||||
})
|
||||
.thumb_active(|thumb| {
|
||||
thumb
|
||||
.bg(theme.scrollbar_thumb_hover_background)
|
||||
.width(px(8.))
|
||||
.inset(px(1.))
|
||||
.radius(px(4.))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// A scrollbar child that resolves the theme while rendering, which the
|
||||
/// [`ScrollableElement`] helpers cannot do at the call site.
|
||||
#[derive(IntoElement)]
|
||||
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
|
||||
id: ElementId,
|
||||
axis: ScrollbarAxis,
|
||||
scroll_handle: H,
|
||||
}
|
||||
|
||||
impl<H> RenderOnce for ScrollbarLayer<H>
|
||||
where
|
||||
H: ScrollbarHandle + Clone + 'static,
|
||||
{
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
scrollbar(&self.scroll_handle, self.axis, cx).id(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A scrollable element wrapper that adds scrollbars to an interactive element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
|
||||
@@ -128,15 +41,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Styled for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.element.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ParentElement for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
@@ -146,18 +50,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Scrollable<Div> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Scrollable<Stateful<Div>> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RenderOnce for Scrollable<E>
|
||||
where
|
||||
E: InteractiveElement + Styled + ParentElement + Element + 'static,
|
||||
@@ -167,8 +59,6 @@ where
|
||||
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
// Inherit the size from the element style.
|
||||
let style = StyleRefinement {
|
||||
size: self.element.style().size.clone(),
|
||||
..Default::default()
|
||||
@@ -190,22 +80,14 @@ where
|
||||
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
|
||||
ScrollbarAxis::Both => this.overflow_scroll(),
|
||||
})
|
||||
.child(
|
||||
self.element
|
||||
// Refine element size to `flex_1`.
|
||||
.size_auto()
|
||||
.flex_1(),
|
||||
),
|
||||
.child(self.element.size_auto().flex_1()),
|
||||
)
|
||||
.child(
|
||||
Scrollbar::new(&scroll_handle)
|
||||
.axis(self.axis)
|
||||
.id("scrollbar"),
|
||||
)
|
||||
.child(scrollbar(&scroll_handle, self.axis, cx).id("scrollbar"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableElement for Div {}
|
||||
|
||||
impl<E> ScrollableElement for Stateful<E>
|
||||
where
|
||||
E: ParentElement + Styled + Element,
|
||||
Self: InteractiveElement,
|
||||
{
|
||||
}
|
||||
|
||||
+15
-17
@@ -1,7 +1,6 @@
|
||||
use gpui::{App, Div, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||
pub use gpui_base::component_traits::{Disableable, Selectable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, surface_radius, surface_shadows};
|
||||
|
||||
/// Returns a `Div` as horizontal flex layout.
|
||||
pub fn h_flex() -> Div {
|
||||
@@ -15,7 +14,7 @@ pub fn v_flex() -> Div {
|
||||
|
||||
/// Returns a `Div` as divider.
|
||||
pub fn divider(cx: &App) -> Div {
|
||||
div().my_1().w_full().h_px().bg(cx.theme().border_variant)
|
||||
div().my_1().w_full().h_px().bg(cx.theme().border_faint)
|
||||
}
|
||||
|
||||
macro_rules! font_weight {
|
||||
@@ -54,18 +53,20 @@ pub trait StyledExt: Styled + Sized {
|
||||
/// Set as Popover style
|
||||
#[inline]
|
||||
fn popover_style(self, cx: &mut App) -> Self {
|
||||
self.bg(cx.theme().background)
|
||||
let shadows = cx.theme().shadow.then(surface_shadows).unwrap_or_default();
|
||||
|
||||
self.bg(cx.theme().surface_overlay)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.shadow_md()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.shadow(shadows)
|
||||
.rounded(px(surface_radius()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Styled> StyledExt for E {}
|
||||
|
||||
/// A size for elements.
|
||||
#[derive(Clone, Default, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Default, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Size {
|
||||
Size(Pixels),
|
||||
XSmall,
|
||||
@@ -83,10 +84,7 @@ impl From<Pixels> for Size {
|
||||
|
||||
/// A trait for setting the size of an element.
|
||||
pub trait Sizable: Sized {
|
||||
/// Set the ui::Size of this element.
|
||||
///
|
||||
/// Also can receive a `ButtonSize` to convert to `IconSize`,
|
||||
/// Or a `Pixels` to set a custom size: `px(30.)`
|
||||
/// Set the element's [`Size`], or a custom pixel size (`px(30.)`).
|
||||
fn with_size(self, size: impl Into<Size>) -> Self;
|
||||
|
||||
/// Set to Size::XSmall
|
||||
@@ -130,18 +128,18 @@ impl<T: Styled> StyleSized<T> for T {
|
||||
match size {
|
||||
Size::XSmall => self.py_0p5(),
|
||||
Size::Medium => self.py_2(),
|
||||
Size::Large => self.py_5(),
|
||||
Size::Large => self.py_1p5(),
|
||||
_ => self.py_1(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_h(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::XSmall => self.h_6().text_xs(),
|
||||
Size::Small => self.h_8().text_sm(),
|
||||
Size::Medium => self.h_9().text_base(),
|
||||
Size::Large => self.h_12().text_lg(),
|
||||
Size::Size(size) => self.h(px(24.)).text_size(size),
|
||||
Size::XSmall => self.h_6().text_style(TextStyle::Caption),
|
||||
Size::Small => self.h_7().text_style(TextStyle::Callout),
|
||||
Size::Medium => self.h_8().text_style(TextStyle::Body),
|
||||
Size::Large => self.h_10().text_style(TextStyle::Title3),
|
||||
Size::Size(size) => self.h(size).text_size(size * 0.5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-51
@@ -4,12 +4,10 @@ use std::time::Duration;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, ElementId, IntoElement, ParentElement as _, RenderOnce, SharedString, Styled as _, Window,
|
||||
div, px, white,
|
||||
div, px, transparent_black,
|
||||
};
|
||||
use gpui_base::{Spring, Switch as BaseSwitch, SwitchThumb, SwitchTrack, spring};
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::{Disableable, Sizable, Size};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
@@ -17,12 +15,9 @@ type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
pub struct Switch {
|
||||
id: ElementId,
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
label: Option<SharedString>,
|
||||
description: Option<SharedString>,
|
||||
label_side: Side,
|
||||
on_click: OnClick,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl Switch {
|
||||
@@ -32,12 +27,9 @@ impl Switch {
|
||||
Self {
|
||||
id: id.clone(),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
label: None,
|
||||
description: None,
|
||||
on_click: None,
|
||||
label_side: Side::Left,
|
||||
size: Size::Medium,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,45 +57,27 @@ impl Switch {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Switch {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Switch {
|
||||
fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Switch {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let checked = self.checked;
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
let (bg, toggle_bg) = match checked {
|
||||
true => (cx.theme().element_background, white()),
|
||||
false => (cx.theme().elevated_surface_background, white()),
|
||||
let bg = if checked {
|
||||
cx.theme().accent_strong
|
||||
} else {
|
||||
cx.theme().input_bg
|
||||
};
|
||||
|
||||
let (bg, toggle_bg) = match self.disabled {
|
||||
true => (bg.opacity(0.3), toggle_bg.opacity(0.8)),
|
||||
false => (bg, toggle_bg),
|
||||
};
|
||||
|
||||
let (bg_width, bg_height) = match self.size {
|
||||
Size::XSmall | Size::Small => (px(28.), px(16.)),
|
||||
_ => (px(36.), px(20.)),
|
||||
};
|
||||
|
||||
let bar_width = match self.size {
|
||||
Size::XSmall | Size::Small => px(12.),
|
||||
_ => px(16.),
|
||||
let toggle_bg = cx.theme().on_solid;
|
||||
// The border is structural (it insets the thumb); it only paints when off.
|
||||
let track_border = if checked {
|
||||
transparent_black()
|
||||
} else {
|
||||
cx.theme().border
|
||||
};
|
||||
|
||||
let bg_width = px(36.);
|
||||
let bg_height = px(20.);
|
||||
let bar_width = px(16.);
|
||||
let inset = px(2.);
|
||||
|
||||
let thumb_left = spring(
|
||||
@@ -124,14 +98,13 @@ impl RenderOnce for Switch {
|
||||
div().child(
|
||||
BaseSwitch::new(self.id.clone())
|
||||
.checked(checked)
|
||||
.disabled(self.disabled)
|
||||
.when_some(accessibility_label, |this, label| {
|
||||
this.accessibility_label(label)
|
||||
})
|
||||
.when_some(on_click, |this, on_click| {
|
||||
this.on_change(move |next, _event, window, cx| on_click(&next, window, cx))
|
||||
})
|
||||
.when(self.label_side.is_left(), |this| this.flex_row_reverse())
|
||||
.flex_row_reverse()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
@@ -140,14 +113,16 @@ impl RenderOnce for Switch {
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.when_some(label, |this, label| {
|
||||
// Label
|
||||
this.child(div().text_sm().text_color(cx.theme().text).child(label))
|
||||
this.child(
|
||||
div()
|
||||
.text_style(TextStyle::Callout)
|
||||
.text_color(cx.theme().text)
|
||||
.child(label),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
// Switch Bar
|
||||
SwitchTrack::new((self.id.clone(), "track"))
|
||||
.checked(checked)
|
||||
.disabled(self.disabled)
|
||||
.flex_shrink_0()
|
||||
.w(bg_width)
|
||||
.h(bg_height)
|
||||
@@ -155,11 +130,10 @@ impl RenderOnce for Switch {
|
||||
.flex()
|
||||
.items_center()
|
||||
.border(inset)
|
||||
.border_color(cx.theme().border_transparent)
|
||||
.border_color(track_border)
|
||||
.bg(bg)
|
||||
.when(!self.disabled, |this| this.cursor_pointer())
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
// Switch Toggle
|
||||
SwitchThumb::new(checked)
|
||||
.rounded_full()
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
@@ -173,7 +147,7 @@ impl RenderOnce for Switch {
|
||||
this.child(
|
||||
div()
|
||||
.pr_3()
|
||||
.text_xs()
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(description),
|
||||
)
|
||||
|
||||
+27
-80
@@ -5,9 +5,9 @@ use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _};
|
||||
|
||||
use crate::{Icon, IconName, Selectable, h_flex};
|
||||
use crate::{Selectable, h_flex};
|
||||
|
||||
pub mod tab_bar;
|
||||
|
||||
@@ -17,61 +17,27 @@ pub mod tab_bar;
|
||||
pub struct Tab {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
pub(super) label: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
prefix: Option<AnyElement>,
|
||||
pub(super) tab_bar_prefix: Option<bool>,
|
||||
suffix: Option<AnyElement>,
|
||||
label: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
pub(super) segmented: bool,
|
||||
tab_bar_prefix: Option<bool>,
|
||||
suffix: Option<AnyElement>,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
segmented: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl From<&'static str> for Tab {
|
||||
fn from(label: &'static str) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Tab {
|
||||
fn from(label: String) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Tab {
|
||||
fn from(label: SharedString) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Icon> for Tab {
|
||||
fn from(icon: Icon) -> Self {
|
||||
Self::default().icon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for Tab {
|
||||
fn from(icon_name: IconName) -> Self {
|
||||
Self::default().icon(Icon::new(icon_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
icon: None,
|
||||
tab_bar_prefix: None,
|
||||
children: Vec::new(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
segmented: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
on_click: None,
|
||||
}
|
||||
@@ -90,32 +56,20 @@ impl Tab {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set icon for the tab.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
pub(crate) fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab, default false.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the tab.
|
||||
pub fn on_click(
|
||||
pub(crate) fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
@@ -179,8 +133,6 @@ impl RenderOnce for Tab {
|
||||
ix,
|
||||
base,
|
||||
label,
|
||||
icon,
|
||||
prefix,
|
||||
suffix,
|
||||
children,
|
||||
disabled,
|
||||
@@ -193,9 +145,9 @@ impl RenderOnce for Tab {
|
||||
let foreground = if disabled {
|
||||
cx.theme().text_muted
|
||||
} else if selected {
|
||||
cx.theme().tab_active_foreground
|
||||
cx.theme().text
|
||||
} else {
|
||||
cx.theme().tab_foreground
|
||||
cx.theme().text_muted
|
||||
};
|
||||
|
||||
let content = h_flex()
|
||||
@@ -206,15 +158,11 @@ impl RenderOnce for Tab {
|
||||
.overflow_hidden()
|
||||
.when(segmented, |this| this.justify_center().px_1())
|
||||
.when(!segmented, |this| this.justify_start())
|
||||
.map(|this| match icon {
|
||||
Some(icon) => this.w(px(38.)).child(icon.size_4()),
|
||||
None => this
|
||||
.map(|this| match label {
|
||||
Some(label) => this.child(label),
|
||||
None => this,
|
||||
})
|
||||
.children(children),
|
||||
});
|
||||
.map(|this| match label {
|
||||
Some(label) => this.child(label),
|
||||
None => this,
|
||||
})
|
||||
.children(children);
|
||||
|
||||
base.id(ix)
|
||||
.group("tab")
|
||||
@@ -222,16 +170,16 @@ impl RenderOnce for Tab {
|
||||
.items_center()
|
||||
.text_color(foreground)
|
||||
.when(segmented, |this| {
|
||||
this.text_xs()
|
||||
this.text_style(TextStyle::Caption)
|
||||
.flex_1()
|
||||
.h_6()
|
||||
.rounded(cx.theme().radius)
|
||||
.rounded(px(theme::button_radius()))
|
||||
.when(selected && !disabled, |this| {
|
||||
this.bg(cx.theme().tab_active_background)
|
||||
this.bg(cx.theme().bg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
})
|
||||
.when(!selected && !disabled, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().tab_hover_background))
|
||||
this.hover(|this| this.bg(cx.theme().element_hover))
|
||||
})
|
||||
})
|
||||
.when(!segmented, |this| {
|
||||
@@ -240,20 +188,19 @@ impl RenderOnce for Tab {
|
||||
.h_7()
|
||||
.gap_1()
|
||||
.px_1p5()
|
||||
.text_sm()
|
||||
.rounded(cx.theme().radius)
|
||||
.text_style(TextStyle::Callout)
|
||||
.rounded(px(theme::button_radius()))
|
||||
.overflow_hidden()
|
||||
.when(selected && !disabled, |this| {
|
||||
this.bg(cx.theme().tab_background)
|
||||
this.bg(cx.theme().element_active)
|
||||
})
|
||||
.when(!selected && !disabled, |this| {
|
||||
this.hover(|this| {
|
||||
this.bg(cx.theme().tab_hover_background)
|
||||
.text_color(cx.theme().tab_active_foreground)
|
||||
this.bg(cx.theme().element_hover)
|
||||
.text_color(cx.theme().text)
|
||||
})
|
||||
})
|
||||
})
|
||||
.when_some(prefix, |this, prefix| this.child(prefix))
|
||||
.child(content)
|
||||
.when_some(suffix, |this, suffix| {
|
||||
this.child(
|
||||
|
||||
@@ -52,25 +52,25 @@ impl TabBar {
|
||||
}
|
||||
|
||||
/// Track the scroll of the TabBar.
|
||||
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
pub(crate) fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle.clone());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the prefix element of the TabBar
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
pub(crate) fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the TabBar
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
pub(crate) fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add children of the TabBar.
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||
pub(crate) fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||
self.children.extend(children.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
@@ -88,14 +88,12 @@ impl TabBar {
|
||||
}
|
||||
|
||||
/// Set the last empty space element of the TabBar.
|
||||
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
pub(crate) fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
self.last_empty_space = last_empty_space.into_any_element();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
|
||||
///
|
||||
/// When this is set, the children's on_click will be ignored.
|
||||
/// Set the on_click callback of the TabBar; when set, the children's on_click is ignored.
|
||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: Fn(&usize, &mut Window, &mut App) + 'static,
|
||||
@@ -124,9 +122,9 @@ impl RenderOnce for TabBar {
|
||||
.min_w_0()
|
||||
.refine_style(&self.style)
|
||||
.when(segmented, |this| {
|
||||
this.bg(cx.theme().tab_background)
|
||||
this.bg(cx.theme().surface)
|
||||
.p_0p5()
|
||||
.rounded(cx.theme().radius)
|
||||
.rounded(px(theme::button_radius()))
|
||||
})
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
|
||||
+24
-53
@@ -1,31 +1,23 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, ClickEvent, Context, Div, Hsla, InteractiveElement as _, IntoElement, MouseButton,
|
||||
ParentElement as _, Pixels, Render, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
||||
Styled, Window, WindowControlArea, div, px,
|
||||
App, Div, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels,
|
||||
RenderOnce, Stateful, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, div,
|
||||
px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, plate_active};
|
||||
|
||||
use crate::{Icon, IconName, Sizable as _, h_flex};
|
||||
|
||||
pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
||||
const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
||||
|
||||
// The Windows control buttons have a fixed width of 35px.
|
||||
//
|
||||
// We don't need implementation the click event for the control buttons.
|
||||
// If user clicked in the bounds, the window event will be triggered.
|
||||
// Windows control buttons rely on the native window event, so no click handlers are needed.
|
||||
#[derive(IntoElement, Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
enum ControlIcon {
|
||||
Minimize,
|
||||
Restore,
|
||||
Maximize,
|
||||
Close {
|
||||
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
|
||||
},
|
||||
Close,
|
||||
}
|
||||
|
||||
impl ControlIcon {
|
||||
@@ -41,9 +33,8 @@ impl ControlIcon {
|
||||
Self::Maximize
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn close(on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>) -> Self {
|
||||
Self::Close { on_close_window }
|
||||
fn close() -> Self {
|
||||
Self::Close
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
@@ -51,7 +42,7 @@ impl ControlIcon {
|
||||
Self::Minimize => "minimize",
|
||||
Self::Restore => "restore",
|
||||
Self::Maximize => "maximize",
|
||||
Self::Close { .. } => "close",
|
||||
Self::Close => "close",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +51,7 @@ impl ControlIcon {
|
||||
Self::Minimize => IconName::WindowMinimize,
|
||||
Self::Restore => IconName::WindowRestore,
|
||||
Self::Maximize => IconName::WindowMaximize,
|
||||
Self::Close { .. } => IconName::WindowClose,
|
||||
Self::Close => IconName::WindowClose,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,18 +59,20 @@ impl ControlIcon {
|
||||
match self {
|
||||
Self::Minimize => WindowControlArea::Min,
|
||||
Self::Restore | Self::Maximize => WindowControlArea::Max,
|
||||
Self::Close { .. } => WindowControlArea::Close,
|
||||
Self::Close => WindowControlArea::Close,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_close(&self) -> bool {
|
||||
matches!(self, Self::Close { .. })
|
||||
matches!(self, Self::Close)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hover_fg(&self, cx: &App) -> Hsla {
|
||||
if self.is_close() {
|
||||
cx.theme().danger_foreground
|
||||
// White on the danger plate in both appearances, per bezel's
|
||||
// destructive pairing.
|
||||
gpui::white()
|
||||
} else {
|
||||
cx.theme().text
|
||||
}
|
||||
@@ -88,18 +81,18 @@ impl ControlIcon {
|
||||
#[inline]
|
||||
fn hover_bg(&self, cx: &App) -> Hsla {
|
||||
if self.is_close() {
|
||||
cx.theme().danger_background
|
||||
cx.theme().danger_strong
|
||||
} else {
|
||||
cx.theme().ghost_element_hover
|
||||
cx.theme().element_hover
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn active_bg(&self, cx: &mut App) -> Hsla {
|
||||
if self.is_close() {
|
||||
cx.theme().danger_active
|
||||
plate_active(cx.theme().danger_strong)
|
||||
} else {
|
||||
cx.theme().ghost_element_active
|
||||
cx.theme().element_active
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,11 +107,6 @@ impl RenderOnce for ControlIcon {
|
||||
let hover_bg = self.hover_bg(cx);
|
||||
let active_bg = self.active_bg(cx);
|
||||
|
||||
let on_close_window = match &self {
|
||||
ControlIcon::Close { on_close_window } => on_close_window.clone(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
div()
|
||||
.id(self.id())
|
||||
.flex()
|
||||
@@ -144,13 +132,7 @@ impl RenderOnce for ControlIcon {
|
||||
match icon {
|
||||
Self::Minimize => window.minimize_window(),
|
||||
Self::Restore | Self::Maximize => window.zoom_window(),
|
||||
Self::Close { .. } => {
|
||||
if let Some(f) = on_close_window.clone() {
|
||||
f(&ClickEvent::default(), window, cx);
|
||||
} else {
|
||||
window.remove_window();
|
||||
}
|
||||
}
|
||||
Self::Close => window.remove_window(),
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -159,15 +141,10 @@ impl RenderOnce for ControlIcon {
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) struct WindowControls {
|
||||
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
|
||||
}
|
||||
pub(crate) struct WindowControls;
|
||||
|
||||
pub(crate) fn window_controls() -> WindowControls {
|
||||
WindowControls {
|
||||
on_close_window: None,
|
||||
}
|
||||
WindowControls
|
||||
}
|
||||
|
||||
pub fn title_bar_drag_handlers(
|
||||
@@ -232,16 +209,10 @@ impl RenderOnce for WindowControls {
|
||||
} else {
|
||||
ControlIcon::maximize()
|
||||
})
|
||||
.child(ControlIcon::close(self.on_close_window))
|
||||
.child(ControlIcon::close())
|
||||
}
|
||||
}
|
||||
|
||||
struct TitleBarState {
|
||||
should_move: bool,
|
||||
}
|
||||
|
||||
impl Render for TitleBarState {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
+14
-17
@@ -1,10 +1,10 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window, div, relative,
|
||||
Window, px,
|
||||
};
|
||||
use gpui_base::Tooltip as BaseTooltip;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
|
||||
pub struct Tooltip {
|
||||
text: SharedString,
|
||||
@@ -18,20 +18,17 @@ impl Tooltip {
|
||||
|
||||
impl Render for Tooltip {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(
|
||||
BaseTooltip::new("tooltip")
|
||||
.font_family(".SystemUIFont")
|
||||
.m_3()
|
||||
.p_1p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text)
|
||||
.line_height(relative(1.25))
|
||||
.child(self.text.clone()),
|
||||
)
|
||||
BaseTooltip::new("tooltip")
|
||||
.font_family(cx.theme().font_sans.clone())
|
||||
.m_3()
|
||||
.p_1p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_overlay)
|
||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
||||
.rounded(px(control_radius()))
|
||||
.text_style(TextStyle::Caption)
|
||||
.text_color(cx.theme().text)
|
||||
.child(self.text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,6 @@ pub trait WindowExtension: Sized {
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Clear all notifications
|
||||
fn clear_notifications(&mut self, cx: &mut App);
|
||||
}
|
||||
|
||||
impl WindowExtension for Window {
|
||||
@@ -90,11 +87,4 @@ impl WindowExtension for Window {
|
||||
root.clear_notification_by_id::<T>(key, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notifications(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.clear_notifications(window, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user