Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec906839ed | ||
|
|
1c091db699 |
+24
-11
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
||||||
Styled as _, Window, deferred, div, ease_out_quint, px,
|
Styled as _, Window, deferred, div, ease_out_quint, px,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole, icon};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
|
|
||||||
use crate::common::DismissHandler;
|
use crate::common::DismissHandler;
|
||||||
@@ -33,7 +33,7 @@ pub struct Alert {
|
|||||||
title: Option<SharedString>,
|
title: Option<SharedString>,
|
||||||
message: Option<SharedString>,
|
message: Option<SharedString>,
|
||||||
severity: AlertSeverity,
|
severity: AlertSeverity,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
show_icon: bool,
|
show_icon: bool,
|
||||||
on_dismiss: Option<DismissHandler>,
|
on_dismiss: Option<DismissHandler>,
|
||||||
}
|
}
|
||||||
@@ -78,9 +78,12 @@ impl Alert {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overrides the severity-derived icon with this glyph.
|
/// Overrides the severity-derived icon.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +119,12 @@ impl RenderOnce for Alert {
|
|||||||
AlertSeverity::Danger => theme.ramps.destructive,
|
AlertSeverity::Danger => theme.ramps.destructive,
|
||||||
};
|
};
|
||||||
|
|
||||||
let icon_role = self.icon.unwrap_or(match self.severity {
|
let icon_glyph = self.icon.unwrap_or_else(|| {
|
||||||
|
icon(match self.severity {
|
||||||
AlertSeverity::Info => IconRole::Info,
|
AlertSeverity::Info => IconRole::Info,
|
||||||
AlertSeverity::Success => IconRole::Check,
|
AlertSeverity::Success => IconRole::Check,
|
||||||
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
|
AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning,
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let foreground = theme.colors.foreground;
|
let foreground = theme.colors.foreground;
|
||||||
@@ -158,7 +163,7 @@ impl RenderOnce for Alert {
|
|||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.h(window.line_height())
|
.h(window.line_height())
|
||||||
.child(icon(icon_role).size(icon_size).color(ramp.base)),
|
.child(icon_glyph.size(icon_size).color(ramp.base)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
@@ -260,11 +265,19 @@ impl Alerts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dismisses every alert.
|
/// Dismisses every alert, running each entry's `on_dismiss` handler.
|
||||||
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
pub fn clear(&mut self, cx: &mut Context<Self>) {
|
||||||
if !self.entries.is_empty() {
|
if self.entries.is_empty() {
|
||||||
self.entries.clear();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = std::mem::take(&mut self.entries);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
if let Some(on_dismiss) = entry.on_dismiss {
|
||||||
|
on_dismiss(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +317,7 @@ impl RenderOnce for AlertsView {
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let enter_duration = theme.motion.overlay;
|
let enter_duration = theme.motion.overlay;
|
||||||
|
|
||||||
let entries: Vec<AlertEntry> = self.alerts.read(cx).entries().to_vec();
|
let entries = self.alerts.read(cx).entries();
|
||||||
|
|
||||||
deferred(
|
deferred(
|
||||||
div()
|
div()
|
||||||
|
|||||||
+11
-18
@@ -4,7 +4,10 @@ use gpui::{
|
|||||||
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
||||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, div,
|
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Oklch, Size, ThemeVariant};
|
use gpui_native_theme::{
|
||||||
|
ActiveTheme as _, Oklch, PIXEL_AVATAR_DARK_FACTOR, PIXEL_AVATAR_DARK_FLOOR,
|
||||||
|
PIXEL_AVATAR_LIGHT_CAP, PIXEL_AVATAR_LIGHT_LIFT, PIXEL_AVATAR_TINT_ALPHA, Size, ThemeVariant,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::common::{Sizable, StyledExt, h_flex};
|
use crate::common::{Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
@@ -108,12 +111,10 @@ impl RenderOnce for Avatar {
|
|||||||
.refine_style(&self.style);
|
.refine_style(&self.style);
|
||||||
|
|
||||||
match self.picture {
|
match self.picture {
|
||||||
Some(url) => base.child(gpui::img(url).size_full().rounded(radius)),
|
Some(url) => base.child(gpui::img(url).size_full()),
|
||||||
None => base.child(PixelAvatar {
|
None => base.child(PixelAvatar {
|
||||||
seed: fnv1a(self.id.as_bytes()),
|
seed: fnv1a(self.id.as_bytes()),
|
||||||
side,
|
side,
|
||||||
radius,
|
|
||||||
style: self.style,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -125,14 +126,6 @@ impl RenderOnce for Avatar {
|
|||||||
struct PixelAvatar {
|
struct PixelAvatar {
|
||||||
seed: u64,
|
seed: u64,
|
||||||
side: gpui::Pixels,
|
side: gpui::Pixels,
|
||||||
radius: gpui::Pixels,
|
|
||||||
style: StyleRefinement,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl gpui::Styled for PixelAvatar {
|
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
|
||||||
&mut self.style
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for PixelAvatar {
|
impl RenderOnce for PixelAvatar {
|
||||||
@@ -147,13 +140,13 @@ impl RenderOnce for PixelAvatar {
|
|||||||
|
|
||||||
let shade = if theme.variant == ThemeVariant::Dark {
|
let shade = if theme.variant == ThemeVariant::Dark {
|
||||||
Oklch {
|
Oklch {
|
||||||
l: (accent.l * 1.6).min(0.95),
|
l: (accent.l * PIXEL_AVATAR_LIGHT_LIFT).min(PIXEL_AVATAR_LIGHT_CAP),
|
||||||
..Oklch::from_hsla(main)
|
..Oklch::from_hsla(main)
|
||||||
}
|
}
|
||||||
.to_hsla()
|
.to_hsla()
|
||||||
} else {
|
} else {
|
||||||
Oklch {
|
Oklch {
|
||||||
l: (accent.l * 0.45).max(0.18),
|
l: (accent.l * PIXEL_AVATAR_DARK_FACTOR).max(PIXEL_AVATAR_DARK_FLOOR),
|
||||||
..Oklch::from_hsla(main)
|
..Oklch::from_hsla(main)
|
||||||
}
|
}
|
||||||
.to_hsla()
|
.to_hsla()
|
||||||
@@ -182,11 +175,11 @@ impl RenderOnce for PixelAvatar {
|
|||||||
.grid_rows(GRID_SIZE as u16)
|
.grid_rows(GRID_SIZE as u16)
|
||||||
.size(self.side)
|
.size(self.side)
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.rounded(self.radius)
|
.bg(gpui::Hsla {
|
||||||
.overflow_hidden()
|
a: PIXEL_AVATAR_TINT_ALPHA,
|
||||||
.bg(gpui::Hsla { a: 0.16, ..main })
|
..main
|
||||||
|
})
|
||||||
.children(cells)
|
.children(cells)
|
||||||
.refine_style(&self.style)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-10
@@ -4,9 +4,9 @@ use gpui::prelude::FluentBuilder as _;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
|
AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
|
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
|
||||||
hsla, relative,
|
relative, transparent_black,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||||
use gpui_native_utils::{focus_ring, record_anchor_bounds};
|
use gpui_native_utils::{focus_ring, record_anchor_bounds};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -38,7 +38,7 @@ pub enum ButtonVariant {
|
|||||||
pub struct Button {
|
pub struct Button {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
size: Size,
|
size: Size,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
@@ -82,8 +82,11 @@ impl Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon beside the label.
|
/// Shows an icon beside the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,9 +202,9 @@ impl RenderOnce for Button {
|
|||||||
match background {
|
match background {
|
||||||
Some(fill) => (fill, hover, pressed),
|
Some(fill) => (fill, hover, pressed),
|
||||||
None => (
|
None => (
|
||||||
hsla(0.0, 0.0, 0.0, 0.0),
|
transparent_black(),
|
||||||
hsla(0.0, 0.0, 0.0, 0.0),
|
transparent_black(),
|
||||||
hsla(0.0, 0.0, 0.0, 0.0),
|
transparent_black(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -236,8 +239,8 @@ impl RenderOnce for Button {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!self.loading, |button| {
|
.when(!self.loading, |button| {
|
||||||
button.when_some(self.icon, |button, role| {
|
button.when_some(self.icon, |button, icon| {
|
||||||
button.child(icon(role).size(metrics.icon_size).color(foreground))
|
button.child(icon.size(metrics.icon_size).color(foreground))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.when_some(self.label, |button, label| button.child(label))
|
.when_some(self.label, |button, label| button.child(label))
|
||||||
|
|||||||
+21
-29
@@ -10,13 +10,13 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, Context, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
|
MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role, ScrollHandle,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription, Task,
|
||||||
Subscription, Task, Window, actions, anchored, canvas, deferred, div, point, px,
|
Window, actions, anchored, canvas, deferred, div, point, px,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{IconRole, icon};
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, POPUP_MAX_HEIGHT, Size};
|
||||||
|
|
||||||
use crate::common::{Sizable, StyledExt as _, h_flex, v_flex};
|
use crate::common::{Sizable, StyledExt as _, h_flex, v_flex};
|
||||||
use crate::input::{Input, InputState, InputStateEvent};
|
use crate::input::{Input, InputState, InputStateEvent};
|
||||||
@@ -177,9 +177,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emitted when the combobox's value changes.
|
|
||||||
pub struct ComboboxChanged;
|
|
||||||
|
|
||||||
/// The stateful combobox.
|
/// The stateful combobox.
|
||||||
pub struct ComboboxState<T>
|
pub struct ComboboxState<T>
|
||||||
where
|
where
|
||||||
@@ -209,8 +206,6 @@ where
|
|||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> EventEmitter<ComboboxChanged> for ComboboxState<T> where T: Clone + PartialEq + 'static {}
|
|
||||||
|
|
||||||
impl<T> Sizable for ComboboxState<T>
|
impl<T> Sizable for ComboboxState<T>
|
||||||
where
|
where
|
||||||
T: Clone + PartialEq + 'static,
|
T: Clone + PartialEq + 'static,
|
||||||
@@ -316,8 +311,8 @@ where
|
|||||||
.map(|(_, label)| label.clone())
|
.map(|(_, label)| label.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recomputes `visible` for `query` and hands the survivors' labels back.
|
/// Recomputes `visible` for `query` and clamps the highlight onto it.
|
||||||
fn refilter(&mut self, query: &str) -> Vec<SharedString> {
|
fn refilter(&mut self, query: &str) {
|
||||||
let filter = self.filter.clone();
|
let filter = self.filter.clone();
|
||||||
self.visible = self
|
self.visible = self
|
||||||
.options
|
.options
|
||||||
@@ -326,10 +321,7 @@ where
|
|||||||
.filter(|(_, (value, label))| filter(query, value, label))
|
.filter(|(_, (value, label))| filter(query, value, label))
|
||||||
.map(|(index, _)| index)
|
.map(|(index, _)| index)
|
||||||
.collect();
|
.collect();
|
||||||
self.visible
|
self.highlight = self.highlight.filter(|row| *row < self.visible.len());
|
||||||
.iter()
|
|
||||||
.map(|index| self.options[*index].1.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_text(&self, cx: &Context<Self>) -> String {
|
fn query_text(&self, cx: &Context<Self>) -> String {
|
||||||
@@ -349,7 +341,6 @@ where
|
|||||||
});
|
});
|
||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
cx.emit(ComboboxChanged);
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +366,7 @@ where
|
|||||||
fn text_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn text_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let query = self.query_text(cx);
|
let query = self.query_text(cx);
|
||||||
self.refilter(&query);
|
self.refilter(&query);
|
||||||
self.highlight = (!self.visible.is_empty()).then_some(0usize);
|
self.highlight = Some(0);
|
||||||
self.set_value(None, window, cx);
|
self.set_value(None, window, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -386,7 +377,7 @@ where
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.refilter("");
|
self.refilter("");
|
||||||
self.highlight = (!self.visible.is_empty()).then_some(0usize);
|
self.highlight = Some(0);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,10 +418,7 @@ where
|
|||||||
if self.visible.is_empty() {
|
if self.visible.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let current = self
|
let current = self.highlight.filter(|row| *row < self.visible.len());
|
||||||
.highlight
|
|
||||||
.and_then(|row| self.visible.get(row).copied())
|
|
||||||
.and_then(|option_ix| self.visible.iter().position(|&row| row == option_ix));
|
|
||||||
let next = match current {
|
let next = match current {
|
||||||
Some(row) => {
|
Some(row) => {
|
||||||
let row = (row as i32 + delta).rem_euclid(self.visible.len() as i32);
|
let row = (row as i32 + delta).rem_euclid(self.visible.len() as i32);
|
||||||
@@ -509,10 +497,14 @@ where
|
|||||||
state.update(cx, |state, cx| {
|
state.update(cx, |state, cx| {
|
||||||
if state.highlight.is_some() {
|
if state.highlight.is_some() {
|
||||||
state.highlight = None;
|
state.highlight = None;
|
||||||
|
} else {
|
||||||
|
if state.selected.is_some() {
|
||||||
|
state.refilter("");
|
||||||
} else {
|
} else {
|
||||||
let query = state.query.read(cx).value().to_string();
|
let query = state.query.read(cx).value().to_string();
|
||||||
state.refilter(&query);
|
state.refilter(&query);
|
||||||
state.highlight = (!state.visible.is_empty()).then_some(0usize);
|
}
|
||||||
|
state.highlight = Some(0);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
@@ -600,8 +592,8 @@ where
|
|||||||
div()
|
div()
|
||||||
.id(("option", row))
|
.id(("option", row))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-option-{row}");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-option-{row}")
|
||||||
})
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -663,7 +655,7 @@ where
|
|||||||
ScrollArea::new((id.clone(), "rows"))
|
ScrollArea::new((id.clone(), "rows"))
|
||||||
.handle(scroll.read(cx).clone())
|
.handle(scroll.read(cx).clone())
|
||||||
.axes(crate::scroll_area::ScrollAxes::Vertical)
|
.axes(crate::scroll_area::ScrollAxes::Vertical)
|
||||||
.max_h(px(320.))
|
.max_h(POPUP_MAX_HEIGHT)
|
||||||
.children(rows)
|
.children(rows)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
@@ -677,8 +669,8 @@ where
|
|||||||
let panel = v_flex()
|
let panel = v_flex()
|
||||||
.id((id.clone(), "panel"))
|
.id((id.clone(), "panel"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-panel");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-panel")
|
||||||
})
|
})
|
||||||
.w(popup_width)
|
.w(popup_width)
|
||||||
.p_1()
|
.p_1()
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
//! The command palette: a modal query field over the application's actions.
|
//! The command palette: a modal query field over the application's actions.
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement,
|
AnyElement, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
|
MouseButton, ParentElement as _, Render, Role, ScrollHandle, SharedString,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Window,
|
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
|
||||||
actions, canvas, div, px, relative,
|
div, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT, Size};
|
||||||
|
|
||||||
use crate::common::{h_flex, v_flex};
|
use crate::common::{h_flex, v_flex};
|
||||||
use crate::input::{Input, InputState, InputStateEvent};
|
use crate::input::{Input, InputState, InputStateEvent};
|
||||||
@@ -50,7 +49,7 @@ pub struct Command {
|
|||||||
/// Gray text on the row's right edge, e.g. the keybinding.
|
/// Gray text on the row's right edge, e.g. the keybinding.
|
||||||
pub hint: Option<SharedString>,
|
pub hint: Option<SharedString>,
|
||||||
/// Leading glyph shown before the label.
|
/// Leading glyph shown before the label.
|
||||||
pub icon: Option<IconRole>,
|
pub icon: Option<Icon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
@@ -65,8 +64,11 @@ impl Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon before the label.
|
/// Shows an icon before the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +324,7 @@ impl Render for CommandPaletteState {
|
|||||||
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
|
let query_focus = self.query.read(cx).focus_handle();
|
||||||
|
|
||||||
let rows: Vec<AnyElement> = self
|
let rows: Vec<AnyElement> = self
|
||||||
.matches
|
.matches
|
||||||
@@ -333,10 +336,10 @@ impl Render for CommandPaletteState {
|
|||||||
let row_ix = row;
|
let row_ix = row;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(("command", row))
|
.id((ElementId::from(command.id.clone()), "command"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-command-{row}");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-command-{row}")
|
||||||
})
|
})
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.px_2()
|
.px_2()
|
||||||
@@ -361,8 +364,8 @@ impl Render for CommandPaletteState {
|
|||||||
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
|
state.update(cx, |state, cx| state.choose_row(row_ix, window, cx));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when_some(command.icon, |row, role| {
|
.when_some(command.icon.clone(), |row, icon| {
|
||||||
row.child(icon(role).size(metrics.icon_size).color(colors.foreground))
|
row.child(icon.size(metrics.icon_size).color(colors.foreground))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -401,19 +404,16 @@ impl Render for CommandPaletteState {
|
|||||||
ScrollArea::new((id.clone(), "rows"))
|
ScrollArea::new((id.clone(), "rows"))
|
||||||
.handle(scroll.read(cx).clone())
|
.handle(scroll.read(cx).clone())
|
||||||
.axes(ScrollAxes::Vertical)
|
.axes(ScrollAxes::Vertical)
|
||||||
.max_h(px(320.))
|
.max_h(POPUP_MAX_HEIGHT)
|
||||||
.children(rows)
|
.children(rows)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
|
|
||||||
let query_focus = self.query.read(cx).focus_handle();
|
|
||||||
|
|
||||||
let panel = v_flex()
|
let panel = v_flex()
|
||||||
.id((id.clone(), "panel"))
|
.id((id.clone(), "panel"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-panel");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-panel")
|
||||||
})
|
})
|
||||||
.key_context(COMMAND_PALETTE_CONTEXT)
|
.key_context(COMMAND_PALETTE_CONTEXT)
|
||||||
.role(Role::Dialog)
|
.role(Role::Dialog)
|
||||||
@@ -460,20 +460,14 @@ impl Render for CommandPaletteState {
|
|||||||
)
|
)
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(|_, _, _| {}, {
|
||||||
{
|
|
||||||
let cell = panel_bounds.clone();
|
|
||||||
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let focus = query_focus.clone();
|
let focus = query_focus.clone();
|
||||||
move |_, _, window, cx| {
|
move |_, _, window, cx| {
|
||||||
if !focus.contains_focused(window, cx) {
|
if !focus.contains_focused(window, cx) {
|
||||||
window.focus(&focus, cx);
|
window.focus(&focus, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0(),
|
.inset_0(),
|
||||||
);
|
);
|
||||||
@@ -485,19 +479,13 @@ impl Render for CommandPaletteState {
|
|||||||
.occlude()
|
.occlude()
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.on_mouse_down(MouseButton::Left, {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
let panel_bounds = panel_bounds.clone();
|
move |_, _window, cx| {
|
||||||
move |event: &MouseDownEvent, _window, cx| {
|
|
||||||
let on_panel = panel_bounds
|
|
||||||
.borrow()
|
|
||||||
.is_some_and(|bounds| bounds.contains(&event.position));
|
|
||||||
if on_panel {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.update(cx, |state, cx| state.close(cx));
|
state.update(cx, |state, cx| state.close(cx));
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
deferred(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0()
|
.inset_0()
|
||||||
@@ -506,7 +494,8 @@ impl Render for CommandPaletteState {
|
|||||||
.items_start()
|
.items_start()
|
||||||
.pt_10()
|
.pt_10()
|
||||||
.child(scrim)
|
.child(scrim)
|
||||||
.child(panel)
|
.child(panel),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use std::cell::RefCell;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt as _, AnyElement, App, Bounds, ElementId, FontWeight,
|
Animation, AnimationExt as _, AnyElement, App, ElementId, FontWeight, InteractiveElement as _,
|
||||||
InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
|
IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString,
|
||||||
ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative,
|
||||||
Styled as _, Window, canvas, div, ease_out_quint, relative,
|
|
||||||
};
|
};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
@@ -16,9 +14,6 @@ use crate::common::v_flex;
|
|||||||
pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
pub type ChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||||
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
|
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
|
||||||
type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>;
|
type ActionsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<AnyElement>>;
|
||||||
/// Bounds captured during the last paint, shared with the scrim listener.
|
|
||||||
type BoundsCell = Rc<RefCell<Option<Bounds<Pixels>>>>;
|
|
||||||
|
|
||||||
/// A centred panel over a dim scrim: the platform's modal.
|
/// A centred panel over a dim scrim: the platform's modal.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Dialog {
|
pub struct Dialog {
|
||||||
@@ -165,7 +160,6 @@ impl RenderOnce for Dialog {
|
|||||||
|
|
||||||
let content = self.content.map(|build| build(window, cx));
|
let content = self.content.map(|build| build(window, cx));
|
||||||
let actions = self.actions.map(|build| build(window, cx));
|
let actions = self.actions.map(|build| build(window, cx));
|
||||||
let panel_bounds: BoundsCell = Rc::new(RefCell::new(None));
|
|
||||||
|
|
||||||
let panel = v_flex()
|
let panel = v_flex()
|
||||||
.id("dialog-panel")
|
.id("dialog-panel")
|
||||||
@@ -211,19 +205,13 @@ impl RenderOnce for Dialog {
|
|||||||
panel.child(div().flex().justify_end().gap_2().children(actions))
|
panel.child(div().flex().justify_end().gap_2().children(actions))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(|_, _, _| {}, {
|
||||||
{
|
|
||||||
let cell = panel_bounds.clone();
|
|
||||||
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
move |_, _, window, cx| {
|
move |_, _, window, cx| {
|
||||||
if !focus.contains_focused(window, cx) {
|
if !focus.contains_focused(window, cx) {
|
||||||
window.focus(&focus, cx);
|
window.focus(&focus, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0(),
|
.inset_0(),
|
||||||
);
|
);
|
||||||
@@ -235,15 +223,8 @@ impl RenderOnce for Dialog {
|
|||||||
.occlude()
|
.occlude()
|
||||||
.when(!alert, |scrim| {
|
.when(!alert, |scrim| {
|
||||||
scrim.on_mouse_down(MouseButton::Left, {
|
scrim.on_mouse_down(MouseButton::Left, {
|
||||||
let panel_bounds = panel_bounds.clone();
|
|
||||||
let on_change = on_change.clone();
|
let on_change = on_change.clone();
|
||||||
move |event: &MouseDownEvent, _window, cx| {
|
move |_, _window, cx| {
|
||||||
let on_panel = panel_bounds
|
|
||||||
.borrow()
|
|
||||||
.is_some_and(|bounds| bounds.contains(&event.position));
|
|
||||||
if on_panel {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(on_change) = &on_change {
|
if let Some(on_change) = &on_change {
|
||||||
on_change(false, _window, cx);
|
on_change(false, _window, cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use gpui::prelude::FluentBuilder as _;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
App, ClickEvent, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
|
App, ClickEvent, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
|
||||||
MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString,
|
MouseButton, MouseDownEvent, ParentElement as _, RenderOnce, Role, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, hsla, relative,
|
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole, icon};
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||||
use gpui_native_utils::{focus_handle, focus_ring};
|
use gpui_native_utils::{focus_handle, focus_ring};
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<MenuItem>>;
|
|||||||
pub struct DropdownMenu {
|
pub struct DropdownMenu {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
size: Size,
|
size: Size,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
@@ -73,8 +73,11 @@ impl DropdownMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon beside the label.
|
/// Shows an icon beside the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +196,7 @@ impl RenderOnce for DropdownMenu {
|
|||||||
let border = if show_border {
|
let border = if show_border {
|
||||||
colors.border
|
colors.border
|
||||||
} else {
|
} else {
|
||||||
background.unwrap_or_else(|| hsla(0.0, 0.0, 0.0, 0.0))
|
background.unwrap_or_else(gpui::transparent_black)
|
||||||
};
|
};
|
||||||
|
|
||||||
let split = self.on_click.is_some();
|
let split = self.on_click.is_some();
|
||||||
@@ -207,7 +210,8 @@ impl RenderOnce for DropdownMenu {
|
|||||||
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
||||||
let focus = focus_handle(&self.id, window, cx).tab_stop(self.tab_stop);
|
let focus = focus_handle(&self.id, window, cx).tab_stop(self.tab_stop);
|
||||||
// How the menu was last opened, read by the frame that mounts the panel.
|
// How the menu was last opened, read by the frame that mounts the panel.
|
||||||
let keyboard_open = window.use_keyed_state("dropdown-open-origin", cx, |_, _| false);
|
let keyboard_open =
|
||||||
|
window.use_keyed_state((self.id.clone(), "dropdown-open-origin"), cx, |_, _| false);
|
||||||
|
|
||||||
let on_open: ChangeHandler = {
|
let on_open: ChangeHandler = {
|
||||||
let focus = focus.clone();
|
let focus = focus.clone();
|
||||||
@@ -266,8 +270,8 @@ impl RenderOnce for DropdownMenu {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(!self.loading, |zone| {
|
.when(!self.loading, |zone| {
|
||||||
zone.when_some(self.icon, |zone, role| {
|
zone.when_some(self.icon, |zone, icon| {
|
||||||
zone.child(icon(role).size(metrics.icon_size).color(foreground))
|
zone.child(icon.size(metrics.icon_size).color(foreground))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.when_some(self.label, |zone, label| zone.child(label));
|
.when_some(self.label, |zone, label| zone.child(label));
|
||||||
@@ -421,10 +425,10 @@ impl RenderOnce for DropdownMenu {
|
|||||||
|
|
||||||
let items = if self.open {
|
let items = if self.open {
|
||||||
self.items
|
self.items
|
||||||
.map(|build| build(window, cx))
|
.map(|build| Rc::from(build(window, cx)))
|
||||||
.unwrap_or_default()
|
.unwrap_or_else(|| Rc::from([]))
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Rc::from([])
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(panel) = anchored_menu_panel(
|
if let Some(panel) = anchored_menu_panel(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use gpui::{
|
|||||||
App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _,
|
App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _,
|
||||||
Window, div, relative,
|
Window, div, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
|
|
||||||
use crate::common::{StyledExt, v_flex};
|
use crate::common::{StyledExt, v_flex};
|
||||||
@@ -11,7 +11,7 @@ use crate::common::{StyledExt, v_flex};
|
|||||||
/// The centered "nothing here yet" panel for lists, tables, and searches.
|
/// The centered "nothing here yet" panel for lists, tables, and searches.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct EmptyState {
|
pub struct EmptyState {
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
headline: SharedString,
|
headline: SharedString,
|
||||||
hint: Option<SharedString>,
|
hint: Option<SharedString>,
|
||||||
action: Option<gpui::AnyElement>,
|
action: Option<gpui::AnyElement>,
|
||||||
@@ -34,8 +34,11 @@ impl EmptyState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a large muted icon above the headline.
|
/// Sets a large muted icon above the headline.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +81,11 @@ impl RenderOnce for EmptyState {
|
|||||||
.justify_center()
|
.justify_center()
|
||||||
.text_center()
|
.text_center()
|
||||||
.p_4()
|
.p_4()
|
||||||
.when_some(self.icon, |state, role| {
|
.when_some(self.icon, |state, icon| {
|
||||||
state.child(
|
state.child(
|
||||||
div()
|
div()
|
||||||
.mb_2()
|
.mb_2()
|
||||||
.child(icon(role).size(illustration).color(muted_foreground)),
|
.child(icon.size(illustration).color(muted_foreground)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ impl RenderOnce for Field {
|
|||||||
|
|
||||||
let title = div()
|
let title = div()
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-label");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-label")
|
||||||
})
|
})
|
||||||
.text_size(metrics.text_size)
|
.text_size(metrics.text_size)
|
||||||
.child(self.label.clone());
|
.child(self.label.clone());
|
||||||
@@ -105,16 +105,16 @@ impl RenderOnce for Field {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-control");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-control")
|
||||||
})
|
})
|
||||||
.children(self.control);
|
.children(self.control);
|
||||||
|
|
||||||
let error = self.error.map(|error| {
|
let error = self.error.map(|error| {
|
||||||
div()
|
div()
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = format!("{id}-error");
|
let id = id.clone();
|
||||||
move || name
|
move || format!("{id}-error")
|
||||||
})
|
})
|
||||||
.text_size(metrics.caption_size)
|
.text_size(metrics.caption_size)
|
||||||
.text_color(error_color)
|
.text_color(error_color)
|
||||||
|
|||||||
+103
-82
@@ -1,4 +1,3 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -16,7 +15,10 @@ use gpui::{
|
|||||||
WrappedLine, actions, canvas, div, fill, point, px,
|
WrappedLine, actions, canvas, div, fill, point, px,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::IconRole;
|
use gpui_native_icons::IconRole;
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size};
|
use gpui_native_theme::{
|
||||||
|
ActiveTheme as _, BORDER_WIDTH, CARET_WIDTH, ColorTokens, Size, UNDERLINE_WIDTH,
|
||||||
|
WHEEL_LINE_HEIGHT,
|
||||||
|
};
|
||||||
use gpui_native_utils::focus_ring;
|
use gpui_native_utils::focus_ring;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariant};
|
use crate::button::{Button, ButtonVariant};
|
||||||
@@ -29,7 +31,7 @@ const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
|||||||
/// How often the focused caret blinks.
|
/// How often the focused caret blinks.
|
||||||
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
/// Pixel height of one scroll "line" for wheel scrolling.
|
/// Pixel height of one scroll "line" for wheel scrolling.
|
||||||
const SCROLL_LINES_HEIGHT: f32 = 20.0;
|
const SCROLL_LINES_HEIGHT: f32 = WHEEL_LINE_HEIGHT;
|
||||||
/// The row count a multiline field shows until the app sets one.
|
/// The row count a multiline field shows until the app sets one.
|
||||||
const DEFAULT_ROWS: usize = 4;
|
const DEFAULT_ROWS: usize = 4;
|
||||||
/// Maximum undo entries kept; the oldest drops off the front.
|
/// Maximum undo entries kept; the oldest drops off the front.
|
||||||
@@ -270,7 +272,7 @@ impl InputBindings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Collects the table into keybindings.
|
/// Collects the table into keybindings.
|
||||||
pub fn into_bindings(self) -> Vec<KeyBinding> {
|
pub(crate) fn into_bindings(self) -> Vec<KeyBinding> {
|
||||||
let mut bindings: Vec<KeyBinding> = [
|
let mut bindings: Vec<KeyBinding> = [
|
||||||
self.escape,
|
self.escape,
|
||||||
self.submit,
|
self.submit,
|
||||||
@@ -491,9 +493,9 @@ pub struct InputState {
|
|||||||
focused: bool,
|
focused: bool,
|
||||||
focus_handle: gpui::FocusHandle,
|
focus_handle: gpui::FocusHandle,
|
||||||
/// The key-down handler, bound to this entity with `cx.listener`.
|
/// The key-down handler, bound to this entity with `cx.listener`.
|
||||||
key_down_handler: Option<KeyDownHandler>,
|
key_down_handler: KeyDownHandler,
|
||||||
/// The mouse-down handler, bound to this entity with `cx.listener`.
|
/// The mouse-down handler, bound to this entity with `cx.listener`.
|
||||||
mouse_down_handler: Option<MouseDownHandler>,
|
mouse_down_handler: MouseDownHandler,
|
||||||
/// Whether a drag-select is in progress.
|
/// Whether a drag-select is in progress.
|
||||||
dragging: bool,
|
dragging: bool,
|
||||||
/// Patch history. `redo_stack` holds undone entries awaiting redo.
|
/// Patch history. `redo_stack` holds undone entries awaiting redo.
|
||||||
@@ -520,9 +522,9 @@ pub struct InputState {
|
|||||||
/// Bumped on every edit.
|
/// Bumped on every edit.
|
||||||
caret_epoch: usize,
|
caret_epoch: usize,
|
||||||
/// When the current caret epoch started.
|
/// When the current caret epoch started.
|
||||||
caret_epoch_started: Option<Instant>,
|
caret_epoch_started: Instant,
|
||||||
/// The shaped line and element bounds, recorded during each paint.
|
/// The shaped line and element bounds, recorded during each paint.
|
||||||
layout: Rc<RefCell<Option<InputLayout>>>,
|
layout: Option<InputLayout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<InputStateEvent> for InputState {}
|
impl EventEmitter<InputStateEvent> for InputState {}
|
||||||
@@ -536,16 +538,14 @@ impl InputState {
|
|||||||
marked_range: None,
|
marked_range: None,
|
||||||
focused: false,
|
focused: false,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
key_down_handler: Some(Rc::new(cx.listener(
|
key_down_handler: Rc::new(cx.listener(|state, event: &KeyDownEvent, window, cx| {
|
||||||
|state, event: &KeyDownEvent, window, cx| {
|
|
||||||
state.handle_key_down(event, window, cx);
|
state.handle_key_down(event, window, cx);
|
||||||
},
|
})),
|
||||||
))),
|
mouse_down_handler: Rc::new(cx.listener(
|
||||||
mouse_down_handler: Some(Rc::new(cx.listener(
|
|
||||||
|state, event: &MouseDownEvent, window, cx| {
|
|state, event: &MouseDownEvent, window, cx| {
|
||||||
state.handle_mouse_down(event, window, cx);
|
state.handle_mouse_down(event, window, cx);
|
||||||
},
|
},
|
||||||
))),
|
)),
|
||||||
dragging: false,
|
dragging: false,
|
||||||
undo_stack: Vec::new(),
|
undo_stack: Vec::new(),
|
||||||
redo_stack: Vec::new(),
|
redo_stack: Vec::new(),
|
||||||
@@ -559,8 +559,8 @@ impl InputState {
|
|||||||
scroll_offset: point(Pixels::ZERO, Pixels::ZERO),
|
scroll_offset: point(Pixels::ZERO, Pixels::ZERO),
|
||||||
scroll_to_caret_pending: true,
|
scroll_to_caret_pending: true,
|
||||||
caret_epoch: 0,
|
caret_epoch: 0,
|
||||||
caret_epoch_started: Some(Instant::now()),
|
caret_epoch_started: Instant::now(),
|
||||||
layout: Rc::new(RefCell::new(None)),
|
layout: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,12 +696,7 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which keystroke submits a multiline field.
|
// Whether the painted line masks the buffer.
|
||||||
pub fn submit_on(&self) -> SubmitOn {
|
|
||||||
self.submit_on
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the painted line masks the buffer.
|
|
||||||
fn masked(&self) -> bool {
|
fn masked(&self) -> bool {
|
||||||
self.password && !self.revealed
|
self.password && !self.revealed
|
||||||
}
|
}
|
||||||
@@ -711,11 +706,21 @@ impl InputState {
|
|||||||
self.redo_stack.clear();
|
self.redo_stack.clear();
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if let Some(last) = self.undo_stack.last_mut()
|
if let Some(last) = self.undo_stack.last_mut() {
|
||||||
&& now.duration_since(last.timestamp) < self.group_interval
|
|
||||||
{
|
|
||||||
let last_end = last.range.start + last.new_len;
|
let last_end = last.range.start + last.new_len;
|
||||||
|
|
||||||
|
if self.marked_range.is_some() {
|
||||||
|
// A pre-edit step overlapping the previous entry's region.
|
||||||
|
if range.start >= last.range.start {
|
||||||
|
last.new_len += new_len;
|
||||||
|
let replaced = range.len();
|
||||||
|
last.new_len -= replaced;
|
||||||
|
last.timestamp = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.duration_since(last.timestamp) < self.group_interval {
|
||||||
// Typing that continues right after the previous edit.
|
// Typing that continues right after the previous edit.
|
||||||
if range.start == last_end {
|
if range.start == last_end {
|
||||||
last.new_len += new_len;
|
last.new_len += new_len;
|
||||||
@@ -739,6 +744,7 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.undo_stack.push(HistoryEntry {
|
self.undo_stack.push(HistoryEntry {
|
||||||
range: range.start..range.start + new_len,
|
range: range.start..range.start + new_len,
|
||||||
@@ -769,7 +775,7 @@ impl InputState {
|
|||||||
self.scroll_to_caret_pending = true;
|
self.scroll_to_caret_pending = true;
|
||||||
self.goal_x = None;
|
self.goal_x = None;
|
||||||
self.caret_epoch += 1;
|
self.caret_epoch += 1;
|
||||||
self.caret_epoch_started = Some(Instant::now());
|
self.caret_epoch_started = Instant::now();
|
||||||
window.notify_text_input_changed(&self.focus_handle);
|
window.notify_text_input_changed(&self.focus_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,20 +787,38 @@ impl InputState {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.multiline {
|
if self.multiline {
|
||||||
self.scroll_offset.y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO);
|
let next_y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO);
|
||||||
} else {
|
if next_y == self.scroll_offset.y {
|
||||||
self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
|
return self.multiline_scrollable();
|
||||||
}
|
}
|
||||||
|
self.scroll_offset.y = next_y;
|
||||||
|
} else {
|
||||||
|
let next_x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
|
||||||
|
if next_x == self.scroll_offset.x {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.scroll_offset.x = next_x;
|
||||||
|
}
|
||||||
|
|
||||||
let scrollable = self
|
let scrollable = self
|
||||||
.layout
|
.layout
|
||||||
.borrow()
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|layout| layout.content_height > layout.visible_height);
|
.is_some_and(|layout| layout.content_height > layout.visible_height);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
window.notify_text_input_changed(&self.focus_handle);
|
window.notify_text_input_changed(&self.focus_handle);
|
||||||
self.multiline && scrollable
|
self.multiline && scrollable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a multiline field's content overflows vertically.
|
||||||
|
fn multiline_scrollable(&self) -> bool {
|
||||||
|
self.multiline
|
||||||
|
&& self
|
||||||
|
.layout
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|layout| layout.content_height > layout.visible_height)
|
||||||
|
}
|
||||||
|
|
||||||
/// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint.
|
/// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint.
|
||||||
fn sync_focus(&mut self, focused: bool, cx: &mut Context<Self>) {
|
fn sync_focus(&mut self, focused: bool, cx: &mut Context<Self>) {
|
||||||
if self.focused == focused {
|
if self.focused == focused {
|
||||||
@@ -802,11 +826,9 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
self.focused = focused;
|
self.focused = focused;
|
||||||
|
|
||||||
// A fresh focus starts a fresh blink epoch, so the caret shows solid
|
|
||||||
// for one interval before blinking resumes.
|
|
||||||
if focused {
|
if focused {
|
||||||
self.caret_epoch += 1;
|
self.caret_epoch += 1;
|
||||||
self.caret_epoch_started = Some(Instant::now());
|
self.caret_epoch_started = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(if focused {
|
cx.emit(if focused {
|
||||||
@@ -817,11 +839,8 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the caret should paint.
|
/// Whether the caret should paint.
|
||||||
pub fn caret_visible(&mut self, focused: bool) -> bool {
|
pub fn caret_visible(&self, focused: bool) -> bool {
|
||||||
let elapsed = self
|
let elapsed = self.caret_epoch_started.elapsed();
|
||||||
.caret_epoch_started
|
|
||||||
.map(|started| started.elapsed())
|
|
||||||
.unwrap_or_default();
|
|
||||||
self.caret_blink_visible_at(focused, elapsed)
|
self.caret_blink_visible_at(focused, elapsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,15 +920,21 @@ impl InputState {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.cursor = to.clamp(0, self.text.len());
|
let cursor = to.clamp(0, self.text.len());
|
||||||
if !extend {
|
let anchor = if extend { self.anchor } else { cursor };
|
||||||
self.anchor = self.cursor;
|
if cursor == self.cursor && anchor == self.anchor {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.anchor = anchor;
|
||||||
self.notify(window, cx);
|
self.notify(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects `range`.
|
/// Selects `range`.
|
||||||
fn select_range(&mut self, range: Range<usize>, window: &mut Window, cx: &mut Context<Self>) {
|
fn select_range(&mut self, range: Range<usize>, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if range.start == self.anchor && range.end == self.cursor {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.anchor = range.start;
|
self.anchor = range.start;
|
||||||
self.cursor = range.end;
|
self.cursor = range.end;
|
||||||
self.notify(window, cx);
|
self.notify(window, cx);
|
||||||
@@ -1085,7 +1110,7 @@ impl InputState {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO);
|
let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO);
|
||||||
*self.layout.borrow_mut() = Some(InputLayout {
|
self.layout = Some(InputLayout {
|
||||||
lines: text.lines,
|
lines: text.lines,
|
||||||
rows,
|
rows,
|
||||||
bounds,
|
bounds,
|
||||||
@@ -1107,8 +1132,7 @@ impl InputState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1138,8 +1162,7 @@ impl InputState {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1155,17 +1178,16 @@ impl InputState {
|
|||||||
let mut line_top = Pixels::ZERO;
|
let mut line_top = Pixels::ZERO;
|
||||||
for entry in &layout.lines {
|
for entry in &layout.lines {
|
||||||
let line_origin = point(origin.x, origin.y + line_top);
|
let line_origin = point(origin.x, origin.y + line_top);
|
||||||
entry
|
if let Err(err) = entry.line.paint(
|
||||||
.line
|
|
||||||
.paint(
|
|
||||||
line_origin,
|
line_origin,
|
||||||
layout.line_height,
|
layout.line_height,
|
||||||
TextAlign::Left,
|
TextAlign::Left,
|
||||||
None,
|
None,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
) {
|
||||||
.ok();
|
eprintln!("painting input text failed: {err}");
|
||||||
|
}
|
||||||
line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32;
|
line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1179,8 +1201,7 @@ impl InputState {
|
|||||||
color: gpui::Hsla,
|
color: gpui::Hsla,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
) {
|
) {
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1192,7 +1213,7 @@ impl InputState {
|
|||||||
let caret_top = origin.y + layout.rows[row_ix].y + (layout.line_height - font_size) / 2.0;
|
let caret_top = origin.y + layout.rows[row_ix].y + (layout.line_height - font_size) / 2.0;
|
||||||
let caret = Bounds::from_corners(
|
let caret = Bounds::from_corners(
|
||||||
point(origin.x + caret_x, caret_top),
|
point(origin.x + caret_x, caret_top),
|
||||||
point(origin.x + caret_x + px(1.5), caret_top + font_size),
|
point(origin.x + caret_x + CARET_WIDTH, caret_top + font_size),
|
||||||
);
|
);
|
||||||
|
|
||||||
window.paint_quad(fill(caret, color));
|
window.paint_quad(fill(caret, color));
|
||||||
@@ -1242,8 +1263,7 @@ impl InputState {
|
|||||||
|
|
||||||
/// The text index at a window point, using the last painted layout.
|
/// The text index at a window point, using the last painted layout.
|
||||||
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
|
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
real_index(&layout.mask_map, layout.index_at(position))
|
real_index(&layout.mask_map, layout.index_at(position))
|
||||||
@@ -1256,7 +1276,11 @@ impl InputState {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.cursor = self.index_at(position);
|
let cursor = self.index_at(position);
|
||||||
|
if cursor == self.cursor {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cursor = cursor;
|
||||||
self.notify(window, cx);
|
self.notify(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1268,14 +1292,13 @@ impl InputState {
|
|||||||
color: gpui::Hsla,
|
color: gpui::Hsla,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
) {
|
) {
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let start = display_index(&layout.mask_map, range.start);
|
let start = display_index(&layout.mask_map, range.start);
|
||||||
let end = display_index(&layout.mask_map, range.end);
|
let end = display_index(&layout.mask_map, range.end);
|
||||||
let thickness = px(1.0);
|
let thickness = UNDERLINE_WIDTH;
|
||||||
|
|
||||||
for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) {
|
for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) {
|
||||||
let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness;
|
let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness;
|
||||||
@@ -1329,7 +1352,7 @@ impl InputState {
|
|||||||
|
|
||||||
if event.click_count >= 3 {
|
if event.click_count >= 3 {
|
||||||
// Triple-click selects the clicked row, which is all of it when single-line.
|
// Triple-click selects the clicked row, which is all of it when single-line.
|
||||||
let row_range = self.layout.borrow().as_ref().and_then(|layout| {
|
let row_range = self.layout.as_ref().and_then(|layout| {
|
||||||
let display = display_index(&layout.mask_map, index);
|
let display = display_index(&layout.mask_map, index);
|
||||||
layout.row_range_at(display).map(|range| {
|
layout.row_range_at(display).map(|range| {
|
||||||
real_index(&layout.mask_map, range.start)
|
real_index(&layout.mask_map, range.start)
|
||||||
@@ -1462,15 +1485,16 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let target = {
|
let target = {
|
||||||
let layout = self.layout.borrow();
|
let Some(layout) = self.layout.as_ref() else {
|
||||||
let Some(layout) = layout.as_ref() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let display = display_index(&layout.mask_map, self.cursor);
|
let display = display_index(&layout.mask_map, self.cursor);
|
||||||
|
|
||||||
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
|
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let target = row_ix as isize + delta;
|
let target = row_ix as isize + delta;
|
||||||
|
|
||||||
let (display, goal_x) = if target < 0 {
|
let (display, goal_x) = if target < 0 {
|
||||||
(0, None)
|
(0, None)
|
||||||
} else if target as usize >= layout.rows.len() {
|
} else if target as usize >= layout.rows.len() {
|
||||||
@@ -1479,6 +1503,7 @@ impl InputState {
|
|||||||
let goal_x = self.goal_x.unwrap_or(caret_x);
|
let goal_x = self.goal_x.unwrap_or(caret_x);
|
||||||
(layout.index_at_row_x(target as usize, goal_x), Some(goal_x))
|
(layout.index_at_row_x(target as usize, goal_x), Some(goal_x))
|
||||||
};
|
};
|
||||||
|
|
||||||
Some((real_index(&layout.mask_map, display), goal_x))
|
Some((real_index(&layout.mask_map, display), goal_x))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1748,8 +1773,7 @@ impl EntityInputHandler for InputState {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<Bounds<Pixels>> {
|
) -> Option<Bounds<Pixels>> {
|
||||||
let layout = self.layout.borrow();
|
let layout = self.layout.as_ref()?;
|
||||||
let layout = layout.as_ref()?;
|
|
||||||
|
|
||||||
let start = display_index(
|
let start = display_index(
|
||||||
&layout.mask_map,
|
&layout.mask_map,
|
||||||
@@ -1797,9 +1821,10 @@ impl EntityInputHandler for InputState {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let layout = self.layout.borrow();
|
let layout = self.layout.as_ref()?;
|
||||||
let layout = layout.as_ref()?;
|
let index =
|
||||||
Some(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()))
|
real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len());
|
||||||
|
Some(utf8_range_to_utf16(&self.text, index..index).start)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_length_utf16(
|
fn text_length_utf16(
|
||||||
@@ -2049,7 +2074,11 @@ impl RenderOnce for Input {
|
|||||||
canvas
|
canvas
|
||||||
.with_animation(
|
.with_animation(
|
||||||
animation_id,
|
animation_id,
|
||||||
Animation::new(CARET_BLINK_INTERVAL * 2).repeat(),
|
// The blink needs four repaints per period,
|
||||||
|
// not the display's refresh rate.
|
||||||
|
Animation::new(CARET_BLINK_INTERVAL * 2)
|
||||||
|
.repeat()
|
||||||
|
.with_max_fps(4.0),
|
||||||
|canvas, _| canvas,
|
|canvas, _| canvas,
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -2172,23 +2201,13 @@ impl RenderOnce for Input {
|
|||||||
.when(!self.disabled, |input| {
|
.when(!self.disabled, |input| {
|
||||||
input
|
input
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.on_mouse_down(MouseButton::Left, {
|
||||||
let mouse_down_handler = self
|
let mouse_down_handler = self.state.read(cx).mouse_down_handler.clone();
|
||||||
.state
|
|
||||||
.read(cx)
|
|
||||||
.mouse_down_handler
|
|
||||||
.clone()
|
|
||||||
.expect("input state always carries its mouse-down handler");
|
|
||||||
move |event: &MouseDownEvent, window, cx| {
|
move |event: &MouseDownEvent, window, cx| {
|
||||||
mouse_down_handler(event, window, cx);
|
mouse_down_handler(event, window, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_key_down({
|
.on_key_down({
|
||||||
let key_down_handler = self
|
let key_down_handler = self.state.read(cx).key_down_handler.clone();
|
||||||
.state
|
|
||||||
.read(cx)
|
|
||||||
.key_down_handler
|
|
||||||
.clone()
|
|
||||||
.expect("input state always carries its key handler");
|
|
||||||
move |event: &KeyDownEvent, window, cx| {
|
move |event: &KeyDownEvent, window, cx| {
|
||||||
key_down_handler(event, window, cx);
|
key_down_handler(event, window, cx);
|
||||||
}
|
}
|
||||||
@@ -2516,7 +2535,9 @@ fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
|
|||||||
|
|
||||||
/// Converts a UTF-8 byte range to a UTF-16 range.
|
/// Converts a UTF-8 byte range to a UTF-16 range.
|
||||||
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
|
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
|
||||||
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
|
let start = text[..range.start].encode_utf16().count();
|
||||||
|
let end = start + text[range.start..range.end].encode_utf16().count();
|
||||||
|
start..end
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a UTF-16 range to a UTF-8 byte range.
|
/// Converts a UTF-16 range to a UTF-8 byte range.
|
||||||
|
|||||||
+25
-21
@@ -9,8 +9,8 @@ use gpui::{
|
|||||||
StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div,
|
StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div,
|
||||||
ease_out_quint, point, px, relative,
|
ease_out_quint, point, px, relative,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon as icon_glyph};
|
use gpui_native_icons::{Icon, IconRole, icon as icon_glyph};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::{ActiveTheme as _, POPUP_MAX_HEIGHT};
|
||||||
|
|
||||||
use crate::common::v_flex;
|
use crate::common::v_flex;
|
||||||
use crate::scroll_area::{ScrollArea, ScrollAxes};
|
use crate::scroll_area::{ScrollArea, ScrollAxes};
|
||||||
@@ -30,12 +30,12 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<MenuItem>>;
|
|||||||
pub struct MenuItem {
|
pub struct MenuItem {
|
||||||
kind: MenuItemKind,
|
kind: MenuItemKind,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
shortcut: Option<SharedString>,
|
shortcut: Option<SharedString>,
|
||||||
handler: Option<ItemHandler>,
|
handler: Option<ItemHandler>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
destructive: bool,
|
destructive: bool,
|
||||||
checked: Option<bool>,
|
checked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -58,7 +58,7 @@ impl MenuItem {
|
|||||||
handler: None,
|
handler: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
checked: None,
|
checked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ impl MenuItem {
|
|||||||
handler: None,
|
handler: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
destructive: false,
|
destructive: false,
|
||||||
checked: None,
|
checked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +86,11 @@ impl MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon before the label.
|
/// Shows an icon before the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +117,7 @@ impl MenuItem {
|
|||||||
|
|
||||||
/// Shows a check slot before the label.
|
/// Shows a check slot before the label.
|
||||||
pub fn checked(mut self, checked: bool) -> Self {
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
self.checked = Some(checked);
|
self.checked = checked;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +339,11 @@ impl RenderOnce for Menu {
|
|||||||
|
|
||||||
let items = if self.open {
|
let items = if self.open {
|
||||||
match self.items {
|
match self.items {
|
||||||
Some(build) => build(window, cx),
|
Some(build) => Rc::from(build(window, cx)),
|
||||||
None => Vec::new(),
|
None => Rc::from([]),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Rc::from([])
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(panel) = anchored_menu_panel(
|
if let Some(panel) = anchored_menu_panel(
|
||||||
@@ -372,7 +375,7 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
id: &ElementId,
|
id: &ElementId,
|
||||||
open: bool,
|
open: bool,
|
||||||
items: Vec<MenuItem>,
|
items: Rc<[MenuItem]>,
|
||||||
initial_selection: Option<usize>,
|
initial_selection: Option<usize>,
|
||||||
preselect: bool,
|
preselect: bool,
|
||||||
on_change: Option<ChangeHandler>,
|
on_change: Option<ChangeHandler>,
|
||||||
@@ -382,7 +385,7 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
chrome: &MenuChrome,
|
chrome: &MenuChrome,
|
||||||
enter_duration: std::time::Duration,
|
enter_duration: std::time::Duration,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
let was_open = window.use_keyed_state("menu-was-open", cx, |_, _| false);
|
let was_open = window.use_keyed_state((id.clone(), "menu-was-open"), cx, |_, _| false);
|
||||||
|
|
||||||
if !open {
|
if !open {
|
||||||
was_open.update(cx, |was_open, _| *was_open = false);
|
was_open.update(cx, |was_open, _| *was_open = false);
|
||||||
@@ -392,8 +395,9 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
let on_change = on_change.clone();
|
let on_change = on_change.clone();
|
||||||
// Keyboard focus rides the panel across frames
|
// Keyboard focus rides the panel across frames
|
||||||
let focus = gpui_native_utils::focus_handle(&(id.clone(), "panel").into(), window, cx);
|
let focus = gpui_native_utils::focus_handle(&(id.clone(), "panel").into(), window, cx);
|
||||||
let scroll = window.use_keyed_state("menu-scroll", cx, |_, _| ScrollHandle::new());
|
let scroll =
|
||||||
let selection = window.use_keyed_state("menu-selection", cx, |_, _| None);
|
window.use_keyed_state((id.clone(), "menu-scroll"), cx, |_, _| ScrollHandle::new());
|
||||||
|
let selection = window.use_keyed_state((id.clone(), "menu-selection"), cx, |_, _| None);
|
||||||
|
|
||||||
let selected = if *was_open.read(cx) {
|
let selected = if *was_open.read(cx) {
|
||||||
*selection.read(cx)
|
*selection.read(cx)
|
||||||
@@ -469,7 +473,7 @@ pub(crate) fn anchored_menu_panel(
|
|||||||
ScrollArea::new("menu-items")
|
ScrollArea::new("menu-items")
|
||||||
.handle(scroll.read(cx).clone())
|
.handle(scroll.read(cx).clone())
|
||||||
.axes(ScrollAxes::Vertical)
|
.axes(ScrollAxes::Vertical)
|
||||||
.max_h(px(320.))
|
.max_h(POPUP_MAX_HEIGHT)
|
||||||
.child(v_flex().gap_0p5().children(rows)),
|
.child(v_flex().gap_0p5().children(rows)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -525,7 +529,7 @@ fn render_item(
|
|||||||
let destructive = item.destructive;
|
let destructive = item.destructive;
|
||||||
let checked = item.checked;
|
let checked = item.checked;
|
||||||
let shortcut = item.shortcut.clone();
|
let shortcut = item.shortcut.clone();
|
||||||
let icon_role = item.icon;
|
let icon = item.icon.clone();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(("menu-item", index))
|
.id(("menu-item", index))
|
||||||
@@ -575,8 +579,8 @@ fn render_item(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when_some(icon_role, |row, role| {
|
.when_some(icon, |row, icon| {
|
||||||
row.child(icon_glyph(role).size(chrome.icon_size).color(label_color))
|
row.child(icon.size(chrome.icon_size).color(label_color))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -595,7 +599,7 @@ fn render_item(
|
|||||||
.child(shortcut),
|
.child(shortcut),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(checked == Some(true), |row| {
|
.when(checked, |row| {
|
||||||
row.child(
|
row.child(
|
||||||
icon_glyph(IconRole::Check)
|
icon_glyph(IconRole::Check)
|
||||||
.size(chrome.dense_icon_size)
|
.size(chrome.dense_icon_size)
|
||||||
|
|||||||
+17
-298
@@ -1,17 +1,13 @@
|
|||||||
use std::sync::Arc;
|
//! A scrollable container with an overlay scrollbar.
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, DispatchPhase, ElementId, Entity, EntityId, InteractiveElement as _, IntoElement,
|
App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, ScrollHandle,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollEvent,
|
SharedString, StatefulInteractiveElement as _, Styled, Window, div,
|
||||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, Task, Window, canvas, div,
|
|
||||||
fill, point,
|
|
||||||
};
|
};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
|
||||||
|
|
||||||
use crate::common::StyledExt;
|
use crate::common::StyledExt;
|
||||||
|
use crate::scrollbar::{Scrollbar, ScrollbarVisibility};
|
||||||
|
|
||||||
/// Which directions the content scrolls in.
|
/// Which directions the content scrolls in.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
@@ -25,108 +21,6 @@ pub enum ScrollAxes {
|
|||||||
Both,
|
Both,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When an overlay scrollbar shows.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub enum ScrollbarVisibility {
|
|
||||||
/// The running platform's behavior, from the theme.
|
|
||||||
#[default]
|
|
||||||
Platform,
|
|
||||||
Always,
|
|
||||||
Autohide,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How long an autohide thumb stays fully visible before its fade begins.
|
|
||||||
const AUTOHIDE_DELAY: Duration = Duration::from_millis(700);
|
|
||||||
/// How long the autohide fade runs.
|
|
||||||
const AUTOHIDE_FADE: Duration = Duration::from_millis(250);
|
|
||||||
/// How often the fade repaints while it runs.
|
|
||||||
const AUTOHIDE_FADE_STEP: Duration = Duration::from_millis(50);
|
|
||||||
|
|
||||||
/// The shared autohide timeline for an overlay scrollbar.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct Autohide {
|
|
||||||
view: EntityId,
|
|
||||||
state: Entity<AutohideState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AutohideState {
|
|
||||||
last_activity: Option<Instant>,
|
|
||||||
hide_task: Option<Task<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Autohide {
|
|
||||||
pub(crate) fn new(key: ElementId, window: &mut Window, cx: &mut App) -> Self {
|
|
||||||
let view = window.current_view();
|
|
||||||
let state = window.use_keyed_state(key, cx, |_, _| AutohideState {
|
|
||||||
last_activity: None,
|
|
||||||
hide_task: None,
|
|
||||||
});
|
|
||||||
Self { view, state }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks interaction.
|
|
||||||
pub(crate) fn touch(&self, window: &mut Window, cx: &mut App) {
|
|
||||||
let view = self.view;
|
|
||||||
let reduce_motion = cx.theme().accessibility.reduce_motion;
|
|
||||||
|
|
||||||
let task = window.spawn(cx, async move |cx| {
|
|
||||||
if reduce_motion {
|
|
||||||
cx.background_executor()
|
|
||||||
.timer(AUTOHIDE_DELAY + AUTOHIDE_FADE)
|
|
||||||
.await;
|
|
||||||
cx.update(|_, cx| cx.notify(view)).ok();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let steps = (AUTOHIDE_FADE.as_millis() / AUTOHIDE_FADE_STEP.as_millis()).max(1);
|
|
||||||
cx.background_executor().timer(AUTOHIDE_DELAY).await;
|
|
||||||
|
|
||||||
for _ in 0..steps {
|
|
||||||
cx.update(|_, cx| cx.notify(view)).ok();
|
|
||||||
cx.background_executor().timer(AUTOHIDE_FADE_STEP).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.update(|_, cx| cx.notify(view)).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.state.update(cx, |state, _| {
|
|
||||||
state.last_activity = Some(Instant::now());
|
|
||||||
state.hide_task = Some(task);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The thumb's opacity.
|
|
||||||
pub(crate) fn alpha(&self, cx: &App, dragging: bool) -> f32 {
|
|
||||||
if dragging {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(activity) = self.state.read(cx).last_activity else {
|
|
||||||
return 0.0;
|
|
||||||
};
|
|
||||||
|
|
||||||
let idle = activity.elapsed();
|
|
||||||
if idle <= AUTOHIDE_DELAY {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fade = idle.saturating_sub(AUTOHIDE_DELAY);
|
|
||||||
if fade >= AUTOHIDE_FADE {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
1.0 - fade.as_secs_f32() / AUTOHIDE_FADE.as_secs_f32()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An overlay scrollbar thumb's style for one paint: `None` hides it.
|
|
||||||
pub(crate) fn thumb_style(color: gpui::Hsla, alpha: f32) -> Option<gpui::Hsla> {
|
|
||||||
(alpha > 0.0).then_some(gpui::Hsla {
|
|
||||||
a: color.a * alpha,
|
|
||||||
..color
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A scrollable container with an overlay scrollbar.
|
/// A scrollable container with an overlay scrollbar.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ScrollArea {
|
pub struct ScrollArea {
|
||||||
@@ -196,32 +90,8 @@ impl ScrollArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Names keyed state under the scrollbar's own id.
|
|
||||||
pub fn scrollbar_state_key(name: &'static str, id: &ElementId) -> ElementId {
|
|
||||||
ElementId::NamedChild(Arc::new(id.clone()), SharedString::from(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pure thumb geometry for an overlay scrollbar.
|
|
||||||
pub(crate) fn thumb_geometry(
|
|
||||||
viewport: Pixels,
|
|
||||||
content: Pixels,
|
|
||||||
offset: Pixels,
|
|
||||||
track: Pixels,
|
|
||||||
min_thumb: Pixels,
|
|
||||||
) -> (Pixels, Pixels) {
|
|
||||||
if content <= viewport || content <= Pixels::ZERO {
|
|
||||||
return (track, Pixels::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
let length = (viewport / content * track).max(min_thumb).min(track);
|
|
||||||
let overflow = content - viewport;
|
|
||||||
let progress = (-offset / overflow).clamp(0., 1.);
|
|
||||||
let travel = track - length;
|
|
||||||
(length, travel * progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for ScrollArea {
|
impl RenderOnce for ScrollArea {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let mut pane_style = self.style.clone();
|
let mut pane_style = self.style.clone();
|
||||||
pane_style.padding = Default::default();
|
pane_style.padding = Default::default();
|
||||||
|
|
||||||
@@ -252,175 +122,24 @@ impl RenderOnce for ScrollArea {
|
|||||||
|
|
||||||
if let Some(handle) = self.handle {
|
if let Some(handle) = self.handle {
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
let autohide = match self.visibility {
|
|
||||||
ScrollbarVisibility::Platform => cx.theme().scrollbar.autohide,
|
|
||||||
ScrollbarVisibility::Always => false,
|
|
||||||
ScrollbarVisibility::Autohide => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if self.axes != ScrollAxes::Horizontal {
|
||||||
wrapper.extend(std::iter::once(
|
wrapper.extend(std::iter::once(
|
||||||
canvas(|bounds, _, _| bounds, {
|
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
|
||||||
let handle = handle.clone();
|
.visibility(self.visibility)
|
||||||
move |overlay, _, window, cx| {
|
|
||||||
paint_scrollbar(&handle, &id, autohide, overlay, window, cx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.absolute()
|
|
||||||
.inset_0()
|
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.axes != ScrollAxes::Vertical {
|
||||||
|
wrapper.extend(std::iter::once(
|
||||||
|
Scrollbar::horizontal((id, "h-scrollbar"), handle)
|
||||||
|
.visibility(self.visibility)
|
||||||
|
.into_any_element(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wrapper
|
wrapper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paints the overlay scrollbar thumb.
|
|
||||||
fn paint_scrollbar(
|
|
||||||
handle: &ScrollHandle,
|
|
||||||
key: &ElementId,
|
|
||||||
autohide: bool,
|
|
||||||
overlay: Bounds<Pixels>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let theme = cx.theme();
|
|
||||||
let view = window.current_view();
|
|
||||||
|
|
||||||
let color = theme.colors.scrollbar_thumb;
|
|
||||||
let width = theme.scrollbar.width;
|
|
||||||
let inset = theme.scrollbar.inset;
|
|
||||||
let min_thumb = theme.scrollbar.min_thumb;
|
|
||||||
|
|
||||||
let viewport = handle.bounds();
|
|
||||||
let max_offset = handle.max_offset().y;
|
|
||||||
|
|
||||||
if max_offset <= Pixels::ZERO || viewport.size.height <= Pixels::ZERO {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let autohide_state =
|
|
||||||
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx));
|
|
||||||
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| {
|
|
||||||
Option::<(Pixels, Pixels)>::None
|
|
||||||
});
|
|
||||||
let dragging = drag.read(cx).is_some();
|
|
||||||
|
|
||||||
let alpha = match (&autohide_state, autohide) {
|
|
||||||
(Some(state), true) => state.alpha(cx, dragging),
|
|
||||||
_ => 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let track = overlay.size.height - inset * 2.0;
|
|
||||||
let (thumb_length, thumb_top) = thumb_geometry(
|
|
||||||
viewport.size.height,
|
|
||||||
viewport.size.height + max_offset,
|
|
||||||
handle.offset().y,
|
|
||||||
track,
|
|
||||||
min_thumb,
|
|
||||||
);
|
|
||||||
|
|
||||||
let thumb_x = overlay.right() - width - inset;
|
|
||||||
let thumb_bounds = Bounds::from_corners(
|
|
||||||
point(thumb_x, overlay.top() + inset + thumb_top),
|
|
||||||
point(
|
|
||||||
thumb_x + width,
|
|
||||||
overlay.top() + inset + thumb_top + thumb_length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(color) = thumb_style(color, alpha) {
|
|
||||||
window.paint_quad(fill(thumb_bounds, color).corner_radii(width / 2.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(state) = &autohide_state {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let state = state.clone();
|
|
||||||
move |event: &ScrollEvent, phase, window, cx| {
|
|
||||||
if phase != DispatchPhase::Bubble || !overlay.contains(&event.position) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.touch(window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let handle = handle.clone();
|
|
||||||
let drag = drag.clone();
|
|
||||||
let autohide_state = autohide_state.clone();
|
|
||||||
move |event: &MouseDownEvent, phase, window, cx| {
|
|
||||||
if phase != DispatchPhase::Bubble
|
|
||||||
|| event.button != gpui::MouseButton::Left
|
|
||||||
|| !thumb_bounds.contains(&event.position)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
drag.update(cx, |drag, _| {
|
|
||||||
*drag = Some((event.position.y, handle.offset().y));
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(state) = &autohide_state {
|
|
||||||
state.touch(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.stop_propagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let drag = drag.clone();
|
|
||||||
let autohide_state = autohide_state.clone();
|
|
||||||
move |event: &MouseUpEvent, phase, window, cx| {
|
|
||||||
if phase != DispatchPhase::Bubble || event.button != gpui::MouseButton::Left {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if drag.read(cx).is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
drag.update(cx, |drag, _| {
|
|
||||||
*drag = None;
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(state) = &autohide_state {
|
|
||||||
state.touch(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.on_mouse_event({
|
|
||||||
let handle = handle.clone();
|
|
||||||
let drag = drag.clone();
|
|
||||||
let autohide_state = autohide_state.clone();
|
|
||||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
|
||||||
if phase != DispatchPhase::Bubble {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(state) = &autohide_state
|
|
||||||
&& drag.read(cx).is_some()
|
|
||||||
{
|
|
||||||
state.touch(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.pressed_button != Some(gpui::MouseButton::Left) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((start_y, start_offset)) = *drag.read(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if max_offset > Pixels::ZERO && track > thumb_length {
|
|
||||||
let scale = max_offset / (track - thumb_length);
|
|
||||||
let offset = (start_offset - (event.position.y - start_y) * scale)
|
|
||||||
.clamp(-max_offset, Pixels::ZERO);
|
|
||||||
handle.set_offset(point(handle.offset().x, offset));
|
|
||||||
cx.notify(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
+143
-28
@@ -1,18 +1,141 @@
|
|||||||
//! Overlay scrollbars for scroll surfaces.
|
//! Overlay scrollbars for scroll surfaces.
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, DispatchPhase, ElementId, IntoElement, ListState, MouseButton, MouseDownEvent,
|
App, Bounds, DispatchPhase, ElementId, Entity, EntityId, IntoElement, ListState, MouseButton,
|
||||||
MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent, ScrollHandle,
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent,
|
||||||
Styled as _, UniformListScrollHandle, Window, canvas, fill, point,
|
ScrollHandle, SharedString, Styled as _, Task, UniformListScrollHandle, Window, canvas, fill,
|
||||||
|
point,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
use crate::scroll_area::{Autohide, scrollbar_state_key, thumb_geometry, thumb_style};
|
/// When an overlay scrollbar shows.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum ScrollbarVisibility {
|
||||||
|
/// The running platform's behavior, from the theme.
|
||||||
|
#[default]
|
||||||
|
Platform,
|
||||||
|
Always,
|
||||||
|
Autohide,
|
||||||
|
}
|
||||||
|
|
||||||
/// Re-exported so callers only need the scrollbar module.
|
/// How long an autohide thumb stays fully visible before its fade begins.
|
||||||
pub use crate::scroll_area::ScrollbarVisibility;
|
const AUTOHIDE_DELAY: Duration = Duration::from_millis(700);
|
||||||
|
/// How long the autohide fade runs.
|
||||||
|
const AUTOHIDE_FADE: Duration = Duration::from_millis(250);
|
||||||
|
/// How often the fade repaints while it runs.
|
||||||
|
const AUTOHIDE_FADE_STEP: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
|
/// The shared autohide timeline for an overlay scrollbar.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Autohide {
|
||||||
|
view: EntityId,
|
||||||
|
state: Entity<AutohideState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutohideState {
|
||||||
|
last_activity: Option<Instant>,
|
||||||
|
hide_task: Option<Task<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Autohide {
|
||||||
|
fn new(key: ElementId, window: &mut Window, cx: &mut App) -> Self {
|
||||||
|
let view = window.current_view();
|
||||||
|
let state = window.use_keyed_state(key, cx, |_, _| AutohideState {
|
||||||
|
last_activity: None,
|
||||||
|
hide_task: None,
|
||||||
|
});
|
||||||
|
Self { view, state }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks interaction.
|
||||||
|
fn touch(&self, window: &mut Window, cx: &mut App) {
|
||||||
|
let view = self.view;
|
||||||
|
let reduce_motion = cx.theme().accessibility.reduce_motion;
|
||||||
|
|
||||||
|
let task = window.spawn(cx, async move |cx| {
|
||||||
|
if reduce_motion {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(AUTOHIDE_DELAY + AUTOHIDE_FADE)
|
||||||
|
.await;
|
||||||
|
cx.update(|_, cx| cx.notify(view)).ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let steps = (AUTOHIDE_FADE.as_millis() / AUTOHIDE_FADE_STEP.as_millis()).max(1);
|
||||||
|
cx.background_executor().timer(AUTOHIDE_DELAY).await;
|
||||||
|
|
||||||
|
for _ in 0..steps {
|
||||||
|
cx.update(|_, cx| cx.notify(view)).ok();
|
||||||
|
cx.background_executor().timer(AUTOHIDE_FADE_STEP).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.update(|_, cx| cx.notify(view)).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state.update(cx, |state, _| {
|
||||||
|
state.last_activity = Some(Instant::now());
|
||||||
|
state.hide_task = Some(task);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The thumb's opacity.
|
||||||
|
fn alpha(&self, cx: &App, dragging: bool) -> f32 {
|
||||||
|
if dragging {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(activity) = self.state.read(cx).last_activity else {
|
||||||
|
return 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
let idle = activity.elapsed();
|
||||||
|
if idle <= AUTOHIDE_DELAY {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fade = idle.saturating_sub(AUTOHIDE_DELAY);
|
||||||
|
if fade >= AUTOHIDE_FADE {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
1.0 - fade.as_secs_f32() / AUTOHIDE_FADE.as_secs_f32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrollbar_state_key(name: &'static str, id: &ElementId) -> ElementId {
|
||||||
|
ElementId::NamedChild(Arc::new(id.clone()), SharedString::from(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure thumb geometry for an overlay scrollbar.
|
||||||
|
fn thumb_geometry(
|
||||||
|
viewport: Pixels,
|
||||||
|
content: Pixels,
|
||||||
|
offset: Pixels,
|
||||||
|
track: Pixels,
|
||||||
|
min_thumb: Pixels,
|
||||||
|
) -> (Pixels, Pixels) {
|
||||||
|
if content <= viewport || content <= Pixels::ZERO {
|
||||||
|
return (track, Pixels::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = (viewport / content * track).max(min_thumb).min(track);
|
||||||
|
let overflow = content - viewport;
|
||||||
|
let progress = (-offset / overflow).clamp(0., 1.);
|
||||||
|
let travel = track - length;
|
||||||
|
(length, travel * progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An overlay scrollbar thumb's style for one paint: `None` hides it.
|
||||||
|
fn thumb_style(color: gpui::Hsla, alpha: f32) -> Option<gpui::Hsla> {
|
||||||
|
(alpha > 0.0).then_some(gpui::Hsla {
|
||||||
|
a: color.a * alpha,
|
||||||
|
..color
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// The scroll state a scrollbar drives, implemented for gpui's scroll handles.
|
/// The scroll state a scrollbar drives, implemented for gpui's scroll handles.
|
||||||
pub trait ScrollbarHandle: 'static {
|
pub trait ScrollbarHandle: 'static {
|
||||||
@@ -128,33 +251,35 @@ pub struct Scrollbar {
|
|||||||
axis: Axis,
|
axis: Axis,
|
||||||
handle: Rc<dyn ScrollbarHandle>,
|
handle: Rc<dyn ScrollbarHandle>,
|
||||||
visibility: ScrollbarVisibility,
|
visibility: ScrollbarVisibility,
|
||||||
key: ElementId,
|
id: ElementId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scrollbar {
|
impl Scrollbar {
|
||||||
/// A vertical scrollbar driven by `handle`.
|
/// A vertical scrollbar named by `id`, driven by `handle`.
|
||||||
pub fn vertical<H>(handle: H) -> Self
|
pub fn vertical<I, H>(id: I, handle: H) -> Self
|
||||||
where
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
H: ScrollbarHandle,
|
H: ScrollbarHandle,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
axis: Axis::Vertical,
|
axis: Axis::Vertical,
|
||||||
handle: Rc::new(handle),
|
handle: Rc::new(handle),
|
||||||
visibility: ScrollbarVisibility::default(),
|
visibility: ScrollbarVisibility::default(),
|
||||||
key: ElementId::Name("scrollbar".into()),
|
id: id.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A horizontal scrollbar driven by `handle`.
|
/// A horizontal scrollbar named by `id`, driven by `handle`.
|
||||||
pub fn horizontal<H>(handle: H) -> Self
|
pub fn horizontal<I, H>(id: I, handle: H) -> Self
|
||||||
where
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
H: ScrollbarHandle,
|
H: ScrollbarHandle,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
axis: Axis::Horizontal,
|
axis: Axis::Horizontal,
|
||||||
handle: Rc::new(handle),
|
handle: Rc::new(handle),
|
||||||
visibility: ScrollbarVisibility::default(),
|
visibility: ScrollbarVisibility::default(),
|
||||||
key: ElementId::Name("scrollbar".into()),
|
id: id.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +288,6 @@ impl Scrollbar {
|
|||||||
self.visibility = visibility;
|
self.visibility = visibility;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Names this scrollbar's state.
|
|
||||||
pub fn key(mut self, key: impl Into<ElementId>) -> Self {
|
|
||||||
self.key = key.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Scrollbar {
|
impl RenderOnce for Scrollbar {
|
||||||
@@ -177,8 +296,8 @@ impl RenderOnce for Scrollbar {
|
|||||||
let handle = self.handle;
|
let handle = self.handle;
|
||||||
let axis = self.axis;
|
let axis = self.axis;
|
||||||
let visibility = self.visibility;
|
let visibility = self.visibility;
|
||||||
let key = self.key;
|
let id = self.id;
|
||||||
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &key, window, cx)
|
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &id, window, cx)
|
||||||
})
|
})
|
||||||
.absolute()
|
.absolute()
|
||||||
.inset_0()
|
.inset_0()
|
||||||
@@ -192,7 +311,7 @@ fn paint_scrollbar(
|
|||||||
handle: &Rc<dyn ScrollbarHandle>,
|
handle: &Rc<dyn ScrollbarHandle>,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
visibility: ScrollbarVisibility,
|
visibility: ScrollbarVisibility,
|
||||||
key: &ElementId,
|
id: &ElementId,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
@@ -217,12 +336,10 @@ fn paint_scrollbar(
|
|||||||
ScrollbarVisibility::Always => false,
|
ScrollbarVisibility::Always => false,
|
||||||
ScrollbarVisibility::Autohide => true,
|
ScrollbarVisibility::Autohide => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let autohide_state =
|
let autohide_state =
|
||||||
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx));
|
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", id), window, cx));
|
||||||
|
|
||||||
let track_length = viewport_along - inset * 2.0;
|
let track_length = viewport_along - inset * 2.0;
|
||||||
|
|
||||||
let (thumb_length, thumb_travel) = thumb_geometry(
|
let (thumb_length, thumb_travel) = thumb_geometry(
|
||||||
viewport_along,
|
viewport_along,
|
||||||
viewport_along + max_offset,
|
viewport_along + max_offset,
|
||||||
@@ -267,9 +384,7 @@ fn paint_scrollbar(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| {
|
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", id), cx, |_, _| Drag::None);
|
||||||
Drag::None
|
|
||||||
});
|
|
||||||
let dragging = drag.read(cx).is_some();
|
let dragging = drag.read(cx).is_some();
|
||||||
|
|
||||||
let alpha = match &autohide_state {
|
let alpha = match &autohide_state {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use gpui::{
|
|||||||
SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window,
|
SpringConfig, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Toggled, Window,
|
||||||
canvas, div,
|
canvas, div,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
use gpui_native_utils::{adjacent_selectable, focus_handle, focus_ring};
|
use gpui_native_utils::{focus_handle, focus_ring};
|
||||||
|
|
||||||
use crate::common::{Sizable, StyledExt, h_flex};
|
use crate::common::{Sizable, StyledExt, h_flex};
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ type ChangeHandler<T> = Rc<dyn Fn(T, &mut Window, &mut App)>;
|
|||||||
pub struct SegmentedOption<T> {
|
pub struct SegmentedOption<T> {
|
||||||
value: T,
|
value: T,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SegmentedOption<T> {
|
impl<T> SegmentedOption<T> {
|
||||||
@@ -38,8 +38,11 @@ impl<T> SegmentedOption<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a leading icon for the segment.
|
/// Sets a leading icon for the segment.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +67,8 @@ where
|
|||||||
T: Clone + PartialEq + 'static,
|
T: Clone + PartialEq + 'static,
|
||||||
{
|
{
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
options: Vec<SegmentedOption<T>>,
|
/// Shared with the per-segment and key handlers, so captures are bumps.
|
||||||
|
options: Rc<[SegmentedOption<T>]>,
|
||||||
selected: T,
|
selected: T,
|
||||||
a11y_label: Option<SharedString>,
|
a11y_label: Option<SharedString>,
|
||||||
size: Size,
|
size: Size,
|
||||||
@@ -82,7 +86,7 @@ where
|
|||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
options: Vec::new(),
|
options: Rc::from([]),
|
||||||
selected,
|
selected,
|
||||||
a11y_label: None,
|
a11y_label: None,
|
||||||
size: Size::Small,
|
size: Size::Small,
|
||||||
@@ -163,12 +167,12 @@ where
|
|||||||
.options
|
.options
|
||||||
.iter()
|
.iter()
|
||||||
.position(|segment| segment.value == self.selected);
|
.position(|segment| segment.value == self.selected);
|
||||||
|
let option_count = self.options.len();
|
||||||
|
|
||||||
let selectable: Vec<usize> = (0..self.options.len()).collect();
|
|
||||||
let on_change = self.on_change.clone();
|
let on_change = self.on_change.clone();
|
||||||
let key_on_change = on_change.clone();
|
let key_on_change = on_change.clone();
|
||||||
let key_options = self.options.clone();
|
let key_options = self.options.clone();
|
||||||
let key_selectable = selectable.clone();
|
let options = self.options.clone();
|
||||||
|
|
||||||
let label = self
|
let label = self
|
||||||
.a11y_label
|
.a11y_label
|
||||||
@@ -176,14 +180,13 @@ where
|
|||||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||||
|
|
||||||
window.with_element_namespace(self.id.clone(), |window| {
|
window.with_element_namespace(self.id.clone(), |window| {
|
||||||
let focus = focus_handle(&self.id, window, cx);
|
|
||||||
|
|
||||||
let geometry: Entity<Vec<Option<Bounds<gpui::Pixels>>>> =
|
let geometry: Entity<Vec<Option<Bounds<gpui::Pixels>>>> =
|
||||||
window.use_keyed_state("segmented-geometry", cx, |_, _| Vec::new());
|
window.use_keyed_state("segmented-geometry", cx, |_, _| Vec::new());
|
||||||
|
|
||||||
let track_origin: Entity<Option<Point<gpui::Pixels>>> =
|
let track_origin: Entity<Option<Point<gpui::Pixels>>> =
|
||||||
window.use_keyed_state("segmented-track-origin", cx, |_, _| None);
|
window.use_keyed_state("segmented-track-origin", cx, |_, _| None);
|
||||||
|
|
||||||
|
let focus = focus_handle(&self.id, window, cx);
|
||||||
let anim_config = SpringConfig::new(500., 35., 1.);
|
let anim_config = SpringConfig::new(500., 35., 1.);
|
||||||
let track_point = *track_origin.read(cx);
|
let track_point = *track_origin.read(cx);
|
||||||
|
|
||||||
@@ -197,12 +200,13 @@ where
|
|||||||
|
|
||||||
let segments = self
|
let segments = self
|
||||||
.options
|
.options
|
||||||
.into_iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(move |(index, segment)| {
|
.map(move |(index, segment)| {
|
||||||
let is_selected = Some(index) == selected_index;
|
let is_selected = Some(index) == selected_index;
|
||||||
let on_change = on_change.clone();
|
let on_change = on_change.clone();
|
||||||
let value = segment.value;
|
let options = options.clone();
|
||||||
|
|
||||||
let content_color = if is_selected {
|
let content_color = if is_selected {
|
||||||
foreground
|
foreground
|
||||||
} else {
|
} else {
|
||||||
@@ -225,10 +229,10 @@ where
|
|||||||
.role(Role::RadioButton)
|
.role(Role::RadioButton)
|
||||||
.aria_label(segment.label.clone())
|
.aria_label(segment.label.clone())
|
||||||
.aria_toggled(Toggled::from(is_selected))
|
.aria_toggled(Toggled::from(is_selected))
|
||||||
.when_some(segment.icon, |segment, role| {
|
.when_some(segment.icon.clone(), |segment, icon| {
|
||||||
segment.child(icon(role).size(icon_size).color(content_color))
|
segment.child(icon.size(icon_size).color(content_color))
|
||||||
})
|
})
|
||||||
.child(segment.label)
|
.child(segment.label.clone())
|
||||||
.child(
|
.child(
|
||||||
canvas(
|
canvas(
|
||||||
{
|
{
|
||||||
@@ -251,8 +255,9 @@ where
|
|||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if Some(index) != selected_index
|
if Some(index) != selected_index
|
||||||
&& let Some(on_change) = &on_change
|
&& let Some(on_change) = &on_change
|
||||||
|
&& let Some(option) = options.get(index)
|
||||||
{
|
{
|
||||||
on_change(value.clone(), window, cx);
|
on_change(option.value.clone(), window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -319,22 +324,26 @@ where
|
|||||||
.on_key_down({
|
.on_key_down({
|
||||||
move |event: &KeyDownEvent, window, cx| {
|
move |event: &KeyDownEvent, window, cx| {
|
||||||
let target = match event.keystroke.key.as_str() {
|
let target = match event.keystroke.key.as_str() {
|
||||||
"right" => {
|
"right" => match selected_index {
|
||||||
adjacent_selectable(&key_selectable, selected_index, true)
|
Some(index) => {
|
||||||
|
index.checked_add(1).filter(|next| *next < option_count)
|
||||||
}
|
}
|
||||||
"left" => {
|
None => (option_count > 0).then_some(0),
|
||||||
adjacent_selectable(&key_selectable, selected_index, false)
|
},
|
||||||
}
|
"left" => selected_index.and_then(|index| index.checked_sub(1)),
|
||||||
"home" => key_selectable.first().copied(),
|
"home" => (option_count > 0).then_some(0),
|
||||||
"end" => key_selectable.last().copied(),
|
"end" => option_count.checked_sub(1),
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(next) = target
|
if let Some(next) = target
|
||||||
&& Some(next) != selected_index
|
&& Some(next) != selected_index
|
||||||
&& let Some(on_change) = &key_on_change
|
&& let Some(on_change) = &key_on_change
|
||||||
|
&& let Some(option) = key_options.get(next)
|
||||||
{
|
{
|
||||||
on_change(key_options[next].value.clone(), window, cx);
|
on_change(option.value.clone(), window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+18
-21
@@ -5,7 +5,7 @@ use gpui::{
|
|||||||
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
||||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
|
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole, icon};
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||||
use gpui_native_utils::focus_ring;
|
use gpui_native_utils::focus_ring;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ pub type SelectHandler<T> = Rc<dyn Fn(Option<T>, &mut Window, &mut App)>;
|
|||||||
pub struct SelectOption<T> {
|
pub struct SelectOption<T> {
|
||||||
value: T,
|
value: T,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SelectOption<T> {
|
impl<T> SelectOption<T> {
|
||||||
@@ -37,8 +37,11 @@ impl<T> SelectOption<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a leading icon for the option's row.
|
/// Sets a leading icon for the option's row.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,24 +209,18 @@ where
|
|||||||
.position(|option| &option.value == value)
|
.position(|option| &option.value == value)
|
||||||
});
|
});
|
||||||
|
|
||||||
let trigger_options = self.options.clone();
|
// The trigger's text is fixed at render time; only it moves into the closure.
|
||||||
let trigger_selected = self.selected.clone();
|
let trigger_text = selected_index
|
||||||
let trigger_placeholder = self.placeholder.clone();
|
.and_then(|index| self.options.get(index))
|
||||||
|
.map(|option| option.label.clone())
|
||||||
|
.unwrap_or_else(|| self.placeholder.clone());
|
||||||
|
|
||||||
|
let trigger_has_value = self.selected.is_some();
|
||||||
let trigger_id = self.id.clone();
|
let trigger_id = self.id.clone();
|
||||||
let trigger_label = label.clone();
|
let trigger_label = label.clone();
|
||||||
let trigger_style = self.style;
|
let trigger_style = self.style;
|
||||||
|
|
||||||
let trigger = move |_window: &mut Window, _cx: &mut App| {
|
let trigger = move |_window: &mut Window, _cx: &mut App| {
|
||||||
let text = trigger_selected
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|value| {
|
|
||||||
trigger_options
|
|
||||||
.iter()
|
|
||||||
.find(|option| &option.value == value)
|
|
||||||
.map(|option| option.label.clone())
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| trigger_placeholder.clone());
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id((trigger_id.clone(), "select-trigger"))
|
.id((trigger_id.clone(), "select-trigger"))
|
||||||
.debug_selector(|| "select-trigger".to_string())
|
.debug_selector(|| "select-trigger".to_string())
|
||||||
@@ -240,7 +237,7 @@ where
|
|||||||
.border_color(rest_border)
|
.border_color(rest_border)
|
||||||
.bg(input)
|
.bg(input)
|
||||||
.text_size(metrics.text_size)
|
.text_size(metrics.text_size)
|
||||||
.text_color(if trigger_selected.is_some() {
|
.text_color(if trigger_has_value {
|
||||||
foreground
|
foreground
|
||||||
} else {
|
} else {
|
||||||
muted_foreground
|
muted_foreground
|
||||||
@@ -249,7 +246,7 @@ where
|
|||||||
trigger.shadow(focus_ring(ring))
|
trigger.shadow(focus_ring(ring))
|
||||||
})
|
})
|
||||||
.hover(|style| style.bg(muted).border_color(hover_border))
|
.hover(|style| style.bg(muted).border_color(hover_border))
|
||||||
.child(text)
|
.child(trigger_text.clone())
|
||||||
.child(
|
.child(
|
||||||
icon(IconRole::ChevronDown)
|
icon(IconRole::ChevronDown)
|
||||||
.size(icon_size)
|
.size(icon_size)
|
||||||
@@ -278,8 +275,8 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut row = MenuItem::new(option.label.clone());
|
let mut row = MenuItem::new(option.label.clone());
|
||||||
if let Some(role) = option.icon {
|
if let Some(icon) = option.icon.clone() {
|
||||||
row = row.icon(role);
|
row = row.icon(icon);
|
||||||
}
|
}
|
||||||
row.checked(is_selected).handler(handler)
|
row.checked(is_selected).handler(handler)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div, px};
|
use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH};
|
||||||
|
|
||||||
/// Which axis the rule runs along.
|
/// Which axis the rule runs along.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
@@ -69,8 +69,8 @@ impl RenderOnce for Separator {
|
|||||||
rule.debug_selector(move || selector())
|
rule.debug_selector(move || selector())
|
||||||
})
|
})
|
||||||
.map(|rule| match self.orientation {
|
.map(|rule| match self.orientation {
|
||||||
SeparatorOrientation::Horizontal => rule.w_full().h(px(1.)),
|
SeparatorOrientation::Horizontal => rule.w_full().h(BORDER_WIDTH),
|
||||||
SeparatorOrientation::Vertical => rule.self_stretch().w(px(1.)),
|
SeparatorOrientation::Vertical => rule.self_stretch().w(BORDER_WIDTH),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,12 +79,6 @@ impl Slider {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the current value; the slider never owns it.
|
|
||||||
pub fn value(mut self, value: f32) -> Self {
|
|
||||||
self.value = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders the slider inert, unfocusable, and dimmed; disabled always wins.
|
/// Renders the slider inert, unfocusable, and dimmed; disabled always wins.
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
@@ -135,16 +129,12 @@ impl RenderOnce for Slider {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// The track bounds, recorded by the overlay canvas each frame,
|
|
||||||
// turn pointer positions into values inside the listeners.
|
|
||||||
let bounds_cell = Rc::new(Cell::new(None::<Bounds<Pixels>>));
|
let bounds_cell = Rc::new(Cell::new(None::<Bounds<Pixels>>));
|
||||||
// Dragging crosses frames, so it lives in keyed element state,
|
|
||||||
// a per-render flag would reset when the owner's update repaints.
|
|
||||||
let dragging = window.use_keyed_state("slider-dragging", cx, |_, _| false);
|
|
||||||
// A held handle gives the arrow-key listener something to check focus against,
|
|
||||||
// and the click-to-focus behavior a target.
|
|
||||||
let focus_handle = gpui_native_utils::focus_handle(&self.id, window, cx);
|
let focus_handle = gpui_native_utils::focus_handle(&self.id, window, cx);
|
||||||
|
|
||||||
|
let dragging =
|
||||||
|
window.use_keyed_state((self.id.clone(), "slider-dragging"), cx, |_, _| false);
|
||||||
|
|
||||||
let set_value: ValueSetter = {
|
let set_value: ValueSetter = {
|
||||||
let bounds_cell = bounds_cell.clone();
|
let bounds_cell = bounds_cell.clone();
|
||||||
let on_change = self.on_change.clone();
|
let on_change = self.on_change.clone();
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ impl RenderOnce for Switch {
|
|||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.relative()
|
.relative()
|
||||||
.debug_selector({
|
.debug_selector(move || format!("{id}-track"))
|
||||||
let name = format!("{id}-track");
|
|
||||||
move || name
|
|
||||||
})
|
|
||||||
.w(track_width)
|
.w(track_width)
|
||||||
.h(track_height)
|
.h(track_height)
|
||||||
.rounded(theme.radius.full)
|
.rounded(theme.radius.full)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
|
||||||
Styled as _, Task, Window, deferred, div, ease_out_quint, px,
|
Styled as _, Task, Window, deferred, div, ease_out_quint, px,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole, icon};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariant};
|
use crate::button::{Button, ButtonVariant};
|
||||||
@@ -43,7 +43,7 @@ impl ToastAction {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Toast {
|
pub struct Toast {
|
||||||
message: SharedString,
|
message: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
action: Option<ToastAction>,
|
action: Option<ToastAction>,
|
||||||
duration: Option<Duration>,
|
duration: Option<Duration>,
|
||||||
on_dismiss: Option<DismissHandler>,
|
on_dismiss: Option<DismissHandler>,
|
||||||
@@ -68,8 +68,11 @@ impl Toast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows this glyph before the content.
|
/// Shows this glyph before the content.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,14 +288,14 @@ fn pill(entry: &ToastEntry, toasts: &Entity<Toasts>, cx: &App) -> gpui::Stateful
|
|||||||
.border_color(border)
|
.border_color(border)
|
||||||
.rounded(radius)
|
.rounded(radius)
|
||||||
.shadow(shadows.clone())
|
.shadow(shadows.clone())
|
||||||
.when_some(toast.icon, |pill, icon_role| {
|
.when_some(toast.icon.clone(), |pill, icon| {
|
||||||
pill.child(
|
pill.child(
|
||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.mr_2()
|
.mr_2()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(icon(icon_role).size(icon_size).color(foreground)),
|
.child(icon.size(icon_size).color(foreground)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
+12
-10
@@ -1,9 +1,9 @@
|
|||||||
use gpui::AppContext as _;
|
use gpui::AppContext as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyView, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement,
|
AnyView, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement,
|
||||||
ParentElement as _, Pixels, Render, SharedString, Styled as _, Window, canvas, div, point, px,
|
ParentElement as _, Pixels, Render, SharedString, Styled as _, Window, canvas, div, point,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH};
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, TOOLTIP_MAX_WIDTH, TOOLTIP_OFFSET};
|
||||||
use gpui_native_utils::{anchor_bounds, record_anchor_bounds};
|
use gpui_native_utils::{anchor_bounds, record_anchor_bounds};
|
||||||
|
|
||||||
const PANEL_BOUNDS_ID: &str = "tooltip-panel-bounds";
|
const PANEL_BOUNDS_ID: &str = "tooltip-panel-bounds";
|
||||||
@@ -34,8 +34,8 @@ impl Tooltip {
|
|||||||
{
|
{
|
||||||
let text = text.into();
|
let text = text.into();
|
||||||
let anchor_id = anchor_id.into();
|
let anchor_id = anchor_id.into();
|
||||||
move |_window, cx| {
|
move |window, cx| {
|
||||||
let anchor = anchor_bounds(&anchor_id, cx);
|
let anchor = anchor_bounds(window.window_handle().window_id(), &anchor_id, cx);
|
||||||
cx.new(|_| Self {
|
cx.new(|_| Self {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
anchor,
|
anchor,
|
||||||
@@ -76,7 +76,7 @@ impl Render for Tooltip {
|
|||||||
.debug_selector(|| "tooltip-panel".to_string())
|
.debug_selector(|| "tooltip-panel".to_string())
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1()
|
.py_1()
|
||||||
.max_w(px(360.))
|
.max_w(TOOLTIP_MAX_WIDTH)
|
||||||
.bg(surface)
|
.bg(surface)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(border)
|
.border_color(border)
|
||||||
@@ -89,14 +89,16 @@ impl Render for Tooltip {
|
|||||||
|
|
||||||
let (wrapper, panel) = match self.anchor {
|
let (wrapper, panel) = match self.anchor {
|
||||||
Some(parent) => {
|
Some(parent) => {
|
||||||
let origin = window.mouse_position() + point(px(1.), px(1.));
|
let origin = window.mouse_position() + point(TOOLTIP_OFFSET, TOOLTIP_OFFSET);
|
||||||
let viewport_height = window.viewport_size().height;
|
let viewport_height = window.viewport_size().height;
|
||||||
|
|
||||||
let height = anchor_bounds(&ElementId::from(PANEL_BOUNDS_ID), cx)
|
let height = anchor_bounds(
|
||||||
|
window.window_handle().window_id(),
|
||||||
|
&ElementId::from(PANEL_BOUNDS_ID),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.map(|bounds| bounds.size.height)
|
.map(|bounds| bounds.size.height)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| text_size * 1.5 + window.rem_size() * 0.5 + BORDER_WIDTH * 2.0);
|
||||||
text_size * 1.5 + window.rem_size() * 0.5 + BORDER_WIDTH * 2.0
|
|
||||||
});
|
|
||||||
|
|
||||||
let below = viewport_height - spacing_unit - parent.bottom();
|
let below = viewport_height - spacing_unit - parent.bottom();
|
||||||
let above = parent.top() - spacing_unit;
|
let above = parent.top() - spacing_unit;
|
||||||
|
|||||||
@@ -322,3 +322,136 @@ fn disabled_trigger_ignores_clicks() {
|
|||||||
assert!(changes.borrow().is_empty());
|
assert!(changes.borrow().is_empty());
|
||||||
assert!(visual.debug_bounds("menu-panel").is_none());
|
assert!(visual.debug_bounds("menu-panel").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PairHarness {
|
||||||
|
open_a: bool,
|
||||||
|
open_b: bool,
|
||||||
|
changes_a: Changes,
|
||||||
|
changes_b: Changes,
|
||||||
|
activated_a: Activated,
|
||||||
|
activated_b: Activated,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PairHarness {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||||
|
let (activated_a, activated_b) = (self.activated_a.clone(), self.activated_b.clone());
|
||||||
|
|
||||||
|
let menu_a = DropdownMenu::new("pair-a")
|
||||||
|
.label("Alpha")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.open(self.open_a)
|
||||||
|
.on_open_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes = changes_a.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.open_a = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.items(move |_, _| {
|
||||||
|
vec![
|
||||||
|
MenuItem::new("png").handler({
|
||||||
|
let activated = activated_a.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("png")
|
||||||
|
}),
|
||||||
|
MenuItem::new("pdf").handler({
|
||||||
|
let activated = activated_a.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("pdf")
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu_b = DropdownMenu::new("pair-b")
|
||||||
|
.label("Beta")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.open(self.open_b)
|
||||||
|
.on_open_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes = changes_b.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.open_b = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.items(move |_, _| {
|
||||||
|
vec![
|
||||||
|
MenuItem::new("zip").handler({
|
||||||
|
let activated = activated_b.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("zip")
|
||||||
|
}),
|
||||||
|
MenuItem::new("7z").handler({
|
||||||
|
let activated = activated_b.clone();
|
||||||
|
move |_, _| activated.borrow_mut().push("7z")
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
div().size_full().flex().gap_2().child(menu_a).child(menu_b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pair_window() -> (
|
||||||
|
VisualTestContext,
|
||||||
|
AnyWindowHandle,
|
||||||
|
Changes,
|
||||||
|
Changes,
|
||||||
|
Activated,
|
||||||
|
Activated,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let activated_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let activated_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||||
|
let (activated_a_for_window, activated_b_for_window) =
|
||||||
|
(activated_a.clone(), activated_b.clone());
|
||||||
|
let window = cx.add_window(move |_, _| PairHarness {
|
||||||
|
open_a: false,
|
||||||
|
open_b: false,
|
||||||
|
changes_a: changes_a_for_window,
|
||||||
|
changes_b: changes_b_for_window,
|
||||||
|
activated_a: activated_a_for_window,
|
||||||
|
activated_b: activated_b_for_window,
|
||||||
|
});
|
||||||
|
let window = window.into();
|
||||||
|
cx.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
(
|
||||||
|
VisualTestContext::from_window(window, &cx),
|
||||||
|
window,
|
||||||
|
changes_a,
|
||||||
|
changes_b,
|
||||||
|
activated_a,
|
||||||
|
activated_b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With a closed sibling dropdown on screen, a pointer-opened menu must still
|
||||||
|
/// arm rows with the keyboard: the menu's open/selection state is per
|
||||||
|
/// instance, not shared keyed state.
|
||||||
|
#[test]
|
||||||
|
fn sibling_dropdowns_keep_independent_menu_state() {
|
||||||
|
let (mut visual, window, changes_a, changes_b, activated_a, activated_b) = pair_window();
|
||||||
|
|
||||||
|
// Menu A sits at the window's top-left; menu B stays closed beside it.
|
||||||
|
click(&mut visual, point(px(30.), px(14.)));
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![true]);
|
||||||
|
draw(&mut visual, window);
|
||||||
|
|
||||||
|
visual.simulate_keystrokes("down enter");
|
||||||
|
assert_eq!(*activated_a.borrow(), vec!["png"]);
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![true, false]);
|
||||||
|
assert!(changes_b.borrow().is_empty(), "B must stay closed");
|
||||||
|
assert!(activated_b.borrow().is_empty());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::{cell::Cell, rc::Rc, time::Duration};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
|
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
|
||||||
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
Styled as _, TestAppContext, VisualTestContext, Window, div,
|
||||||
@@ -9,10 +11,12 @@ use gpui_native_core::input::{Input, InputState};
|
|||||||
|
|
||||||
struct FieldHost {
|
struct FieldHost {
|
||||||
url: Entity<InputState>,
|
url: Entity<InputState>,
|
||||||
|
renders: Rc<Cell<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for FieldHost {
|
impl Render for FieldHost {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.renders.set(self.renders.get() + 1);
|
||||||
let url_error = self
|
let url_error = self
|
||||||
.url
|
.url
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@@ -34,15 +38,27 @@ impl Render for FieldHost {
|
|||||||
|
|
||||||
/// A field page in a real window; the url input is focused unless
|
/// A field page in a real window; the url input is focused unless
|
||||||
/// `focus` is false, which leaves the page truly idle.
|
/// `focus` is false, which leaves the page truly idle.
|
||||||
fn field_host(focus: bool) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
fn field_host(
|
||||||
|
focus: bool,
|
||||||
|
) -> (
|
||||||
|
TestAppContext,
|
||||||
|
Entity<InputState>,
|
||||||
|
VisualTestContext,
|
||||||
|
Rc<Cell<usize>>,
|
||||||
|
) {
|
||||||
let cx = TestAppContext::single();
|
let cx = TestAppContext::single();
|
||||||
cx.update(init);
|
cx.update(init);
|
||||||
|
let renders = Rc::new(Cell::new(0));
|
||||||
|
let renders_for_host = renders.clone();
|
||||||
let (url, window) = cx.update(|cx| {
|
let (url, window) = cx.update(|cx| {
|
||||||
let url = InputState::new(cx);
|
let url = InputState::new(cx);
|
||||||
let url_for_host = url.clone();
|
let url_for_host = url.clone();
|
||||||
let window = cx
|
let window = cx
|
||||||
.open_window(gpui::WindowOptions::default(), |_, cx| {
|
.open_window(gpui::WindowOptions::default(), |_, cx| {
|
||||||
cx.new(|_| FieldHost { url: url_for_host })
|
cx.new(|_| FieldHost {
|
||||||
|
url: url_for_host,
|
||||||
|
renders: renders_for_host.clone(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
(url, window)
|
(url, window)
|
||||||
@@ -55,12 +71,12 @@ fn field_host(focus: bool) -> (TestAppContext, Entity<InputState>, VisualTestCon
|
|||||||
}
|
}
|
||||||
window.draw(cx).clear(cx);
|
window.draw(cx).clear(cx);
|
||||||
});
|
});
|
||||||
(cx, url, visual)
|
(cx, url, visual, renders)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_idle_field_page_schedules_no_frames() {
|
fn an_idle_field_page_schedules_no_frames() {
|
||||||
let (_cx, _url, mut visual) = field_host(false);
|
let (_cx, _url, mut visual, _renders) = field_host(false);
|
||||||
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
|
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
frames, 0,
|
frames, 0,
|
||||||
@@ -68,12 +84,25 @@ fn an_idle_field_page_schedules_no_frames() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The caret blink is fps-capped: it must never demand a next frame, and its
|
||||||
|
/// redraws arrive on the executor timer at the blink cadence.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_focused_field_page_schedules_frames_for_its_blink() {
|
fn a_focused_field_blink_is_fps_capped_and_still_redraws() {
|
||||||
let (_cx, _url, mut visual) = field_host(true);
|
let (cx, _url, mut visual, renders) = field_host(true);
|
||||||
|
let renders_after_focus = renders.get();
|
||||||
|
|
||||||
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
|
let frames = visual.update(|window, cx| window.simulate_next_frame(cx));
|
||||||
|
assert_eq!(
|
||||||
|
frames, 0,
|
||||||
|
"the blink must be fps-capped, not per-frame scheduled"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The blink redraws through the executor timer (4 fps cap).
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(251));
|
||||||
|
cx.run_until_parked();
|
||||||
|
visual.update(|window, cx| window.draw(cx).clear(cx));
|
||||||
assert!(
|
assert!(
|
||||||
frames > 0,
|
renders.get() > renders_after_focus,
|
||||||
"the focused caret's blink needs frames to redraw at the blink cadence"
|
"the capped blink must still redraw its page"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ impl Render for UniformShell {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.handle.clone())),
|
.child(Scrollbar::vertical("rows", self.handle.clone())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ impl Render for ListShell {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.state.clone())),
|
.child(Scrollbar::vertical("rows", self.state.clone())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,3 +271,99 @@ fn dragging_the_thumb_drives_a_list_state_and_reports_the_drag() {
|
|||||||
"the drag ends with the mouse"
|
"the drag ends with the mouse"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TwoShell {
|
||||||
|
left: UniformListScrollHandle,
|
||||||
|
right: UniformListScrollHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TwoShell {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.h(px(VIEWPORT_HEIGHT))
|
||||||
|
.w(px(300.))
|
||||||
|
.child(
|
||||||
|
uniform_list("left-rows", ROWS, |range, _, _| {
|
||||||
|
range
|
||||||
|
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.left)
|
||||||
|
.h_full()
|
||||||
|
.w_full(),
|
||||||
|
)
|
||||||
|
.child(Scrollbar::vertical("left", self.left.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.h(px(VIEWPORT_HEIGHT))
|
||||||
|
.w(px(300.))
|
||||||
|
.child(
|
||||||
|
uniform_list("right-rows", ROWS, |range, _, _| {
|
||||||
|
range
|
||||||
|
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.track_scroll(&self.right)
|
||||||
|
.h_full()
|
||||||
|
.w_full(),
|
||||||
|
)
|
||||||
|
.child(Scrollbar::vertical("right", self.right.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two_shell() -> (
|
||||||
|
TestAppContext,
|
||||||
|
VisualTestContext,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
UniformListScrollHandle,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(init);
|
||||||
|
let (left, right) = (
|
||||||
|
UniformListScrollHandle::new(),
|
||||||
|
UniformListScrollHandle::new(),
|
||||||
|
);
|
||||||
|
let (left_for_view, right_for_view) = (left.clone(), right.clone());
|
||||||
|
let window = cx.add_window(move |_, _| TwoShell {
|
||||||
|
left: left_for_view,
|
||||||
|
right: right_for_view,
|
||||||
|
});
|
||||||
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
(cx, visual, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dragging one scrollbar's thumb must never move a sibling scrollbar: the
|
||||||
|
/// drag state is keyed per scrollbar id, not shared across instances.
|
||||||
|
#[test]
|
||||||
|
fn sibling_scrollbars_drag_independently() {
|
||||||
|
let (cx, mut visual, left, right) = two_shell();
|
||||||
|
let metrics = Metrics::measure(&cx, &left);
|
||||||
|
assert!(metrics.max_offset > px(0.));
|
||||||
|
|
||||||
|
press(&mut visual, metrics.thumb_center());
|
||||||
|
drag_to(&mut visual, metrics.full_drag_target());
|
||||||
|
release(&mut visual, metrics.full_drag_target());
|
||||||
|
|
||||||
|
let offset = left.offset().y;
|
||||||
|
assert!(
|
||||||
|
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
||||||
|
"the left drag reaches the end, got {offset}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
right.offset().y,
|
||||||
|
px(0.),
|
||||||
|
"the left drag leaked into the right scrollbar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,3 +153,132 @@ fn arrow_keys_step_the_value_while_focused() {
|
|||||||
|
|
||||||
assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]);
|
assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TwoSliders {
|
||||||
|
changes_a: Rc<RefCell<Vec<f32>>>,
|
||||||
|
changes_b: Rc<RefCell<Vec<f32>>>,
|
||||||
|
value_a: f32,
|
||||||
|
value_b: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TwoSliders {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
div().absolute().top_0().left_0().w(px(200.)).child(
|
||||||
|
Slider::new("slider-a", self.value_a)
|
||||||
|
.min(0.0)
|
||||||
|
.max(100.0)
|
||||||
|
.step(10.0)
|
||||||
|
.on_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
let changes_a = changes_a.clone();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes_a.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.value_a = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().absolute().top(px(40.)).left_0().w(px(200.)).child(
|
||||||
|
Slider::new("slider-b", self.value_b)
|
||||||
|
.min(0.0)
|
||||||
|
.max(100.0)
|
||||||
|
.step(10.0)
|
||||||
|
.on_change({
|
||||||
|
let view = cx.entity();
|
||||||
|
move |next, _, cx| {
|
||||||
|
changes_b.borrow_mut().push(next);
|
||||||
|
view.update(cx, |harness, cx| {
|
||||||
|
harness.value_b = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn two_slider_window() -> (
|
||||||
|
VisualTestContext,
|
||||||
|
Rc<RefCell<Vec<f32>>>,
|
||||||
|
Rc<RefCell<Vec<f32>>>,
|
||||||
|
) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||||
|
let window = cx.add_window(move |_, _| TwoSliders {
|
||||||
|
changes_a: changes_a_for_window,
|
||||||
|
changes_b: changes_b_for_window,
|
||||||
|
value_a: 0.0,
|
||||||
|
value_b: 0.0,
|
||||||
|
});
|
||||||
|
let window = window.into();
|
||||||
|
cx.update_window(window, |_, window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
(
|
||||||
|
VisualTestContext::from_window(window, &cx),
|
||||||
|
changes_a,
|
||||||
|
changes_b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dragging one slider must never drive a sibling slider: the drag flag is
|
||||||
|
/// per instance, not shared keyed state.
|
||||||
|
#[test]
|
||||||
|
fn two_sliders_drag_independently() {
|
||||||
|
let (mut cx, changes_a, changes_b) = two_slider_window();
|
||||||
|
|
||||||
|
// Slider A occupies y 0..16, slider B starts at y 40.
|
||||||
|
cx.simulate_mouse_down(
|
||||||
|
point(px(100.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_move(
|
||||||
|
point(px(120.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_up(
|
||||||
|
point(px(120.), px(10.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(*changes_a.borrow(), vec![50.0, 60.0]);
|
||||||
|
assert!(changes_b.borrow().is_empty(), "A's drag leaked into B");
|
||||||
|
|
||||||
|
cx.simulate_mouse_down(
|
||||||
|
point(px(100.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_move(
|
||||||
|
point(px(40.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
cx.simulate_mouse_up(
|
||||||
|
point(px(40.), px(50.)),
|
||||||
|
MouseButton::Left,
|
||||||
|
Modifiers::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(*changes_b.borrow(), vec![50.0, 20.0]);
|
||||||
|
assert_eq!(
|
||||||
|
*changes_a.borrow(),
|
||||||
|
vec![50.0, 60.0],
|
||||||
|
"B's drag leaked into A"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+10
-11
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
||||||
Styled as _, Window, div,
|
Styled as _, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<ListItem>>;
|
|||||||
/// One row of a [`List`].
|
/// One row of a [`List`].
|
||||||
pub struct ListItem {
|
pub struct ListItem {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
detail: Option<SharedString>,
|
detail: Option<SharedString>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,12 @@ impl ListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon leading the label.
|
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +225,8 @@ fn render_row(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.when_some(item.icon, |row, role| {
|
.when_some(item.icon.clone(), |row, icon| {
|
||||||
row.child(
|
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||||
icon(role)
|
|
||||||
.size(theme.sizes.medium.icon_size)
|
|
||||||
.color(label_color),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
+35
-20
@@ -10,7 +10,7 @@ use gpui::{
|
|||||||
Styled as _, Window, div,
|
Styled as _, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scroll_area::ScrollArea;
|
use gpui_native_core::scroll_area::ScrollArea;
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::Icon;
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ pub enum SidebarEntry {
|
|||||||
/// A selectable row.
|
/// A selectable row.
|
||||||
Row {
|
Row {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
detail: Option<SharedString>,
|
detail: Option<SharedString>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
},
|
},
|
||||||
@@ -56,10 +56,13 @@ impl SidebarEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon leading the label.
|
/// Shows an icon leading the label, a role or any custom [`IconNamed`].
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
if let Self::Row { icon, .. } = &mut self {
|
where
|
||||||
*icon = Some(role);
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
if let Self::Row { icon: slot, .. } = &mut self {
|
||||||
|
*slot = Some(icon.into());
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -195,19 +198,33 @@ impl RenderOnce for Sidebar {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||||
|
|
||||||
let id_text = self.id.to_string();
|
|
||||||
let scroll_area_id = (self.id.clone(), "entries");
|
let scroll_area_id = (self.id.clone(), "entries");
|
||||||
|
|
||||||
// Entry paint reads the theme through this borrow.
|
let row_count = entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| matches!(entry, SidebarEntry::Row { .. }))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let mut row_position = 0usize;
|
||||||
let children: Vec<AnyElement> = entries
|
let children: Vec<AnyElement> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, entry)| {
|
.map(|(index, entry)| {
|
||||||
|
let is_row = matches!(entry, SidebarEntry::Row { .. });
|
||||||
|
if is_row {
|
||||||
|
row_position += 1;
|
||||||
|
}
|
||||||
|
let (position_in_set, size_of_set) = if is_row {
|
||||||
|
(row_position, row_count)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
render_entry(
|
render_entry(
|
||||||
cx,
|
cx,
|
||||||
entry,
|
entry,
|
||||||
index,
|
index,
|
||||||
entries.len(),
|
position_in_set,
|
||||||
|
size_of_set,
|
||||||
self.selected == Some(index),
|
self.selected == Some(index),
|
||||||
material,
|
material,
|
||||||
&self.on_change,
|
&self.on_change,
|
||||||
@@ -216,9 +233,10 @@ impl RenderOnce for Sidebar {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let id = self.id;
|
||||||
div()
|
div()
|
||||||
.id(self.id)
|
.id(id.clone())
|
||||||
.debug_selector(move || format!("sidebar-{id_text}"))
|
.debug_selector(move || format!("sidebar-{id}"))
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -258,7 +276,8 @@ fn render_entry(
|
|||||||
cx: &App,
|
cx: &App,
|
||||||
entry: &SidebarEntry,
|
entry: &SidebarEntry,
|
||||||
index: usize,
|
index: usize,
|
||||||
count: usize,
|
position_in_set: usize,
|
||||||
|
size_of_set: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
material: bool,
|
material: bool,
|
||||||
on_change: &Option<ChangeHandler>,
|
on_change: &Option<ChangeHandler>,
|
||||||
@@ -294,8 +313,8 @@ fn render_entry(
|
|||||||
.rounded(theme.radius.sm)
|
.rounded(theme.radius.sm)
|
||||||
.role(Role::ListItem)
|
.role(Role::ListItem)
|
||||||
.aria_selected(selected)
|
.aria_selected(selected)
|
||||||
.aria_position_in_set(index + 1)
|
.aria_position_in_set(position_in_set)
|
||||||
.aria_size_of_set(count)
|
.aria_size_of_set(size_of_set)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
if material && !*row_disabled {
|
if material && !*row_disabled {
|
||||||
if selected {
|
if selected {
|
||||||
@@ -321,12 +340,8 @@ fn render_entry(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
row = row.when_some(*row_icon, |row, role| {
|
row = row.when_some(row_icon.clone(), |row, icon| {
|
||||||
row.child(
|
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||||
icon(role)
|
|
||||||
.size(theme.sizes.medium.icon_size)
|
|
||||||
.color(label_color),
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
|
row = row.child(div().flex_1().text_color(label_color).child(label.clone()));
|
||||||
|
|||||||
+31
-23
@@ -10,7 +10,7 @@ use gpui_native_core::button::{Button, ButtonVariant};
|
|||||||
use gpui_native_core::common::Sizable as _;
|
use gpui_native_core::common::Sizable as _;
|
||||||
use gpui_native_core::spinner::Spinner;
|
use gpui_native_core::spinner::Spinner;
|
||||||
use gpui_native_core::tooltip::Tooltip;
|
use gpui_native_core::tooltip::Tooltip;
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
use gpui_native_utils::adjacent_selectable;
|
use gpui_native_utils::adjacent_selectable;
|
||||||
|
|
||||||
@@ -32,8 +32,10 @@ pub enum TabsVariant {
|
|||||||
|
|
||||||
/// One tab of a [`Tabs`] strip.
|
/// One tab of a [`Tabs`] strip.
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
|
/// The stable identity for element state; must survive reordering.
|
||||||
|
key: ElementId,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
lead: Option<AnyElement>,
|
lead: Option<AnyElement>,
|
||||||
busy: bool,
|
busy: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
@@ -42,12 +44,14 @@ pub struct Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
/// Creates a tab with this label.
|
/// Creates a tab shown as `label`, identified by `key`.
|
||||||
pub fn new<L>(label: L) -> Self
|
pub fn new<K, L>(key: K, label: L) -> Self
|
||||||
where
|
where
|
||||||
|
K: Into<ElementId>,
|
||||||
L: Into<SharedString>,
|
L: Into<SharedString>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
|
key: key.into(),
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
icon: None,
|
icon: None,
|
||||||
lead: None,
|
lead: None,
|
||||||
@@ -59,8 +63,11 @@ impl Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon leading the label.
|
/// Shows an icon leading the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +217,6 @@ impl RenderOnce for Tabs {
|
|||||||
cx,
|
cx,
|
||||||
tab,
|
tab,
|
||||||
index,
|
index,
|
||||||
self.selected == index,
|
|
||||||
self.selected,
|
self.selected,
|
||||||
self.variant,
|
self.variant,
|
||||||
&self.on_change,
|
&self.on_change,
|
||||||
@@ -268,7 +274,6 @@ fn render_tab(
|
|||||||
cx: &App,
|
cx: &App,
|
||||||
tab: Tab,
|
tab: Tab,
|
||||||
index: usize,
|
index: usize,
|
||||||
selected: bool,
|
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
variant: TabsVariant,
|
variant: TabsVariant,
|
||||||
on_change: &Option<ChangeHandler>,
|
on_change: &Option<ChangeHandler>,
|
||||||
@@ -276,6 +281,7 @@ fn render_tab(
|
|||||||
focus: &gpui::FocusHandle,
|
focus: &gpui::FocusHandle,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let mut tab = tab;
|
let mut tab = tab;
|
||||||
|
let selected = index == selected_index;
|
||||||
|
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let metrics = theme.sizes.resolve(Size::default());
|
let metrics = theme.sizes.resolve(Size::default());
|
||||||
@@ -301,10 +307,11 @@ fn render_tab(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut strip_tab = div()
|
let mut strip_tab = div()
|
||||||
.id(("tab", index))
|
.id((tab.key.clone(), "tab"))
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let id = format!("{tabs_id}-tab-{}", tab.label);
|
let tabs_id = tabs_id.clone();
|
||||||
move || id
|
let label = tab.label.clone();
|
||||||
|
move || format!("{tabs_id}-tab-{}", label)
|
||||||
})
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -340,18 +347,18 @@ fn render_tab(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let lead = tab.lead.take();
|
let lead = tab.lead.take();
|
||||||
|
let icon = tab.icon.take();
|
||||||
|
|
||||||
strip_tab = match lead {
|
strip_tab = match lead {
|
||||||
Some(lead) => strip_tab.child(lead),
|
Some(lead) => strip_tab.child(lead),
|
||||||
None => match variant {
|
None => match variant {
|
||||||
TabsVariant::Underline => strip_tab.child(div().flex_none().pb_0p5().when_some(
|
TabsVariant::Underline => {
|
||||||
tab.icon,
|
strip_tab.child(div().flex_none().pb_0p5().when_some(icon, |slot, icon| {
|
||||||
|slot, role| {
|
slot.child(icon.size(metrics.dense_icon_size).color(label_color))
|
||||||
slot.child(icon(role).size(metrics.dense_icon_size).color(label_color))
|
}))
|
||||||
},
|
}
|
||||||
)),
|
TabsVariant::Pill => strip_tab.when_some(icon, |strip_tab, icon| {
|
||||||
TabsVariant::Pill => strip_tab.when_some(tab.icon, |strip_tab, role| {
|
strip_tab.child(icon.size(metrics.dense_icon_size).color(label_color))
|
||||||
strip_tab.child(icon(role).size(metrics.dense_icon_size).color(label_color))
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -364,7 +371,7 @@ fn render_tab(
|
|||||||
|
|
||||||
if tab.busy && !tab.disabled {
|
if tab.busy && !tab.disabled {
|
||||||
strip_tab = strip_tab.child(
|
strip_tab = strip_tab.child(
|
||||||
Spinner::new(("tab-busy", index))
|
Spinner::new((tab.key.clone(), "tab-busy"))
|
||||||
.small()
|
.small()
|
||||||
.accessibility_label("Loading"),
|
.accessibility_label("Loading"),
|
||||||
);
|
);
|
||||||
@@ -401,14 +408,15 @@ fn render_tab_close(
|
|||||||
index: usize,
|
index: usize,
|
||||||
on_close: &Option<CloseHandler>,
|
on_close: &Option<CloseHandler>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
Button::new(("tab-close", index))
|
Button::new((tab.key.clone(), "tab-close"))
|
||||||
.icon(IconRole::Close)
|
.icon(IconRole::Close)
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.small()
|
.small()
|
||||||
.tab_stop(false)
|
.tab_stop(false)
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let id = format!("{tabs_id}-close-{}", tab.label);
|
let tabs_id = tabs_id.clone();
|
||||||
move || id.clone()
|
let label = tab.label.clone();
|
||||||
|
move || format!("{tabs_id}-close-{}", label)
|
||||||
})
|
})
|
||||||
.tooltip(Tooltip::text("Close"))
|
.tooltip(Tooltip::text("Close"))
|
||||||
.accessibility_label(SharedString::from(format!("Close {}", tab.label)))
|
.accessibility_label(SharedString::from(format!("Close {}", tab.label)))
|
||||||
|
|||||||
+51
-16
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
||||||
Styled as _, Window, div,
|
Styled as _, Window, div,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::{Icon, IconRole, icon};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color};
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ type ItemsFn = Box<dyn Fn(&mut Window, &mut App) -> Vec<TreeItem>>;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TreeItem {
|
pub struct TreeItem {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
expandable: bool,
|
expandable: bool,
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
@@ -44,8 +44,11 @@ impl TreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon leading the label.
|
/// Shows an icon leading the label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,14 +169,49 @@ impl RenderOnce for Tree {
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
.unwrap_or_else(|| SharedString::from(self.id.to_string()));
|
||||||
|
|
||||||
|
let sibling_sets: Vec<(usize, usize)> = {
|
||||||
|
let mut sets = vec![(0usize, 0usize); items.len()];
|
||||||
|
let mut start = 0usize;
|
||||||
|
|
||||||
|
while start < items.len() {
|
||||||
|
let depth = items[start].depth;
|
||||||
|
|
||||||
|
let mut end = start + 1;
|
||||||
|
while end < items.len() && items[end].depth > depth {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut position = 0usize;
|
||||||
|
for index in start..end {
|
||||||
|
if items[index].depth == depth {
|
||||||
|
position += 1;
|
||||||
|
sets[index] = (position, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for index in start..end {
|
||||||
|
if items[index].depth == depth {
|
||||||
|
sets[index].1 = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
sets
|
||||||
|
};
|
||||||
|
|
||||||
let rows: Vec<AnyElement> = items
|
let rows: Vec<AnyElement> = items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, _item)| {
|
.map(|(index, _item)| {
|
||||||
|
let (position_in_set, size_of_set) = sibling_sets[index];
|
||||||
render_row(
|
render_row(
|
||||||
cx,
|
cx,
|
||||||
&items,
|
&items[index],
|
||||||
index,
|
index,
|
||||||
|
position_in_set,
|
||||||
|
size_of_set,
|
||||||
self.selected == Some(index),
|
self.selected == Some(index),
|
||||||
&self.on_change,
|
&self.on_change,
|
||||||
&self.on_disclosure,
|
&self.on_disclosure,
|
||||||
@@ -228,21 +266,21 @@ impl RenderOnce for Tree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_row(
|
fn render_row(
|
||||||
cx: &App,
|
cx: &App,
|
||||||
items: &[TreeItem],
|
item: &TreeItem,
|
||||||
index: usize,
|
index: usize,
|
||||||
|
position_in_set: usize,
|
||||||
|
size_of_set: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
on_change: &Option<ChangeHandler>,
|
on_change: &Option<ChangeHandler>,
|
||||||
on_disclosure: &Option<DisclosureHandler>,
|
on_disclosure: &Option<DisclosureHandler>,
|
||||||
focus: &FocusHandle,
|
focus: &FocusHandle,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let item = &items[index];
|
|
||||||
let count = items.len();
|
|
||||||
let label_color = selectable_label_color(cx, selected, item.disabled);
|
let label_color = selectable_label_color(cx, selected, item.disabled);
|
||||||
let disabled = item.disabled;
|
let disabled = item.disabled;
|
||||||
|
|
||||||
// One indent step per depth level, on the theme's horizontal padding.
|
// One indent step per depth level, on the theme's horizontal padding.
|
||||||
let indent = theme.sizes.medium.padding_x * (item.depth as f32 + 1.0);
|
let indent = theme.sizes.medium.padding_x * (item.depth as f32 + 1.0);
|
||||||
|
|
||||||
@@ -258,7 +296,8 @@ fn render_row(
|
|||||||
.role(Role::TreeItem)
|
.role(Role::TreeItem)
|
||||||
.aria_selected(selected)
|
.aria_selected(selected)
|
||||||
.aria_level(item.depth + 1)
|
.aria_level(item.depth + 1)
|
||||||
.aria_size_of_set(count)
|
.aria_position_in_set(position_in_set)
|
||||||
|
.aria_size_of_set(size_of_set)
|
||||||
.when(item.expandable, |row| row.aria_expanded(item.expanded))
|
.when(item.expandable, |row| row.aria_expanded(item.expanded))
|
||||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||||
.when(!disabled, |row| {
|
.when(!disabled, |row| {
|
||||||
@@ -281,12 +320,8 @@ fn render_row(
|
|||||||
selected,
|
selected,
|
||||||
on_disclosure,
|
on_disclosure,
|
||||||
))
|
))
|
||||||
.when_some(item.icon, |row, role| {
|
.when_some(item.icon.clone(), |row, icon| {
|
||||||
row.child(
|
row.child(icon.size(theme.sizes.medium.icon_size).color(label_color))
|
||||||
icon(role)
|
|
||||||
.size(theme.sizes.medium.icon_size)
|
|
||||||
.color(label_color),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ impl Render for Harness {
|
|||||||
})
|
})
|
||||||
.tabs(|_, _| {
|
.tabs(|_, _| {
|
||||||
vec![
|
vec![
|
||||||
Tab::new("One"),
|
Tab::new("one", "One"),
|
||||||
Tab::new("Two"),
|
Tab::new("two", "Two"),
|
||||||
Tab::new("Off").disabled(true),
|
Tab::new("off", "Off").disabled(true),
|
||||||
Tab::new("End"),
|
Tab::new("end", "End"),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ use gpui::{
|
|||||||
actions, canvas, div, uniform_list,
|
actions, canvas, div, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, MONO_ADVANCE_RATIO, Ramps};
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, Language, Role, highlight_with_tree};
|
use crate::highlight::{HighlightedCode, Language, Role, highlight};
|
||||||
use crate::utils::{gutter_cell, run_style};
|
use crate::utils::{gutter_cell, run_style};
|
||||||
|
|
||||||
actions!(code_view, [Copy]);
|
actions!(code_view, [Copy]);
|
||||||
@@ -53,8 +53,6 @@ pub struct CodeViewState {
|
|||||||
source: SharedString,
|
source: SharedString,
|
||||||
language: Language,
|
language: Language,
|
||||||
code: HighlightedCode,
|
code: HighlightedCode,
|
||||||
/// The parse tree of `source`, so updates reparse incrementally instead of from scratch.
|
|
||||||
tree: Option<tree_sitter::Tree>,
|
|
||||||
handle: UniformListScrollHandle,
|
handle: UniformListScrollHandle,
|
||||||
/// The hovered line, driving the active-line wash and number emphasis.
|
/// The hovered line, driving the active-line wash and number emphasis.
|
||||||
active_line: Option<usize>,
|
active_line: Option<usize>,
|
||||||
@@ -74,14 +72,13 @@ impl CodeViewState {
|
|||||||
S: Into<SharedString>,
|
S: Into<SharedString>,
|
||||||
{
|
{
|
||||||
let source = source.into();
|
let source = source.into();
|
||||||
let (code, tree) = highlight_with_tree(&source, language, None);
|
let code = highlight(&source, language);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
source,
|
source,
|
||||||
language,
|
language,
|
||||||
code,
|
code,
|
||||||
tree,
|
|
||||||
handle: UniformListScrollHandle::new(),
|
handle: UniformListScrollHandle::new(),
|
||||||
active_line: None,
|
active_line: None,
|
||||||
selection: None,
|
selection: None,
|
||||||
@@ -98,18 +95,9 @@ impl CodeViewState {
|
|||||||
{
|
{
|
||||||
let source = source.into();
|
let source = source.into();
|
||||||
|
|
||||||
// The old tree is only reusable when the grammar stays the same.
|
self.code = highlight(&source, language);
|
||||||
let old_tree = if language == self.language {
|
|
||||||
self.tree.as_ref()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (code, tree) = highlight_with_tree(&source, language, old_tree);
|
|
||||||
self.source = source;
|
self.source = source;
|
||||||
self.language = language;
|
self.language = language;
|
||||||
self.code = code;
|
|
||||||
self.tree = tree;
|
|
||||||
self.selection = None;
|
self.selection = None;
|
||||||
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||||
|
|
||||||
@@ -134,9 +122,7 @@ impl CodeViewState {
|
|||||||
lines.splice(line_ix..line_ix + 1, replacement);
|
lines.splice(line_ix..line_ix + 1, replacement);
|
||||||
self.source = (lines.join("\n") + if trailing_newline { "\n" } else { "" }).into();
|
self.source = (lines.join("\n") + if trailing_newline { "\n" } else { "" }).into();
|
||||||
|
|
||||||
let (code, tree) = highlight_with_tree(&self.source, self.language, self.tree.as_ref());
|
self.code = highlight(&self.source, self.language);
|
||||||
self.code = code;
|
|
||||||
self.tree = tree;
|
|
||||||
self.selection = None;
|
self.selection = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -146,11 +132,6 @@ impl CodeViewState {
|
|||||||
&self.source
|
&self.source
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The language the source highlights as.
|
|
||||||
pub fn language(&self) -> Language {
|
|
||||||
self.language
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The highlighted lines.
|
/// The highlighted lines.
|
||||||
pub fn code(&self) -> &HighlightedCode {
|
pub fn code(&self) -> &HighlightedCode {
|
||||||
&self.code
|
&self.code
|
||||||
@@ -223,13 +204,7 @@ impl CodeViewState {
|
|||||||
(!selected.is_empty()).then(|| selected.join("\n"))
|
(!selected.is_empty()).then(|| selected.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drops the selection.
|
// Overrides the accessibility label of the view.
|
||||||
pub fn clear_selection(&mut self) {
|
|
||||||
self.selection = None;
|
|
||||||
self.selecting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overrides the accessibility label of the view.
|
|
||||||
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
||||||
where
|
where
|
||||||
L: Into<SharedString>,
|
L: Into<SharedString>,
|
||||||
@@ -249,8 +224,7 @@ impl CodeViewState {
|
|||||||
text_x: Pixels,
|
text_x: Pixels,
|
||||||
) -> SelectionPoint {
|
) -> SelectionPoint {
|
||||||
let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize)
|
let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize)
|
||||||
.clamp(0, self.code.lines.len() as isize - 1)
|
.clamp(0, self.code.lines.len() as isize - 1) as usize;
|
||||||
.max(0) as usize;
|
|
||||||
|
|
||||||
let chars = self.code.lines[row]
|
let chars = self.code.lines[row]
|
||||||
.spans
|
.spans
|
||||||
@@ -258,9 +232,8 @@ impl CodeViewState {
|
|||||||
.map(|span| span.text.chars().count())
|
.map(|span| span.text.chars().count())
|
||||||
.sum::<usize>();
|
.sum::<usize>();
|
||||||
|
|
||||||
let col = (((position.x - text_x) / char_width).round() as isize)
|
let col = (((position.x - text_x) / char_width).round() as isize).clamp(0, chars as isize)
|
||||||
.clamp(0, chars as isize)
|
as usize;
|
||||||
.max(0) as usize;
|
|
||||||
|
|
||||||
SelectionPoint { row, col }
|
SelectionPoint { row, col }
|
||||||
}
|
}
|
||||||
@@ -279,7 +252,7 @@ impl Render for CodeViewState {
|
|||||||
let mono_font = theme.mono_font;
|
let mono_font = theme.mono_font;
|
||||||
let text_size = theme.sizes.small.text_size;
|
let text_size = theme.sizes.small.text_size;
|
||||||
let line_height = text_size * 1.5;
|
let line_height = text_size * 1.5;
|
||||||
let char_width = text_size * 0.62;
|
let char_width = text_size * MONO_ADVANCE_RATIO;
|
||||||
let root_font_size = theme.root_font_size;
|
let root_font_size = theme.root_font_size;
|
||||||
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
@@ -348,7 +321,7 @@ impl Render for CodeViewState {
|
|||||||
code.position_at(
|
code.position_at(
|
||||||
*bounds,
|
*bounds,
|
||||||
event.position,
|
event.position,
|
||||||
Pixels::ZERO,
|
ScrollbarHandle::offset(&code.handle).y,
|
||||||
line_height,
|
line_height,
|
||||||
char_width,
|
char_width,
|
||||||
text_x,
|
text_x,
|
||||||
@@ -453,7 +426,8 @@ impl Render for CodeViewState {
|
|||||||
)
|
)
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.when_some(self.a11y_label.clone(), |view, label| {
|
||||||
view.aria_label(label)
|
view.aria_label(label)
|
||||||
@@ -475,10 +449,10 @@ fn render_row(
|
|||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
let line = code.code().lines.get(line_ix).cloned().unwrap_or_default();
|
let line = code.code().lines.get(line_ix);
|
||||||
|
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
for span in &line.spans {
|
for span in line.map_or(&[][..], |line| line.spans.as_slice()) {
|
||||||
text.push_str(&span.text);
|
text.push_str(&span.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +460,7 @@ fn render_row(
|
|||||||
let mut roles = vec![Role::Text; char_count];
|
let mut roles = vec![Role::Text; char_count];
|
||||||
let mut offset = 0usize;
|
let mut offset = 0usize;
|
||||||
|
|
||||||
for span in &line.spans {
|
for span in line.map_or(&[][..], |line| line.spans.as_slice()) {
|
||||||
let chars = span.text.chars().count();
|
let chars = span.text.chars().count();
|
||||||
for slot in &mut roles[offset..offset + chars] {
|
for slot in &mut roles[offset..offset + chars] {
|
||||||
*slot = span.role;
|
*slot = span.role;
|
||||||
@@ -556,10 +530,7 @@ fn render_row(
|
|||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.debug_selector({
|
.debug_selector(move || format!("{id}-row-{line_ix}"))
|
||||||
let name = format!("{id}-row-{line_ix}");
|
|
||||||
move || name
|
|
||||||
})
|
|
||||||
.id(("code-row", line_ix))
|
.id(("code-row", line_ix))
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(line_height)
|
.h(line_height)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use gpui::{
|
|||||||
point, uniform_list,
|
point, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::{ActiveTheme as _, MONO_ADVANCE_RATIO};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
@@ -237,6 +237,8 @@ pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
|
|||||||
pub struct DiffViewState {
|
pub struct DiffViewState {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
rows: Vec<DiffRow>,
|
rows: Vec<DiffRow>,
|
||||||
|
/// The row whose width drives horizontal scroll extent, `rows`-derived.
|
||||||
|
widest_row: Option<usize>,
|
||||||
options: DiffViewOptions,
|
options: DiffViewOptions,
|
||||||
/// Drives both scroll axes of the virtualized row list.
|
/// Drives both scroll axes of the virtualized row list.
|
||||||
handle: UniformListScrollHandle,
|
handle: UniformListScrollHandle,
|
||||||
@@ -251,13 +253,32 @@ impl DiffViewState {
|
|||||||
O: AsRef<str>,
|
O: AsRef<str>,
|
||||||
N: AsRef<str>,
|
N: AsRef<str>,
|
||||||
{
|
{
|
||||||
Self {
|
let mut state = Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
rows: diff_rows(old.as_ref(), new.as_ref(), language),
|
rows: Vec::new(),
|
||||||
|
widest_row: None,
|
||||||
options: DiffViewOptions::default(),
|
options: DiffViewOptions::default(),
|
||||||
handle: UniformListScrollHandle::new(),
|
handle: UniformListScrollHandle::new(),
|
||||||
a11y_label: None,
|
a11y_label: None,
|
||||||
|
};
|
||||||
|
state.set_rows(old, new, language);
|
||||||
|
state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replaces the rows, recomputing `widest_row` with them.
|
||||||
|
fn set_rows<O, N>(&mut self, old: O, new: N, language: Language)
|
||||||
|
where
|
||||||
|
O: AsRef<str>,
|
||||||
|
N: AsRef<str>,
|
||||||
|
{
|
||||||
|
self.rows = diff_rows(old.as_ref(), new.as_ref(), language);
|
||||||
|
self.widest_row = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, row)| row.kind != DiffRowKind::Hunk)
|
||||||
|
.max_by_key(|(_, row)| row.char_count())
|
||||||
|
.map(|(ix, _)| ix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets how changed rows are marked.
|
/// Sets how changed rows are marked.
|
||||||
@@ -284,7 +305,7 @@ impl DiffViewState {
|
|||||||
O: AsRef<str>,
|
O: AsRef<str>,
|
||||||
N: AsRef<str>,
|
N: AsRef<str>,
|
||||||
{
|
{
|
||||||
self.rows = diff_rows(old.as_ref(), new.as_ref(), language);
|
self.set_rows(old, new, language);
|
||||||
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
self.handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||||
// The replaced versions rarely share the old width; start at the left.
|
// The replaced versions rarely share the old width; start at the left.
|
||||||
let offset = ScrollbarHandle::offset(&self.handle);
|
let offset = ScrollbarHandle::offset(&self.handle);
|
||||||
@@ -328,14 +349,7 @@ impl Render for DiffViewState {
|
|||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
let row_count = self.rows.len();
|
let row_count = self.rows.len();
|
||||||
let options = self.options;
|
let options = self.options;
|
||||||
|
let widest_row = self.widest_row;
|
||||||
let widest_row = self
|
|
||||||
.rows
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, row)| row.kind != DiffRowKind::Hunk)
|
|
||||||
.max_by_key(|(_, row)| row.char_count())
|
|
||||||
.map(|(ix, _)| ix);
|
|
||||||
|
|
||||||
let mut rows = uniform_list((id.clone(), "rows"), row_count, {
|
let mut rows = uniform_list((id.clone(), "rows"), row_count, {
|
||||||
move |range, _window, cx| {
|
move |range, _window, cx| {
|
||||||
@@ -363,13 +377,11 @@ impl Render for DiffViewState {
|
|||||||
})
|
})
|
||||||
.child(rows)
|
.child(rows)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone())
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
.key("v-scrollbar")
|
|
||||||
.visibility(ScrollbarVisibility::Platform),
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::horizontal(self.handle.clone())
|
Scrollbar::horizontal((self.id.clone(), "h-scrollbar"), self.handle.clone())
|
||||||
.key("h-scrollbar")
|
|
||||||
.visibility(ScrollbarVisibility::Platform),
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -387,7 +399,7 @@ fn render_row(
|
|||||||
let ramps = theme.ramps;
|
let ramps = theme.ramps;
|
||||||
let text_size = theme.sizes.small.text_size;
|
let text_size = theme.sizes.small.text_size;
|
||||||
let line_height = text_size * 1.5;
|
let line_height = text_size * 1.5;
|
||||||
let char_width = text_size * 0.62;
|
let char_width = text_size * MONO_ADVANCE_RATIO;
|
||||||
|
|
||||||
let frame = div()
|
let frame = div()
|
||||||
.debug_selector(move || format!("diff-row-{row_ix}"))
|
.debug_selector(move || format!("diff-row-{row_ix}"))
|
||||||
|
|||||||
@@ -67,17 +67,8 @@ pub struct HighlightedCode {
|
|||||||
|
|
||||||
/// Highlights `source` with `language`'s tree-sitter grammar.
|
/// Highlights `source` with `language`'s tree-sitter grammar.
|
||||||
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
||||||
highlight_with_tree(source, language, None).0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlights `source`, reusing `old_tree` so only the changed region is re-lexed.
|
|
||||||
pub fn highlight_with_tree(
|
|
||||||
source: &str,
|
|
||||||
language: Language,
|
|
||||||
old_tree: Option<&tree_sitter::Tree>,
|
|
||||||
) -> (HighlightedCode, Option<tree_sitter::Tree>) {
|
|
||||||
let Some((grammar, query_source)) = grammar_for(language) else {
|
let Some((grammar, query_source)) = grammar_for(language) else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
HIGHLIGHTERS.with(|cache| {
|
HIGHLIGHTERS.with(|cache| {
|
||||||
@@ -104,11 +95,11 @@ pub fn highlight_with_tree(
|
|||||||
.as_mut();
|
.as_mut();
|
||||||
|
|
||||||
let Some(highlighter) = highlighter else {
|
let Some(highlighter) = highlighter else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(tree) = highlighter.parser.parse(source, old_tree) else {
|
let Some(tree) = highlighter.parser.parse(source, None) else {
|
||||||
return (plain(source), None);
|
return plain(source);
|
||||||
};
|
};
|
||||||
|
|
||||||
// One role per byte; matches arrive in query order
|
// One role per byte; matches arrive in query order
|
||||||
@@ -134,7 +125,7 @@ pub fn highlight_with_tree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(build_lines(source, &role_at), Some(tree))
|
build_lines(source, &role_at)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, T
|
|||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, highlight_with_tree, plain};
|
use crate::highlight::{HighlightedCode, highlight, plain};
|
||||||
use crate::utils::{code_line, grammar_language};
|
use crate::utils::{code_line, grammar_language};
|
||||||
|
|
||||||
/// Heading scale steps relative to the root font size, H1 first.
|
/// Heading scale steps relative to the root font size, H1 first.
|
||||||
@@ -101,8 +101,6 @@ pub enum InlineStyle {
|
|||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct MarkdownTableCell {
|
pub struct MarkdownTableCell {
|
||||||
pub spans: Vec<InlineSpan>,
|
pub spans: Vec<InlineSpan>,
|
||||||
/// 1-based column position, so renderers can align to the table's columns.
|
|
||||||
pub column: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A table row; the header row precedes the body rows.
|
/// A table row; the header row precedes the body rows.
|
||||||
@@ -277,7 +275,8 @@ impl Render for MarkdownViewState {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.child(blocks)
|
.child(blocks)
|
||||||
.child(
|
.child(
|
||||||
Scrollbar::vertical(self.handle.clone()).visibility(ScrollbarVisibility::Platform),
|
Scrollbar::vertical((self.id.clone(), "v-scrollbar"), self.handle.clone())
|
||||||
|
.visibility(ScrollbarVisibility::Platform),
|
||||||
)
|
)
|
||||||
.when_some(self.a11y_label.clone(), |view, label| {
|
.when_some(self.a11y_label.clone(), |view, label| {
|
||||||
view.aria_label(label)
|
view.aria_label(label)
|
||||||
@@ -292,24 +291,6 @@ struct ListFrame {
|
|||||||
task: Option<bool>,
|
task: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The table being assembled, when the parser is inside one.
|
|
||||||
enum TableBuilder {
|
|
||||||
None,
|
|
||||||
Body { rows: Vec<MarkdownTableRow> },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One open code block, accumulating its raw text.
|
|
||||||
#[derive(Default)]
|
|
||||||
struct CodeBuilder {
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeBuilder {
|
|
||||||
fn push(&mut self, text: &str) {
|
|
||||||
self.text.push_str(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accumulates inline spans across inline events.
|
/// Accumulates inline spans across inline events.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct InlineBuilder {
|
struct InlineBuilder {
|
||||||
@@ -402,8 +383,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
// Whether each open list item already flushed content rows.
|
// Whether each open list item already flushed content rows.
|
||||||
let mut items: Vec<bool> = Vec::new();
|
let mut items: Vec<bool> = Vec::new();
|
||||||
let mut quote_depth = 0usize;
|
let mut quote_depth = 0usize;
|
||||||
let mut table = TableBuilder::None;
|
let mut table: Option<Vec<MarkdownTableRow>> = None;
|
||||||
let mut code: Option<CodeBuilder> = None;
|
let mut code: Option<String> = None;
|
||||||
// The fence info string arrives on Start, its text follows.
|
// The fence info string arrives on Start, its text follows.
|
||||||
let mut code_language: Option<SharedString> = None;
|
let mut code_language: Option<SharedString> = None;
|
||||||
|
|
||||||
@@ -468,7 +449,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
&mut blocks,
|
&mut blocks,
|
||||||
quote_depth,
|
quote_depth,
|
||||||
);
|
);
|
||||||
code = Some(CodeBuilder::default());
|
code = Some(String::new());
|
||||||
if let CodeBlockKind::Fenced(info) = kind {
|
if let CodeBlockKind::Fenced(info) = kind {
|
||||||
let info = info.trim();
|
let info = info.trim();
|
||||||
code_language =
|
code_language =
|
||||||
@@ -483,10 +464,10 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
&mut blocks,
|
&mut blocks,
|
||||||
quote_depth,
|
quote_depth,
|
||||||
);
|
);
|
||||||
table = TableBuilder::Body { rows: Vec::new() }
|
table = Some(Vec::new())
|
||||||
}
|
}
|
||||||
Tag::TableHead => {
|
Tag::TableHead => {
|
||||||
if let TableBuilder::Body { rows } = &mut table {
|
if let Some(rows) = &mut table {
|
||||||
rows.push(MarkdownTableRow {
|
rows.push(MarkdownTableRow {
|
||||||
cells: Vec::new(),
|
cells: Vec::new(),
|
||||||
header: true,
|
header: true,
|
||||||
@@ -494,7 +475,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tag::TableRow => {
|
Tag::TableRow => {
|
||||||
if let TableBuilder::Body { rows } = &mut table {
|
if let Some(rows) = &mut table {
|
||||||
rows.push(MarkdownTableRow {
|
rows.push(MarkdownTableRow {
|
||||||
cells: Vec::new(),
|
cells: Vec::new(),
|
||||||
header: false,
|
header: false,
|
||||||
@@ -582,13 +563,13 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
}
|
}
|
||||||
TagEnd::CodeBlock => {
|
TagEnd::CodeBlock => {
|
||||||
let built = code.take().unwrap_or_default();
|
let built = code.take().unwrap_or_default();
|
||||||
let text = built.text.trim_end_matches('\n');
|
let text = built.trim_end_matches('\n');
|
||||||
let language = code_language.take();
|
let language = code_language.take();
|
||||||
let lines = match highlight_code
|
let lines = match highlight_code
|
||||||
.then(|| language.as_deref().and_then(grammar_language))
|
.then(|| language.as_deref().and_then(grammar_language))
|
||||||
.flatten()
|
.flatten()
|
||||||
{
|
{
|
||||||
Some(grammar) => highlight_with_tree(text, grammar, None).0,
|
Some(grammar) => highlight(text, grammar),
|
||||||
None => plain(text),
|
None => plain(text),
|
||||||
};
|
};
|
||||||
blocks.push(MarkdownBlock::Code {
|
blocks.push(MarkdownBlock::Code {
|
||||||
@@ -599,8 +580,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
TagEnd::Table => {
|
TagEnd::Table => {
|
||||||
let built = std::mem::replace(&mut table, TableBuilder::None);
|
if let Some(rows) = table.take() {
|
||||||
if let TableBuilder::Body { rows } = built {
|
|
||||||
blocks.push(MarkdownBlock::Table {
|
blocks.push(MarkdownBlock::Table {
|
||||||
table: MarkdownTable { rows },
|
table: MarkdownTable { rows },
|
||||||
indent: lists.len(),
|
indent: lists.len(),
|
||||||
@@ -610,16 +590,13 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
}
|
}
|
||||||
TagEnd::TableCell => {
|
TagEnd::TableCell => {
|
||||||
let spans = inline.finish();
|
let spans = inline.finish();
|
||||||
let TableBuilder::Body { rows } = &mut table else {
|
let Some(rows) = &mut table else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(row) = rows.last_mut() else {
|
let Some(row) = rows.last_mut() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
row.cells.push(MarkdownTableCell {
|
row.cells.push(MarkdownTableCell { spans });
|
||||||
spans,
|
|
||||||
column: row.cells.len() + 1,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
TagEnd::Emphasis
|
TagEnd::Emphasis
|
||||||
| TagEnd::Strong
|
| TagEnd::Strong
|
||||||
@@ -629,8 +606,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Event::Text(text) => {
|
Event::Text(text) => {
|
||||||
if let Some(built) = code.as_mut() {
|
if let Some(buffer) = code.as_mut() {
|
||||||
built.push(&text);
|
buffer.push_str(&text);
|
||||||
} else {
|
} else {
|
||||||
inline.push_text(&text);
|
inline.push_text(&text);
|
||||||
}
|
}
|
||||||
@@ -659,9 +636,9 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
|||||||
frame.task = Some(checked);
|
frame.task = Some(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::FootnoteReference(label) => inline.push_text(&label),
|
// Footnotes are disabled in the parser options.
|
||||||
// HTML and math render nothing in this view.
|
Event::FootnoteReference(_)
|
||||||
Event::InlineHtml(_)
|
| Event::InlineHtml(_)
|
||||||
| Event::Html(_)
|
| Event::Html(_)
|
||||||
| Event::InlineMath(_)
|
| Event::InlineMath(_)
|
||||||
| Event::DisplayMath(_) => {}
|
| Event::DisplayMath(_) => {}
|
||||||
@@ -783,8 +760,8 @@ fn block_frame(
|
|||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let content = div()
|
let content = div()
|
||||||
.debug_selector({
|
.debug_selector({
|
||||||
let name = SharedString::from(format!("{id}"));
|
let id = id.clone();
|
||||||
move || name.to_string()
|
move || id.to_string()
|
||||||
})
|
})
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -305,14 +305,6 @@ fn tables_parse_header_first_with_column_positions() {
|
|||||||
.map(|cell| spans_text(&cell.spans))
|
.map(|cell| spans_text(&cell.spans))
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(header, vec!["a", "b"]);
|
assert_eq!(header, vec!["a", "b"]);
|
||||||
assert_eq!(
|
|
||||||
table.rows[0]
|
|
||||||
.cells
|
|
||||||
.iter()
|
|
||||||
.map(|cell| cell.column)
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![1, 2]
|
|
||||||
);
|
|
||||||
assert_eq!(spans_text(&table.rows[2].cells[1].spans), "4");
|
assert_eq!(spans_text(&table.rows[2].cells[1].spans), "4");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "gpui-native-icons"
|
name = "gpui-native-icons"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "System icons for GPUI: semantic roles resolved per platform, with a bundled Lucide fallback"
|
description = "System icons for GPUI: semantic roles resolved per platform, a bundled Lucide fallback, and app-defined icon sets"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|||||||
+106
-40
@@ -1,10 +1,11 @@
|
|||||||
//! System icons for GPUI: components and apps reference semantic roles,
|
//! System icons for GPUI: components and apps reference semantic roles,
|
||||||
//! and the crate resolves each role to a glyph for the current platform.
|
//! and the crate resolves each role to a glyph for the current platform.
|
||||||
|
//! Apps ship their own icon set by implementing [`IconNamed`].
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
|
AnyElement, AssetSource, Hsla, IntoElement, ParentElement as _, Pixels, RenderOnce,
|
||||||
Styled as _, Transformation, Window, div, px, svg,
|
SharedString, Styled as _, Transformation, Window, div, px, svg,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
@@ -194,10 +195,29 @@ impl IconRole {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A glyph named by an application's own icon set.
|
||||||
|
pub trait IconNamed {
|
||||||
|
/// The SVG asset path, e.g. `icons/commit.svg`.
|
||||||
|
fn path(self) -> SharedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconNamed for IconRole {
|
||||||
|
fn path(self) -> SharedString {
|
||||||
|
SharedString::from(self.asset_path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The glyph an icon draws.
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Glyph {
|
||||||
|
Role(IconRole),
|
||||||
|
Named(SharedString),
|
||||||
|
}
|
||||||
|
|
||||||
/// An icon element. Unpainted unless a color is set.
|
/// An icon element. Unpainted unless a color is set.
|
||||||
#[derive(IntoElement)]
|
#[derive(Clone, IntoElement)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
role: IconRole,
|
glyph: Glyph,
|
||||||
size: Pixels,
|
size: Pixels,
|
||||||
color: Option<Hsla>,
|
color: Option<Hsla>,
|
||||||
transformation: Option<Transformation>,
|
transformation: Option<Transformation>,
|
||||||
@@ -206,7 +226,7 @@ pub struct Icon {
|
|||||||
/// Creates an icon for the role at the default size of 16px.
|
/// Creates an icon for the role at the default size of 16px.
|
||||||
pub fn icon(role: IconRole) -> Icon {
|
pub fn icon(role: IconRole) -> Icon {
|
||||||
Icon {
|
Icon {
|
||||||
role,
|
glyph: Glyph::Role(role),
|
||||||
size: px(16.0),
|
size: px(16.0),
|
||||||
color: None,
|
color: None,
|
||||||
transformation: None,
|
transformation: None,
|
||||||
@@ -214,6 +234,19 @@ pub fn icon(role: IconRole) -> Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
|
/// Creates an icon from an application-defined icon set.
|
||||||
|
pub fn named<I>(named: I) -> Self
|
||||||
|
where
|
||||||
|
I: IconNamed,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
glyph: Glyph::Named(named.path()),
|
||||||
|
size: px(16.0),
|
||||||
|
color: None,
|
||||||
|
transformation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size(mut self, size: Pixels) -> Self {
|
pub fn size(mut self, size: Pixels) -> Self {
|
||||||
self.size = size;
|
self.size = size;
|
||||||
self
|
self
|
||||||
@@ -232,65 +265,98 @@ impl Icon {
|
|||||||
|
|
||||||
/// The asset key a resolved freedesktop file is served under.
|
/// The asset key a resolved freedesktop file is served under.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn freedesktop_asset_path(&self) -> Option<SharedString> {
|
fn freedesktop_asset_path(size: Pixels, role: &IconRole) -> Option<SharedString> {
|
||||||
let size = self.size.as_f32().max(1.0).round() as u32;
|
let size = size.as_f32().max(1.0).round() as u32;
|
||||||
freedesktop::cached_file(self.role, size).map(|_| {
|
freedesktop::cached_file(*role, size)
|
||||||
SharedString::from(format!("icons/freedesktop/{}-{size}.svg", self.role.slug()))
|
.map(|_| SharedString::from(format!("icons/freedesktop/{}-{size}.svg", role.slug())))
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Icon {
|
/// Roles resolve per platform.
|
||||||
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
fn render_role(
|
||||||
|
size: Pixels,
|
||||||
|
color: Option<Hsla>,
|
||||||
|
transformation: Option<Transformation>,
|
||||||
|
role: IconRole,
|
||||||
|
) -> AnyElement {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return self.render_glyph();
|
if let Some(glyph) = segoe::glyph(role) {
|
||||||
#[cfg(not(target_os = "windows"))]
|
return Self::render_glyph(size, color, glyph).into_any_element();
|
||||||
return self.render_svg();
|
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Some(path) = Self::freedesktop_asset_path(size, &role) {
|
||||||
|
return Self::render_svg_path(size, color, transformation, path).into_any_element();
|
||||||
|
}
|
||||||
|
Self::render_svg_path(
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
transformation,
|
||||||
|
SharedString::from(role.asset_path()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
/// Windows: the Segoe Fluent codepoint drawn as text in the system font.
|
||||||
/// Windows: the Segoe Fluent codepoint drawn as text in the system font;
|
|
||||||
/// roles without a codepoint fall back to the bundled SVG.
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn render_glyph(self) -> impl IntoElement {
|
fn render_glyph(size: Pixels, color: Option<Hsla>, glyph: char) -> AnyElement {
|
||||||
match segoe::glyph(self.role) {
|
div()
|
||||||
Some(glyph) => div()
|
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.size(self.size)
|
.size(size)
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.font_family(segoe::FONT_FAMILY)
|
.font_family(segoe::FONT_FAMILY)
|
||||||
.text_size(self.size)
|
.text_size(size)
|
||||||
.when_some(self.color, |el, color| el.text_color(color))
|
.when_some(color, |el, color| el.text_color(color))
|
||||||
.child(glyph.to_string()),
|
.child(glyph.to_string()),
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element()
|
||||||
None => self.render_svg().into_any_element(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_svg(self) -> impl IntoElement {
|
fn render_svg_path(
|
||||||
#[cfg(target_os = "linux")]
|
size: Pixels,
|
||||||
let path: SharedString = self
|
color: Option<Hsla>,
|
||||||
.freedesktop_asset_path()
|
transformation: Option<Transformation>,
|
||||||
.unwrap_or_else(|| SharedString::from(self.role.asset_path()));
|
path: SharedString,
|
||||||
|
) -> AnyElement {
|
||||||
#[cfg(not(target_os = "linux"))]
|
div()
|
||||||
let path: SharedString = SharedString::from(self.role.asset_path());
|
.flex_none()
|
||||||
|
.size(size)
|
||||||
div().flex_none().size(self.size).child(
|
.child(
|
||||||
svg()
|
svg()
|
||||||
.size_full()
|
.size_full()
|
||||||
.path(path)
|
.path(path)
|
||||||
.when_some(self.color, |el, color| el.text_color(color))
|
.when_some(color, |el, color| el.text_color(color))
|
||||||
.when_some(self.transformation, |el, transformation| {
|
.when_some(transformation, |el, transformation| {
|
||||||
el.with_transformation(transformation)
|
el.with_transformation(transformation)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> From<I> for Icon
|
||||||
|
where
|
||||||
|
I: IconNamed,
|
||||||
|
{
|
||||||
|
fn from(named: I) -> Self {
|
||||||
|
Icon::named(named)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for Icon {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut gpui::App) -> impl IntoElement {
|
||||||
|
let Icon {
|
||||||
|
glyph,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
transformation,
|
||||||
|
} = self;
|
||||||
|
match glyph {
|
||||||
|
Glyph::Named(path) => Self::render_svg_path(size, color, transformation, path),
|
||||||
|
Glyph::Role(role) => Self::render_role(size, color, transformation, role),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ default = ["full"]
|
|||||||
full = ["dock", "resize_handle", "resizable", "shell", "window_controls"]
|
full = ["dock", "resize_handle", "resizable", "shell", "window_controls"]
|
||||||
dock = [
|
dock = [
|
||||||
"resize_handle",
|
"resize_handle",
|
||||||
"resizable",
|
|
||||||
"dep:gpui-native-core",
|
"dep:gpui-native-core",
|
||||||
"dep:gpui-native-data",
|
"dep:gpui-native-data",
|
||||||
"dep:gpui-native-icons",
|
"dep:gpui-native-icons",
|
||||||
|
|||||||
+53
-32
@@ -15,7 +15,7 @@ use gpui_native_core::common::{h_flex, v_flex};
|
|||||||
use gpui_native_core::context_menu::ContextMenu;
|
use gpui_native_core::context_menu::ContextMenu;
|
||||||
use gpui_native_core::menu::MenuItem;
|
use gpui_native_core::menu::MenuItem;
|
||||||
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
|
use gpui_native_data::tabs::{Tab, Tabs, TabsVariant};
|
||||||
use gpui_native_icons::IconRole;
|
use gpui_native_icons::{Icon, IconRole};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
|
|
||||||
use crate::resize_handle::ResizeHandle;
|
use crate::resize_handle::ResizeHandle;
|
||||||
@@ -35,7 +35,7 @@ type OpenRightSidebarHandler = Rc<dyn Fn(&mut App)>;
|
|||||||
pub struct PanelDescriptor {
|
pub struct PanelDescriptor {
|
||||||
id: SharedString,
|
id: SharedString,
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
build: PanelBuilder,
|
build: PanelBuilder,
|
||||||
right_sidebar: Option<PanelBuilder>,
|
right_sidebar: Option<PanelBuilder>,
|
||||||
}
|
}
|
||||||
@@ -58,8 +58,11 @@ impl PanelDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows an icon leading the tab label.
|
/// Shows an icon leading the tab label.
|
||||||
pub fn icon(mut self, role: IconRole) -> Self {
|
pub fn icon<I>(mut self, icon: I) -> Self
|
||||||
self.icon = Some(role);
|
where
|
||||||
|
I: Into<Icon>,
|
||||||
|
{
|
||||||
|
self.icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +133,7 @@ impl Render for DragPreview {
|
|||||||
struct PanelTab {
|
struct PanelTab {
|
||||||
panel: SharedString,
|
panel: SharedString,
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
view: AnyView,
|
view: AnyView,
|
||||||
right_view: Option<AnyView>,
|
right_view: Option<AnyView>,
|
||||||
}
|
}
|
||||||
@@ -192,10 +195,8 @@ impl DockState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a panel descriptor, replacing any earlier one with its id.
|
/// Registers a panel descriptor, replacing any earlier one with its id.
|
||||||
pub fn register_panel(&mut self, panel: PanelDescriptor, cx: &mut Context<Self>) {
|
pub fn register_panel(&mut self, panel: PanelDescriptor) {
|
||||||
if self.panels.insert(panel.id.clone(), panel).is_none() {
|
self.panels.insert(panel.id.clone(), panel);
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a panel into the active pane and activates its tab.
|
/// Opens a panel into the active pane and activates its tab.
|
||||||
@@ -217,7 +218,7 @@ impl DockState {
|
|||||||
let tab = PanelTab {
|
let tab = PanelTab {
|
||||||
panel: SharedString::from(id),
|
panel: SharedString::from(id),
|
||||||
title: panel.title.clone(),
|
title: panel.title.clone(),
|
||||||
icon: panel.icon,
|
icon: panel.icon.clone(),
|
||||||
view: (panel.build)(window, cx),
|
view: (panel.build)(window, cx),
|
||||||
right_view,
|
right_view,
|
||||||
};
|
};
|
||||||
@@ -330,6 +331,17 @@ impl DockState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
|
// The kept tab may have no right view, show the first remaining one.
|
||||||
|
self.right_view = self
|
||||||
|
.locate(id)
|
||||||
|
.and_then(|(pane_key, _)| self.pane(pane_key))
|
||||||
|
.and_then(|pane| {
|
||||||
|
pane.active.as_deref().and_then(|active| {
|
||||||
|
pane.tabs.iter().find(|tab| tab.panel.as_ref() == active)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|tab| tab.right_view.clone())
|
||||||
|
.or_else(|| first_right_view(&self.root));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,10 +366,14 @@ impl DockState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.detach_pane(pane_key);
|
self.detach_pane(pane_key);
|
||||||
|
|
||||||
if self.active_pane == pane_key {
|
if self.active_pane == pane_key {
|
||||||
self.active_pane = self.first_pane_key().unwrap_or(0);
|
self.active_pane = self.first_pane_key().unwrap_or(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The closed pane's views are gone; show the first remaining one.
|
||||||
|
self.right_view = first_right_view(&self.root);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +542,19 @@ impl DockState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pane(&self, key: u64) -> Option<&Pane> {
|
||||||
|
Self::pane_in(&self.root, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pane_in(node: &Node, key: u64) -> Option<&Pane> {
|
||||||
|
match node {
|
||||||
|
Node::Pane(pane) => (pane.key == key).then_some(pane),
|
||||||
|
Node::Split(split) => {
|
||||||
|
Self::pane_in(&split.a, key).or_else(|| Self::pane_in(&split.b, key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pane_mut(&mut self, key: u64) -> Option<&mut Pane> {
|
fn pane_mut(&mut self, key: u64) -> Option<&mut Pane> {
|
||||||
Self::pane_mut_in(&mut self.root, key)
|
Self::pane_mut_in(&mut self.root, key)
|
||||||
}
|
}
|
||||||
@@ -653,7 +682,7 @@ impl DockState {
|
|||||||
.map(|tab| TabView {
|
.map(|tab| TabView {
|
||||||
panel: tab.panel.clone(),
|
panel: tab.panel.clone(),
|
||||||
title: tab.title.clone(),
|
title: tab.title.clone(),
|
||||||
icon: tab.icon,
|
icon: tab.icon.clone(),
|
||||||
view: tab.view.clone(),
|
view: tab.view.clone(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -721,7 +750,7 @@ fn take_node(node: &mut Node) -> Node {
|
|||||||
struct TabView {
|
struct TabView {
|
||||||
panel: SharedString,
|
panel: SharedString,
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
icon: Option<IconRole>,
|
icon: Option<Icon>,
|
||||||
view: AnyView,
|
view: AnyView,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,15 +998,10 @@ fn render_pane(
|
|||||||
.map(|tab| tab.view.clone());
|
.map(|tab| tab.view.clone());
|
||||||
|
|
||||||
// The cache keeps an un-notified panel from re-rendering on dock frames.
|
// The cache keeps an un-notified panel from re-rendering on dock frames.
|
||||||
|
let content_id = SharedString::from(format!("{dock_id}-content-{}", pane.key));
|
||||||
let content = div()
|
let content = div()
|
||||||
.id(SharedString::from(format!(
|
.id(content_id.clone())
|
||||||
"{dock_id}-content-{}",
|
.debug_selector(move || content_id.to_string())
|
||||||
pane.key
|
|
||||||
)))
|
|
||||||
.debug_selector({
|
|
||||||
let id = SharedString::from(format!("{dock_id}-content-{}", pane.key));
|
|
||||||
move || id.to_string()
|
|
||||||
})
|
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
@@ -999,6 +1023,7 @@ fn render_pane(
|
|||||||
let pane = pane.clone();
|
let pane = pane.clone();
|
||||||
let titlebar = *titlebar;
|
let titlebar = *titlebar;
|
||||||
let padding_x = metrics.dense_padding_x;
|
let padding_x = metrics.dense_padding_x;
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
let selected = pane
|
let selected = pane
|
||||||
.active
|
.active
|
||||||
@@ -1016,9 +1041,9 @@ fn render_pane(
|
|||||||
move |_, _| {
|
move |_, _| {
|
||||||
tabs.iter()
|
tabs.iter()
|
||||||
.map(|tab| {
|
.map(|tab| {
|
||||||
let mut entry = Tab::new(tab.title.clone());
|
let mut entry = Tab::new(tab.panel.clone(), tab.title.clone());
|
||||||
if let Some(role) = tab.icon {
|
if let Some(icon) = tab.icon.clone() {
|
||||||
entry = entry.icon(role);
|
entry = entry.icon(icon);
|
||||||
}
|
}
|
||||||
entry.closeable(true).wrap({
|
entry.closeable(true).wrap({
|
||||||
let panel = tab.panel.clone();
|
let panel = tab.panel.clone();
|
||||||
@@ -1065,21 +1090,17 @@ fn render_pane(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let bar_id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key));
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(SharedString::from(format!(
|
.id(bar_id.clone())
|
||||||
"{dock_id}-pane-{}-bar",
|
.debug_selector(move || bar_id.to_string())
|
||||||
pane.key
|
|
||||||
)))
|
|
||||||
.debug_selector({
|
|
||||||
let id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key));
|
|
||||||
move || id.to_string()
|
|
||||||
})
|
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(titlebar.height)
|
.h(titlebar.height)
|
||||||
.items_center()
|
.items_center()
|
||||||
.pl(if titlebar.traffic_light_pad {
|
.pl(if titlebar.traffic_light_pad {
|
||||||
px(TRAFFIC_LIGHT_PAD)
|
TRAFFIC_LIGHT_PAD
|
||||||
} else {
|
} else {
|
||||||
padding_x
|
padding_x
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
|
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
|
||||||
fill, point, px,
|
fill, point, px,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::{ActiveTheme as _, Size};
|
use gpui_native_theme::{ActiveTheme as _, RESIZE_KEYBOARD_STEP, Size};
|
||||||
use gpui_native_utils::{focus_handle, focus_ring};
|
use gpui_native_utils::{focus_handle, focus_ring};
|
||||||
|
|
||||||
/// How far one keyboard arrow step moves the seam.
|
/// How far one keyboard arrow step moves the seam.
|
||||||
const KEYBOARD_STEP: f32 = 32.0;
|
const KEYBOARD_STEP: Pixels = RESIZE_KEYBOARD_STEP;
|
||||||
|
|
||||||
type ResizeHandler = Rc<dyn Fn(Pixels, &mut Window, &mut App)>;
|
type ResizeHandler = Rc<dyn Fn(Pixels, &mut Window, &mut App)>;
|
||||||
type ResetHandler = Rc<dyn Fn(&mut Window, &mut App)>;
|
type ResetHandler = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||||
@@ -180,10 +180,10 @@ impl RenderOnce for ResizeHandle {
|
|||||||
let on_resize = self.on_resize.clone();
|
let on_resize = self.on_resize.clone();
|
||||||
move |event: &KeyDownEvent, window, cx| {
|
move |event: &KeyDownEvent, window, cx| {
|
||||||
let delta = match (axis, event.keystroke.key.as_str()) {
|
let delta = match (axis, event.keystroke.key.as_str()) {
|
||||||
(Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)),
|
(Axis::Horizontal, "left") => Some(-KEYBOARD_STEP),
|
||||||
(Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)),
|
(Axis::Horizontal, "right") => Some(KEYBOARD_STEP),
|
||||||
(Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)),
|
(Axis::Vertical, "up") => Some(-KEYBOARD_STEP),
|
||||||
(Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)),
|
(Axis::Vertical, "down") => Some(KEYBOARD_STEP),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(delta) = delta {
|
if let Some(delta) = delta {
|
||||||
@@ -255,6 +255,14 @@ impl RenderOnce for ResizeHandle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if event.pressed_button != Some(MouseButton::Left) {
|
||||||
|
drag.update(cx, |drag, cx| {
|
||||||
|
drag.dragging = false;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let delta = match axis {
|
let delta = match axis {
|
||||||
Axis::Horizontal => event.position.x - last.x,
|
Axis::Horizontal => event.position.x - last.x,
|
||||||
Axis::Vertical => event.position.y - last.y,
|
Axis::Vertical => event.position.y - last.y,
|
||||||
|
|||||||
+12
-32
@@ -4,8 +4,7 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
|
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
|
||||||
IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, Styled as _, Window, div,
|
IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px,
|
||||||
px,
|
|
||||||
};
|
};
|
||||||
use gpui_native_core::button::{Button, ButtonVariant};
|
use gpui_native_core::button::{Button, ButtonVariant};
|
||||||
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
||||||
@@ -108,20 +107,6 @@ impl Side {
|
|||||||
Side::Right => "right",
|
Side::Right => "right",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_icon(self) -> IconRole {
|
|
||||||
match self {
|
|
||||||
Side::Left => IconRole::PanelLeft,
|
|
||||||
Side::Right => IconRole::PanelRight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_label(self) -> SharedString {
|
|
||||||
match self {
|
|
||||||
Side::Left => SharedString::from("Hide left sidebar"),
|
|
||||||
Side::Right => SharedString::from("Hide right sidebar"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The application shell.
|
/// The application shell.
|
||||||
@@ -204,6 +189,7 @@ impl RenderOnce for Shell {
|
|||||||
let width = state.read(cx).left_sidebar_width();
|
let width = state.read(cx).left_sidebar_width();
|
||||||
layout
|
layout
|
||||||
.child(sidebar_region(
|
.child(sidebar_region(
|
||||||
|
(id.clone(), "left"),
|
||||||
width,
|
width,
|
||||||
left.take()
|
left.take()
|
||||||
.map(|sidebar| sidebar.into_any_element())
|
.map(|sidebar| sidebar.into_any_element())
|
||||||
@@ -242,6 +228,7 @@ impl RenderOnce for Shell {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.child(sidebar_region(
|
.child(sidebar_region(
|
||||||
|
(id.clone(), "right"),
|
||||||
width,
|
width,
|
||||||
right.take().unwrap_or_else(|| div().into_any_element()),
|
right.take().unwrap_or_else(|| div().into_any_element()),
|
||||||
Side::Right,
|
Side::Right,
|
||||||
@@ -262,6 +249,7 @@ impl RenderOnce for Shell {
|
|||||||
/// One sidebar: a header strip with the hide toggle over the app's content.
|
/// One sidebar: a header strip with the hide toggle over the app's content.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn sidebar_region(
|
fn sidebar_region(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
width: Pixels,
|
width: Pixels,
|
||||||
content: AnyElement,
|
content: AnyElement,
|
||||||
side: Side,
|
side: Side,
|
||||||
@@ -272,6 +260,7 @@ fn sidebar_region(
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let id = id.into();
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let background = theme.colors.background;
|
let background = theme.colors.background;
|
||||||
let strip_background = match (side, material) {
|
let strip_background = match (side, material) {
|
||||||
@@ -280,7 +269,7 @@ fn sidebar_region(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let strip = h_flex()
|
let strip = h_flex()
|
||||||
.id(SharedString::from(format!("sidebar-strip-{}", side.name())))
|
.id((id.clone(), "strip"))
|
||||||
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.h(strip_height)
|
.h(strip_height)
|
||||||
@@ -288,32 +277,23 @@ fn sidebar_region(
|
|||||||
.items_center()
|
.items_center()
|
||||||
.map(|strip| match side {
|
.map(|strip| match side {
|
||||||
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
|
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
|
||||||
strip.pl(px(TRAFFIC_LIGHT_PAD))
|
strip.pl(TRAFFIC_LIGHT_PAD)
|
||||||
}),
|
}),
|
||||||
Side::Right => strip,
|
Side::Right => strip,
|
||||||
})
|
})
|
||||||
.px(padding_x)
|
.px(padding_x)
|
||||||
.child(titlebar_strip(
|
.child(titlebar_strip(
|
||||||
div()
|
div().id((id.clone(), "strip-drag")).flex_1().self_stretch(),
|
||||||
.id(SharedString::from(format!(
|
|
||||||
"sidebar-strip-drag-{}",
|
|
||||||
side.name()
|
|
||||||
)))
|
|
||||||
.flex_1()
|
|
||||||
.self_stretch(),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.map(|strip| match side {
|
.map(|strip| match side {
|
||||||
Side::Left => strip.child(
|
Side::Left => strip.child(
|
||||||
Button::new(SharedString::from(format!(
|
Button::new((id, "sidebar-toggle"))
|
||||||
"sidebar-toggle-{}",
|
.icon(IconRole::PanelLeft)
|
||||||
side.name()
|
|
||||||
)))
|
|
||||||
.icon(side.toggle_icon())
|
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.small()
|
.small()
|
||||||
.accessibility_label(side.toggle_label())
|
.accessibility_label("Hide left sidebar")
|
||||||
.on_click({
|
.on_click({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move |_, _, cx| {
|
move |_, _, cx| {
|
||||||
@@ -321,7 +301,7 @@ fn sidebar_region(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Side::Right => strip.child(WindowControls::new("sidebar-window-controls")),
|
Side::Right => strip.child(WindowControls::new((id, "window-controls"))),
|
||||||
});
|
});
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -23,13 +23,9 @@ use gpui::Decorations;
|
|||||||
use gpui::{SharedString, px, rgba};
|
use gpui::{SharedString, px, rgba};
|
||||||
|
|
||||||
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
||||||
pub const TRAFFIC_LIGHT_PAD: f32 = 78.0;
|
pub use gpui_native_theme::TRAFFIC_LIGHT_PAD;
|
||||||
/// The width of a Windows caption button.
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const CAPTION_WIDTH: f32 = 36.0;
|
use gpui_native_theme::{WINDOWS_CAPTION_WIDTH as CAPTION_WIDTH, WINDOWS_CLOSE_HOVER_HEX};
|
||||||
/// Windows paints its caption close hover red in every theme.
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
const WINDOWS_CLOSE_HOVER: (f32, f32, f32) = (232.0 / 255.0, 17.0 / 255.0, 32.0 / 255.0);
|
|
||||||
|
|
||||||
/// The window controls for the trailing end of a titlebar strip.
|
/// The window controls for the trailing end of a titlebar strip.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
@@ -150,7 +146,7 @@ impl RenderOnce for WindowControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Double-click on a titlebar strip maximizes where the platform does not do it.
|
/// Double-click on a titlebar strip maximizes where the platform does not do it.
|
||||||
pub fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static {
|
fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static {
|
||||||
move |event, window, _cx| {
|
move |event, window, _cx| {
|
||||||
if event.click_count() != 2 {
|
if event.click_count() != 2 {
|
||||||
return;
|
return;
|
||||||
@@ -210,16 +206,9 @@ fn caption_button(
|
|||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let glyph_size = theme.sizes.small.caption_size;
|
let glyph_size = theme.sizes.small.caption_size;
|
||||||
let (close_r, close_g, close_b) = WINDOWS_CLOSE_HOVER;
|
|
||||||
|
|
||||||
let close_hover = rgba((
|
|
||||||
(close_r * 255.0) as u8,
|
|
||||||
(close_g * 255.0) as u8,
|
|
||||||
(close_b * 255.0) as u8,
|
|
||||||
0xff,
|
|
||||||
));
|
|
||||||
let hover = if close {
|
let hover = if close {
|
||||||
close_hover
|
rgba(WINDOWS_CLOSE_HOVER_HEX)
|
||||||
} else {
|
} else {
|
||||||
theme.colors.muted_hover
|
theme.colors.muted_hover
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,12 +7,8 @@ use gpui::{
|
|||||||
IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _,
|
IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _,
|
||||||
Tiling, Window, canvas, div, point, px, size,
|
Tiling, Window, canvas, div, point, px, size,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, WINDOW_RESIZE_INSET};
|
||||||
|
|
||||||
/// The width of the band inside the window's edge that resizes it.
|
|
||||||
const RESIZE_INSET: f32 = 4.0;
|
|
||||||
/// The hairline around the content.
|
|
||||||
const BORDER_WIDTH: f32 = 1.0;
|
|
||||||
/// How much of the shadow's strength survives losing window focus.
|
/// How much of the shadow's strength survives losing window focus.
|
||||||
const INACTIVE_SHADOW_OPACITY: f32 = 0.7;
|
const INACTIVE_SHADOW_OPACITY: f32 = 0.7;
|
||||||
|
|
||||||
@@ -28,7 +24,7 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
|
|||||||
tiling
|
tiling
|
||||||
};
|
};
|
||||||
|
|
||||||
let inset = px(RESIZE_INSET);
|
let inset = WINDOW_RESIZE_INSET;
|
||||||
let border = cx.theme().colors.border;
|
let border = cx.theme().colors.border;
|
||||||
let radius = cx.theme().radius.overlay;
|
let radius = cx.theme().radius.overlay;
|
||||||
|
|
||||||
@@ -75,10 +71,10 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.border_color(border)
|
.border_color(border)
|
||||||
.when(!tiling.top, |surface| surface.border_t(px(BORDER_WIDTH)))
|
.when(!tiling.top, |surface| surface.border_t(BORDER_WIDTH))
|
||||||
.when(!tiling.bottom, |surface| surface.border_b(px(BORDER_WIDTH)))
|
.when(!tiling.bottom, |surface| surface.border_b(BORDER_WIDTH))
|
||||||
.when(!tiling.left, |surface| surface.border_l(px(BORDER_WIDTH)))
|
.when(!tiling.left, |surface| surface.border_l(BORDER_WIDTH))
|
||||||
.when(!tiling.right, |surface| surface.border_r(px(BORDER_WIDTH)))
|
.when(!tiling.right, |surface| surface.border_r(BORDER_WIDTH))
|
||||||
.when(!(tiling.top || tiling.left), |surface| {
|
.when(!(tiling.top || tiling.left), |surface| {
|
||||||
surface.rounded_tl(radius)
|
surface.rounded_tl(radius)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ fn drag(visual: &mut VisualTestContext, from: gpui::Point<Pixels>, to: gpui::Poi
|
|||||||
|
|
||||||
fn register(dock: &Entity<DockState>, tags: &[&'static str], cx: &mut TestAppContext) {
|
fn register(dock: &Entity<DockState>, tags: &[&'static str], cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
dock.register_panel(descriptor(tag), cx);
|
dock.register_panel(descriptor(tag));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -492,9 +492,9 @@ fn activating_a_panel_with_a_right_view_opens_the_sidebar() {
|
|||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| {
|
||||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
dock.register_panel(descriptor_with_right_sidebar("overview"));
|
||||||
dock.register_panel(descriptor("inbox"), cx);
|
dock.register_panel(descriptor("inbox"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -519,9 +519,9 @@ fn closing_the_displayed_tab_falls_back_to_another_right_view() {
|
|||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
let (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| {
|
||||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
dock.register_panel(descriptor_with_right_sidebar("overview"));
|
||||||
dock.register_panel(descriptor_with_right_sidebar("explore"), cx);
|
dock.register_panel(descriptor_with_right_sidebar("explore"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
open(&mut visual, &dock, "overview");
|
open(&mut visual, &dock, "overview");
|
||||||
|
|||||||
@@ -151,6 +151,28 @@ fn moves_without_a_drag_report_nothing() {
|
|||||||
assert!(deltas.borrow().is_empty());
|
assert!(deltas.borrow().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The button may be released outside the window, where no MouseUp is
|
||||||
|
/// delivered — the drag must end when a move reports no pressed button.
|
||||||
|
#[test]
|
||||||
|
fn releasing_outside_the_window_ends_the_drag() {
|
||||||
|
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
||||||
|
let start = handle_center(&mut visual);
|
||||||
|
|
||||||
|
visual.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
|
||||||
|
visual.simulate_event(MouseMoveEvent {
|
||||||
|
position: point(start.x + px(30.), start.y),
|
||||||
|
pressed_button: None,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
});
|
||||||
|
visual.simulate_event(MouseMoveEvent {
|
||||||
|
position: point(start.x + px(60.), start.y),
|
||||||
|
pressed_button: None,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(deltas.borrow().is_empty(), "the drag outlived its button");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arrow_keys_move_the_seam_when_focused() {
|
fn arrow_keys_move_the_seam_when_focused() {
|
||||||
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
let (_cx, deltas, _resets, mut visual) = handle_window(Axis::Horizontal, false);
|
||||||
|
|||||||
@@ -74,9 +74,7 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() {
|
|||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview")));
|
||||||
dock.register_panel(descriptor("overview"), cx)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
open(&mut visual, &dock, "overview");
|
open(&mut visual, &dock, "overview");
|
||||||
|
|
||||||
@@ -87,9 +85,7 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() {
|
|||||||
let (dock, mut visual) = dock_window(false, true, &mut cx);
|
let (dock, mut visual) = dock_window(false, true, &mut cx);
|
||||||
let _ = &dock;
|
let _ = &dock;
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview")));
|
||||||
dock.register_panel(descriptor("overview"), cx)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
open(&mut visual, &dock, "overview");
|
open(&mut visual, &dock, "overview");
|
||||||
|
|
||||||
@@ -101,11 +97,11 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() {
|
|||||||
// The bare bar keeps the standard leading padding; the padded bar clears
|
// The bare bar keeps the standard leading padding; the padded bar clears
|
||||||
// the traffic lights.
|
// the traffic lights.
|
||||||
assert!(
|
assert!(
|
||||||
bare_inset < px(TRAFFIC_LIGHT_PAD),
|
bare_inset < TRAFFIC_LIGHT_PAD,
|
||||||
"the bare bar must not reserve the traffic-light pad (bare {bare_inset:?})"
|
"the bare bar must not reserve the traffic-light pad (bare {bare_inset:?})"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
padded_inset >= px(TRAFFIC_LIGHT_PAD) - px(1.),
|
padded_inset >= TRAFFIC_LIGHT_PAD - px(1.),
|
||||||
"the pad must clear the traffic lights (bare {bare_inset:?}, padded {padded_inset:?})"
|
"the pad must clear the traffic lights (bare {bare_inset:?}, padded {padded_inset:?})"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -115,9 +111,7 @@ fn window_controls_render_only_when_the_flag_is_set() {
|
|||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview")));
|
||||||
dock.register_panel(descriptor("overview"), cx)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
open(&mut visual, &dock, "overview");
|
open(&mut visual, &dock, "overview");
|
||||||
|
|
||||||
@@ -138,9 +132,7 @@ fn the_flag_hosts_the_platform_controls_in_the_bar() {
|
|||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
let (dock, mut visual) = dock_window(true, false, &mut cx);
|
let (dock, mut visual) = dock_window(true, false, &mut cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
dock.update(cx, |dock, cx| {
|
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("overview")));
|
||||||
dock.register_panel(descriptor("overview"), cx)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
open(&mut visual, &dock, "overview");
|
open(&mut visual, &dock, "overview");
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -23,9 +23,39 @@ pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemThe
|
|||||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px};
|
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, WindowBackgroundAppearance, px};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// The hairline width of the control border every button and field paints;
|
/// The hairline width of the control border every button and field paints.
|
||||||
/// sized outside the stated metrics so control sizes match across platforms.
|
|
||||||
pub const BORDER_WIDTH: Pixels = px(1.0);
|
pub const BORDER_WIDTH: Pixels = px(1.0);
|
||||||
|
/// The tallest an anchored popup's list may grow.
|
||||||
|
pub const POPUP_MAX_HEIGHT: Pixels = px(320.0);
|
||||||
|
/// The widest a tooltip may grow.
|
||||||
|
pub const TOOLTIP_MAX_WIDTH: Pixels = px(360.0);
|
||||||
|
/// How far a tooltip sits from the pointer.
|
||||||
|
pub const TOOLTIP_OFFSET: Pixels = px(1.0);
|
||||||
|
/// The caret's thickness.
|
||||||
|
pub const CARET_WIDTH: Pixels = px(1.5);
|
||||||
|
/// The IME preedit underline's thickness.
|
||||||
|
pub const UNDERLINE_WIDTH: Pixels = px(1.0);
|
||||||
|
/// How far one wheel line scrolls, as a multiple of the line height.
|
||||||
|
pub const WHEEL_LINE_HEIGHT: f32 = 20.0;
|
||||||
|
/// How far one keyboard arrow step moves a resize seam.
|
||||||
|
pub const RESIZE_KEYBOARD_STEP: Pixels = px(32.0);
|
||||||
|
/// The inset around a window's edge that starts a resize gesture.
|
||||||
|
pub const WINDOW_RESIZE_INSET: Pixels = px(4.0);
|
||||||
|
/// The pad macOS reserves for its traffic lights at a strip's leading edge.
|
||||||
|
pub const TRAFFIC_LIGHT_PAD: Pixels = px(78.0);
|
||||||
|
/// The width of a Windows caption button.
|
||||||
|
pub const WINDOWS_CAPTION_WIDTH: Pixels = px(36.0);
|
||||||
|
/// Windows paints its caption close-button hover red in every theme.
|
||||||
|
pub const WINDOWS_CLOSE_HOVER_HEX: u32 = 0xE81120FF;
|
||||||
|
/// The monospace advance estimate as a share of the text size.
|
||||||
|
pub const MONO_ADVANCE_RATIO: f32 = 0.62;
|
||||||
|
/// How much a pixel avatar's tint washes over its background.
|
||||||
|
pub const PIXEL_AVATAR_TINT_ALPHA: f32 = 0.16;
|
||||||
|
/// Pixel avatar cell lightness multipliers.
|
||||||
|
pub const PIXEL_AVATAR_LIGHT_LIFT: f32 = 1.6;
|
||||||
|
pub const PIXEL_AVATAR_LIGHT_CAP: f32 = 0.95;
|
||||||
|
pub const PIXEL_AVATAR_DARK_FACTOR: f32 = 0.45;
|
||||||
|
pub const PIXEL_AVATAR_DARK_FLOOR: f32 = 0.18;
|
||||||
|
|
||||||
/// Light or dark, resolved from the system appearance.
|
/// Light or dark, resolved from the system appearance.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
|||||||
+16
-18
@@ -1,7 +1,8 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, BoxShadow, Div, ElementId, FocusHandle, Global, Hsla, IntoElement, Pixels,
|
App, Bounds, BoxShadow, Div, ElementId, FocusHandle, Global, Hsla, IntoElement, Pixels,
|
||||||
Refineable as _, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, px,
|
Refineable as _, StatefulInteractiveElement, StyleRefinement, Styled, Window, WindowId, div,
|
||||||
|
px,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::ActiveTheme as _;
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -36,39 +37,36 @@ pub fn focus_handle(id: &ElementId, window: &mut Window, cx: &mut App) -> FocusH
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The painted bounds of elements that tooltips anchor to, keyed by element id.
|
/// The painted bounds of elements that tooltips anchor to, per window and element id.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Default)]
|
||||||
struct AnchorBoundsRegistry(HashMap<ElementId, Bounds<Pixels>>);
|
struct AnchorBoundsRegistry(HashMap<(WindowId, ElementId), Bounds<Pixels>>);
|
||||||
|
|
||||||
impl Global for AnchorBoundsRegistry {}
|
impl Global for AnchorBoundsRegistry {}
|
||||||
|
|
||||||
impl AnchorBoundsRegistry {
|
impl AnchorBoundsRegistry {
|
||||||
fn record(&mut self, id: ElementId, bounds: Bounds<Pixels>) {
|
fn record(&mut self, window: WindowId, id: ElementId, bounds: Bounds<Pixels>) {
|
||||||
self.0.insert(id, bounds);
|
self.0.insert((window, id), bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, id: &ElementId) -> Option<Bounds<Pixels>> {
|
fn get(&self, window: WindowId, id: &ElementId) -> Option<Bounds<Pixels>> {
|
||||||
self.0.get(id).copied()
|
self.0.get(&(window, id.clone())).copied()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The last bounds an element painted at.
|
/// The last bounds an element painted at, in the given window.
|
||||||
pub fn anchor_bounds(id: &ElementId, cx: &mut App) -> Option<Bounds<Pixels>> {
|
pub fn anchor_bounds(window: WindowId, id: &ElementId, cx: &App) -> Option<Bounds<Pixels>> {
|
||||||
cx.try_global::<AnchorBoundsRegistry>()
|
cx.try_global::<AnchorBoundsRegistry>()
|
||||||
.and_then(|registry| registry.get(id))
|
.and_then(|registry| registry.get(window, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A canvas prepaint closure recording the element's painted bounds.
|
/// A canvas prepaint closure recording the element's painted bounds.
|
||||||
pub fn record_anchor_bounds(
|
pub fn record_anchor_bounds(
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
) -> impl Fn(Bounds<Pixels>, &mut Window, &mut App) + 'static {
|
) -> impl Fn(Bounds<Pixels>, &mut Window, &mut App) + 'static {
|
||||||
move |bounds, _window, cx| {
|
move |bounds, window, cx| {
|
||||||
let mut registry = cx
|
let window_id = window.window_handle().window_id();
|
||||||
.try_global::<AnchorBoundsRegistry>()
|
cx.default_global::<AnchorBoundsRegistry>()
|
||||||
.cloned()
|
.record(window_id, id.clone(), bounds);
|
||||||
.unwrap_or_default();
|
|
||||||
registry.record(id.clone(), bounds);
|
|
||||||
cx.set_global(registry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ Gaps surfaced by the signed comparison; ordered by signed's usage.
|
|||||||
|
|
||||||
- [x] `IconRole` + bundled Lucide fallback, Segoe Fluent codepoints (Windows),
|
- [x] `IconRole` + bundled Lucide fallback, Segoe Fluent codepoints (Windows),
|
||||||
freedesktop theme walker (Linux/Omarchy)
|
freedesktop theme walker (Linux/Omarchy)
|
||||||
|
- [x] `IconNamed` — app-defined icon sets: implement the trait for an enum of
|
||||||
|
asset paths, render it with `Icon::named`, and hand it to any icon
|
||||||
|
builder (`impl Into<Icon>`) alongside the built-in roles
|
||||||
|
|
||||||
### core (`crates/core`)
|
### core (`crates/core`)
|
||||||
|
|
||||||
|
|||||||
+122
-212
@@ -424,135 +424,83 @@ impl Gallery {
|
|||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
cx,
|
|
||||||
);
|
);
|
||||||
dock.register_panel(
|
dock.register_panel(PanelDescriptor::new("button", "Button", |_, cx| {
|
||||||
PanelDescriptor::new("button", "Button", |_, cx| {
|
|
||||||
cx.new(|_| ButtonDemo { clicks: 0 }).into()
|
cx.new(|_| ButtonDemo { clicks: 0 }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("input", "Input", |_, cx| {
|
||||||
);
|
cx.new(InputDemo::new).into()
|
||||||
dock.register_panel(
|
}));
|
||||||
PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()),
|
dock.register_panel(PanelDescriptor::new("field", "Field", |window, cx| {
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("field", "Field", |window, cx| {
|
|
||||||
cx.new(|cx| FieldDemo::new(window, cx)).into()
|
cx.new(|cx| FieldDemo::new(window, cx)).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new(
|
||||||
);
|
"combobox",
|
||||||
dock.register_panel(
|
"Combobox",
|
||||||
PanelDescriptor::new("combobox", "Combobox", |window, cx| {
|
|window, cx| cx.new(|cx| ComboboxDemo::new(window, cx)).into(),
|
||||||
cx.new(|cx| ComboboxDemo::new(window, cx)).into()
|
));
|
||||||
}),
|
dock.register_panel(PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
|
|
||||||
cx.new(|_| StatusDotDemo).into()
|
cx.new(|_| StatusDotDemo).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("checkbox", "Checkbox", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("checkbox", "Checkbox", |_, cx| {
|
|
||||||
cx.new(|_| CheckboxDemo { checked: false }).into()
|
cx.new(|_| CheckboxDemo { checked: false }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("radio", "Radio", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("radio", "Radio", |_, cx| {
|
|
||||||
cx.new(|_| RadioDemo {
|
cx.new(|_| RadioDemo {
|
||||||
plan: SharedString::from("starter"),
|
plan: SharedString::from("starter"),
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("switch", "Switch", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("switch", "Switch", |_, cx| {
|
|
||||||
cx.new(|_| SwitchDemo { checked: true }).into()
|
cx.new(|_| SwitchDemo { checked: true }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("toggle", "Toggle", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("toggle", "Toggle", |_, cx| {
|
|
||||||
cx.new(|_| ToggleDemo { pressed: false }).into()
|
cx.new(|_| ToggleDemo { pressed: false }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("slider", "Slider", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("slider", "Slider", |_, cx| {
|
|
||||||
cx.new(|_| SliderDemo { volume: 40.0 }).into()
|
cx.new(|_| SliderDemo { volume: 40.0 }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("segmented", "Segmented", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("segmented", "Segmented", |_, cx| {
|
|
||||||
cx.new(|_| SegmentedDemo {
|
cx.new(|_| SegmentedDemo {
|
||||||
filter: ExploreFilter::default(),
|
filter: ExploreFilter::default(),
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("select", "Select", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("select", "Select", |_, cx| {
|
|
||||||
cx.new(|_| SelectDemo {
|
cx.new(|_| SelectDemo {
|
||||||
strategy: Some(Strategy::Curated),
|
strategy: Some(Strategy::Curated),
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("link", "Link", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("link", "Link", |_, cx| {
|
|
||||||
cx.new(|_| LinkDemo { clicks: 0 }).into()
|
cx.new(|_| LinkDemo { clicks: 0 }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("clipboard", "Clipboard", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("clipboard", "Clipboard", |_, cx| {
|
|
||||||
cx.new(|_| ClipboardDemo).into()
|
cx.new(|_| ClipboardDemo).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("badge", "Badge", |_, cx| {
|
||||||
);
|
cx.new(|_| BadgeDemo).into()
|
||||||
dock.register_panel(
|
}));
|
||||||
PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()),
|
dock.register_panel(PanelDescriptor::new("avatar", "Avatar", |_, cx| {
|
||||||
cx,
|
cx.new(|_| AvatarDemo).into()
|
||||||
);
|
}));
|
||||||
dock.register_panel(
|
dock.register_panel(PanelDescriptor::new("progress", "Progress", |_, cx| {
|
||||||
PanelDescriptor::new("avatar", "Avatar", |_, cx| cx.new(|_| AvatarDemo).into()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("progress", "Progress", |_, cx| {
|
|
||||||
cx.new(|_| ProgressDemo).into()
|
cx.new(|_| ProgressDemo).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("spinner", "Spinner", |_, cx| {
|
||||||
);
|
cx.new(|_| SpinnerDemo).into()
|
||||||
dock.register_panel(
|
}));
|
||||||
PanelDescriptor::new("spinner", "Spinner", |_, cx| cx.new(|_| SpinnerDemo).into()),
|
dock.register_panel(PanelDescriptor::new("list", "List", |_, cx| {
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("list", "List", |_, cx| {
|
|
||||||
cx.new(|_| ListDemo { selected: None }).into()
|
cx.new(|_| ListDemo { selected: None }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("tree", "Tree", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("tree", "Tree", |_, cx| {
|
|
||||||
cx.new(|_| TreeDemo {
|
cx.new(|_| TreeDemo {
|
||||||
nodes: TreeDemo::demo(),
|
nodes: TreeDemo::demo(),
|
||||||
selected: None,
|
selected: None,
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("tabs", "Tabs", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("tabs", "Tabs", |_, cx| {
|
|
||||||
cx.new(|_| TabsDemo {
|
cx.new(|_| TabsDemo {
|
||||||
selected: 0,
|
selected: 0,
|
||||||
pill_tabs: vec![
|
pill_tabs: vec![
|
||||||
@@ -563,11 +511,8 @@ impl Gallery {
|
|||||||
pill_selected: 0,
|
pill_selected: 0,
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("alert", "Alert", {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("alert", "Alert", {
|
|
||||||
let alerts = alerts.clone();
|
let alerts = alerts.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
cx.new(|_| AlertDemo {
|
cx.new(|_| AlertDemo {
|
||||||
@@ -576,11 +521,8 @@ impl Gallery {
|
|||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("toast", "Toast", {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("toast", "Toast", {
|
|
||||||
let toasts = toasts.clone();
|
let toasts = toasts.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
cx.new(|_| ToastDemo {
|
cx.new(|_| ToastDemo {
|
||||||
@@ -588,11 +530,8 @@ impl Gallery {
|
|||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("dialog", "Dialog", {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("dialog", "Dialog", {
|
|
||||||
let gallery = gallery.clone();
|
let gallery = gallery.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
cx.new(|_| DialogDemo {
|
cx.new(|_| DialogDemo {
|
||||||
@@ -600,118 +539,89 @@ impl Gallery {
|
|||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new(
|
||||||
);
|
"command-palette",
|
||||||
dock.register_panel(
|
"Command palette",
|
||||||
PanelDescriptor::new("command-palette", "Command palette", {
|
{
|
||||||
let palette = palette.clone();
|
let palette = palette.clone();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx))
|
cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
cx,
|
));
|
||||||
);
|
dock.register_panel(PanelDescriptor::new("menu", "Menu", |_, cx| {
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("menu", "Menu", |_, cx| {
|
|
||||||
cx.new(|_| MenuDemo { open: false }).into()
|
cx.new(|_| MenuDemo { open: false }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new(
|
||||||
);
|
"dropdown-menu",
|
||||||
dock.register_panel(
|
"Dropdown menu",
|
||||||
PanelDescriptor::new("dropdown-menu", "Dropdown menu", |_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| DropdownMenuDemo {
|
cx.new(|_| DropdownMenuDemo {
|
||||||
open_menu: None,
|
open_menu: None,
|
||||||
status: None,
|
status: None,
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
},
|
||||||
cx,
|
));
|
||||||
);
|
dock.register_panel(PanelDescriptor::new(
|
||||||
dock.register_panel(
|
"context-menu",
|
||||||
PanelDescriptor::new("context-menu", "Context menu", |_, cx| {
|
"Context menu",
|
||||||
|
|_, cx| {
|
||||||
cx.new(|_| ContextMenuDemo {
|
cx.new(|_| ContextMenuDemo {
|
||||||
action: SharedString::from("Right-click the card"),
|
action: SharedString::from("Right-click the card"),
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}),
|
},
|
||||||
cx,
|
));
|
||||||
);
|
dock.register_panel(PanelDescriptor::new("popover", "Popover", |_, cx| {
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("popover", "Popover", |_, cx| {
|
|
||||||
cx.new(|_| PopoverDemo { open: false }).into()
|
cx.new(|_| PopoverDemo { open: false }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("tooltip", "Tooltip", |_, cx| {
|
||||||
);
|
cx.new(|_| TooltipDemo).into()
|
||||||
dock.register_panel(
|
}));
|
||||||
PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()),
|
dock.register_panel(PanelDescriptor::new(
|
||||||
cx,
|
"empty-state",
|
||||||
);
|
"Empty state",
|
||||||
dock.register_panel(
|
|_, cx| cx.new(|_| EmptyStateDemo).into(),
|
||||||
PanelDescriptor::new("empty-state", "Empty state", |_, cx| {
|
));
|
||||||
cx.new(|_| EmptyStateDemo).into()
|
dock.register_panel(PanelDescriptor::new(
|
||||||
}),
|
"scroll-area",
|
||||||
cx,
|
"Scroll area",
|
||||||
);
|
|_, cx| cx.new(|_| ScrollAreaDemo).into(),
|
||||||
dock.register_panel(
|
));
|
||||||
PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| {
|
dock.register_panel(PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| {
|
||||||
cx.new(|_| ScrollAreaDemo).into()
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| {
|
|
||||||
cx.new(|_| ScrollbarDemo::new()).into()
|
cx.new(|_| ScrollbarDemo::new()).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("code-view", "Code view", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("code-view", "Code view", |_, cx| {
|
|
||||||
cx.new(CodeViewDemo::new).into()
|
cx.new(CodeViewDemo::new).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("diff-view", "Diff view", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("diff-view", "Diff view", |_, cx| {
|
|
||||||
cx.new(DiffViewDemo::new).into()
|
cx.new(DiffViewDemo::new).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new(
|
||||||
);
|
"markdown-view",
|
||||||
dock.register_panel(
|
"Markdown view",
|
||||||
PanelDescriptor::new("markdown-view", "Markdown view", |_, cx| {
|
|_, cx| cx.new(MarkdownViewDemo::new).into(),
|
||||||
cx.new(MarkdownViewDemo::new).into()
|
));
|
||||||
}),
|
dock.register_panel(PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
|
|
||||||
cx.new(|_| SidebarDemo { selected: None }).into()
|
cx.new(|_| SidebarDemo { selected: None }).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new(
|
||||||
);
|
"resize-handle",
|
||||||
dock.register_panel(
|
"Resize handle",
|
||||||
PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| {
|
|_, cx| cx.new(|_| ResizeDemo { width: px(200.) }).into(),
|
||||||
cx.new(|_| ResizeDemo { width: px(200.) }).into()
|
));
|
||||||
}),
|
dock.register_panel(PanelDescriptor::new("resizable", "Resizable", |_, cx| {
|
||||||
cx,
|
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("resizable", "Resizable", |_, cx| {
|
|
||||||
cx.new(ResizableDemo::new).into()
|
cx.new(ResizableDemo::new).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("separator", "Separator", |_, cx| {
|
||||||
);
|
|
||||||
dock.register_panel(
|
|
||||||
PanelDescriptor::new("separator", "Separator", |_, cx| {
|
|
||||||
cx.new(|_| SeparatorDemo).into()
|
cx.new(|_| SeparatorDemo).into()
|
||||||
}),
|
}));
|
||||||
cx,
|
dock.register_panel(PanelDescriptor::new("theme", "Theme", |_, cx| {
|
||||||
);
|
cx.new(|_| ThemeDemo).into()
|
||||||
dock.register_panel(
|
}));
|
||||||
PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2309,10 +2219,10 @@ impl Render for TabsDemo {
|
|||||||
})
|
})
|
||||||
.tabs(|_, _| {
|
.tabs(|_, _| {
|
||||||
vec![
|
vec![
|
||||||
Tab::new("Overview"),
|
Tab::new("overview", "Overview"),
|
||||||
Tab::new("Details"),
|
Tab::new("details", "Details"),
|
||||||
Tab::new("Settings"),
|
Tab::new("settings", "Settings"),
|
||||||
Tab::new("Locked").disabled(true),
|
Tab::new("locked", "Locked").disabled(true),
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
@@ -2356,7 +2266,7 @@ impl Render for TabsDemo {
|
|||||||
labels
|
labels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|label| {
|
.map(|label| {
|
||||||
Tab::new(label.clone())
|
Tab::new(label.clone(), label.clone())
|
||||||
.closeable(true)
|
.closeable(true)
|
||||||
.busy(label == "Syncing")
|
.busy(label == "Syncing")
|
||||||
})
|
})
|
||||||
@@ -3228,7 +3138,7 @@ impl Render for ScrollbarDemo {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.uniform.clone()).visibility(visibility)),
|
.child(Scrollbar::vertical("uniform", self.uniform.clone()).visibility(visibility)),
|
||||||
))
|
))
|
||||||
.child(demo_section(
|
.child(demo_section(
|
||||||
"list",
|
"list",
|
||||||
@@ -3254,7 +3164,7 @@ impl Render for ScrollbarDemo {
|
|||||||
.h_full()
|
.h_full()
|
||||||
.w_full(),
|
.w_full(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(self.list.clone()).visibility(visibility)),
|
.child(Scrollbar::vertical("list", self.list.clone()).visibility(visibility)),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
Reference in New Issue
Block a user