Move layout metrics into theme and apply audit cleanups
This commit is contained in:
@@ -260,11 +260,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>) {
|
||||
if !self.entries.is_empty() {
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
if self.entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = std::mem::take(&mut self.entries);
|
||||
cx.notify();
|
||||
|
||||
for entry in entries {
|
||||
if let Some(on_dismiss) = entry.on_dismiss {
|
||||
on_dismiss(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-18
@@ -4,7 +4,10 @@ use gpui::{
|
||||
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
||||
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};
|
||||
|
||||
@@ -108,12 +111,10 @@ impl RenderOnce for Avatar {
|
||||
.refine_style(&self.style);
|
||||
|
||||
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 {
|
||||
seed: fnv1a(self.id.as_bytes()),
|
||||
side,
|
||||
radius,
|
||||
style: self.style,
|
||||
}),
|
||||
}
|
||||
.into_any_element()
|
||||
@@ -125,14 +126,6 @@ impl RenderOnce for Avatar {
|
||||
struct PixelAvatar {
|
||||
seed: u64,
|
||||
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 {
|
||||
@@ -147,13 +140,13 @@ impl RenderOnce for PixelAvatar {
|
||||
|
||||
let shade = if theme.variant == ThemeVariant::Dark {
|
||||
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)
|
||||
}
|
||||
.to_hsla()
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
.to_hsla()
|
||||
@@ -182,11 +175,11 @@ impl RenderOnce for PixelAvatar {
|
||||
.grid_rows(GRID_SIZE as u16)
|
||||
.size(self.side)
|
||||
.flex_none()
|
||||
.rounded(self.radius)
|
||||
.overflow_hidden()
|
||||
.bg(gpui::Hsla { a: 0.16, ..main })
|
||||
.bg(gpui::Hsla {
|
||||
a: PIXEL_AVATAR_TINT_ALPHA,
|
||||
..main
|
||||
})
|
||||
.children(cells)
|
||||
.refine_style(&self.style)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyView, App, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas,
|
||||
hsla, relative,
|
||||
relative, transparent_black,
|
||||
};
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||
@@ -199,9 +199,9 @@ impl RenderOnce for Button {
|
||||
match background {
|
||||
Some(fill) => (fill, hover, pressed),
|
||||
None => (
|
||||
hsla(0.0, 0.0, 0.0, 0.0),
|
||||
hsla(0.0, 0.0, 0.0, 0.0),
|
||||
hsla(0.0, 0.0, 0.0, 0.0),
|
||||
transparent_black(),
|
||||
transparent_black(),
|
||||
transparent_black(),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
+13
-25
@@ -10,13 +10,13 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, Bounds, Context, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
|
||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||
Subscription, Task, Window, actions, anchored, canvas, deferred, div, point, px,
|
||||
App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription, Task,
|
||||
Window, actions, anchored, canvas, deferred, div, point, px,
|
||||
};
|
||||
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::input::{Input, InputState, InputStateEvent};
|
||||
@@ -177,9 +177,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Emitted when the combobox's value changes.
|
||||
pub struct ComboboxChanged;
|
||||
|
||||
/// The stateful combobox.
|
||||
pub struct ComboboxState<T>
|
||||
where
|
||||
@@ -209,8 +206,6 @@ where
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl<T> EventEmitter<ComboboxChanged> for ComboboxState<T> where T: Clone + PartialEq + 'static {}
|
||||
|
||||
impl<T> Sizable for ComboboxState<T>
|
||||
where
|
||||
T: Clone + PartialEq + 'static,
|
||||
@@ -316,8 +311,8 @@ where
|
||||
.map(|(_, label)| label.clone())
|
||||
}
|
||||
|
||||
/// Recomputes `visible` for `query` and hands the survivors' labels back.
|
||||
fn refilter(&mut self, query: &str) -> Vec<SharedString> {
|
||||
/// Recomputes `visible` for `query` and clamps the highlight onto it.
|
||||
fn refilter(&mut self, query: &str) {
|
||||
let filter = self.filter.clone();
|
||||
self.visible = self
|
||||
.options
|
||||
@@ -326,10 +321,7 @@ where
|
||||
.filter(|(_, (value, label))| filter(query, value, label))
|
||||
.map(|(index, _)| index)
|
||||
.collect();
|
||||
self.visible
|
||||
.iter()
|
||||
.map(|index| self.options[*index].1.clone())
|
||||
.collect()
|
||||
self.highlight = self.highlight.filter(|row| *row < self.visible.len());
|
||||
}
|
||||
|
||||
fn query_text(&self, cx: &Context<Self>) -> String {
|
||||
@@ -349,7 +341,6 @@ where
|
||||
});
|
||||
self.tasks.push(task);
|
||||
}
|
||||
cx.emit(ComboboxChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -375,7 +366,7 @@ where
|
||||
fn text_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let query = self.query_text(cx);
|
||||
self.refilter(&query);
|
||||
self.highlight = (!self.visible.is_empty()).then_some(0usize);
|
||||
self.highlight = Some(0);
|
||||
self.set_value(None, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -386,7 +377,7 @@ where
|
||||
return;
|
||||
}
|
||||
self.refilter("");
|
||||
self.highlight = (!self.visible.is_empty()).then_some(0usize);
|
||||
self.highlight = Some(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -427,10 +418,7 @@ where
|
||||
if self.visible.is_empty() {
|
||||
return;
|
||||
}
|
||||
let current = self
|
||||
.highlight
|
||||
.and_then(|row| self.visible.get(row).copied())
|
||||
.and_then(|option_ix| self.visible.iter().position(|&row| row == option_ix));
|
||||
let current = self.highlight.filter(|row| *row < self.visible.len());
|
||||
let next = match current {
|
||||
Some(row) => {
|
||||
let row = (row as i32 + delta).rem_euclid(self.visible.len() as i32);
|
||||
@@ -516,7 +504,7 @@ where
|
||||
let query = state.query.read(cx).value().to_string();
|
||||
state.refilter(&query);
|
||||
}
|
||||
state.highlight = (!state.visible.is_empty()).then_some(0usize);
|
||||
state.highlight = Some(0);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
@@ -667,7 +655,7 @@ where
|
||||
ScrollArea::new((id.clone(), "rows"))
|
||||
.handle(scroll.read(cx).clone())
|
||||
.axes(crate::scroll_area::ScrollAxes::Vertical)
|
||||
.max_h(px(320.))
|
||||
.max_h(POPUP_MAX_HEIGHT)
|
||||
.children(rows)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
@@ -7,10 +7,10 @@ use gpui::{
|
||||
AnyElement, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement as _, Render, Role, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
|
||||
div, px, relative,
|
||||
div, relative,
|
||||
};
|
||||
use gpui_native_icons::{IconRole, 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::input::{Input, InputState, InputStateEvent};
|
||||
@@ -333,7 +333,7 @@ impl Render for CommandPaletteState {
|
||||
let row_ix = row;
|
||||
|
||||
h_flex()
|
||||
.id(("command", row))
|
||||
.id((ElementId::from(command.id.clone()), "command"))
|
||||
.debug_selector({
|
||||
let id = id.clone();
|
||||
move || format!("{id}-command-{row}")
|
||||
@@ -401,7 +401,7 @@ impl Render for CommandPaletteState {
|
||||
ScrollArea::new((id.clone(), "rows"))
|
||||
.handle(scroll.read(cx).clone())
|
||||
.axes(ScrollAxes::Vertical)
|
||||
.max_h(px(320.))
|
||||
.max_h(POPUP_MAX_HEIGHT)
|
||||
.children(rows)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
+11
-30
@@ -1,12 +1,10 @@
|
||||
use std::cell::RefCell;
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, App, Bounds, ElementId, FontWeight,
|
||||
InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
|
||||
ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _,
|
||||
Styled as _, Window, canvas, div, ease_out_quint, relative,
|
||||
Animation, AnimationExt as _, AnyElement, App, ElementId, FontWeight, InteractiveElement as _,
|
||||
IntoElement, KeyDownEvent, MouseButton, ParentElement as _, RenderOnce, Role, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, Window, canvas, div, ease_out_quint, relative,
|
||||
};
|
||||
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)>;
|
||||
type ContentFn = Box<dyn Fn(&mut Window, &mut App) -> 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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Dialog {
|
||||
@@ -165,7 +160,6 @@ impl RenderOnce for Dialog {
|
||||
|
||||
let content = self.content.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()
|
||||
.id("dialog-panel")
|
||||
@@ -211,19 +205,13 @@ impl RenderOnce for Dialog {
|
||||
panel.child(div().flex().justify_end().gap_2().children(actions))
|
||||
})
|
||||
.child(
|
||||
canvas(
|
||||
{
|
||||
let cell = panel_bounds.clone();
|
||||
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
|
||||
},
|
||||
{
|
||||
move |_, _, window, cx| {
|
||||
if !focus.contains_focused(window, cx) {
|
||||
window.focus(&focus, cx);
|
||||
}
|
||||
canvas(|_, _, _| {}, {
|
||||
move |_, _, window, cx| {
|
||||
if !focus.contains_focused(window, cx) {
|
||||
window.focus(&focus, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
);
|
||||
@@ -235,15 +223,8 @@ impl RenderOnce for Dialog {
|
||||
.occlude()
|
||||
.when(!alert, |scrim| {
|
||||
scrim.on_mouse_down(MouseButton::Left, {
|
||||
let panel_bounds = panel_bounds.clone();
|
||||
let on_change = on_change.clone();
|
||||
move |event: &MouseDownEvent, _window, cx| {
|
||||
let on_panel = panel_bounds
|
||||
.borrow()
|
||||
.is_some_and(|bounds| bounds.contains(&event.position));
|
||||
if on_panel {
|
||||
return;
|
||||
}
|
||||
move |_, _window, cx| {
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(false, _window, cx);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, ClickEvent, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent,
|
||||
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_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||
@@ -193,7 +193,7 @@ impl RenderOnce for DropdownMenu {
|
||||
let border = if show_border {
|
||||
colors.border
|
||||
} 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();
|
||||
|
||||
+116
-103
@@ -1,4 +1,3 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -16,7 +15,10 @@ use gpui::{
|
||||
WrappedLine, actions, canvas, div, fill, point, px,
|
||||
};
|
||||
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 crate::button::{Button, ButtonVariant};
|
||||
@@ -29,7 +31,7 @@ const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300);
|
||||
/// How often the focused caret blinks.
|
||||
const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
/// 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.
|
||||
const DEFAULT_ROWS: usize = 4;
|
||||
/// Maximum undo entries kept; the oldest drops off the front.
|
||||
@@ -270,7 +272,7 @@ impl InputBindings {
|
||||
}
|
||||
|
||||
/// 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> = [
|
||||
self.escape,
|
||||
self.submit,
|
||||
@@ -491,9 +493,9 @@ pub struct InputState {
|
||||
focused: bool,
|
||||
focus_handle: gpui::FocusHandle,
|
||||
/// 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`.
|
||||
mouse_down_handler: Option<MouseDownHandler>,
|
||||
mouse_down_handler: MouseDownHandler,
|
||||
/// Whether a drag-select is in progress.
|
||||
dragging: bool,
|
||||
/// Patch history. `redo_stack` holds undone entries awaiting redo.
|
||||
@@ -520,9 +522,9 @@ pub struct InputState {
|
||||
/// Bumped on every edit.
|
||||
caret_epoch: usize,
|
||||
/// 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.
|
||||
layout: Rc<RefCell<Option<InputLayout>>>,
|
||||
layout: Option<InputLayout>,
|
||||
}
|
||||
|
||||
impl EventEmitter<InputStateEvent> for InputState {}
|
||||
@@ -536,16 +538,14 @@ impl InputState {
|
||||
marked_range: None,
|
||||
focused: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
key_down_handler: Some(Rc::new(cx.listener(
|
||||
|state, event: &KeyDownEvent, window, cx| {
|
||||
state.handle_key_down(event, window, cx);
|
||||
},
|
||||
))),
|
||||
mouse_down_handler: Some(Rc::new(cx.listener(
|
||||
key_down_handler: Rc::new(cx.listener(|state, event: &KeyDownEvent, window, cx| {
|
||||
state.handle_key_down(event, window, cx);
|
||||
})),
|
||||
mouse_down_handler: Rc::new(cx.listener(
|
||||
|state, event: &MouseDownEvent, window, cx| {
|
||||
state.handle_mouse_down(event, window, cx);
|
||||
},
|
||||
))),
|
||||
)),
|
||||
dragging: false,
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
@@ -559,8 +559,8 @@ impl InputState {
|
||||
scroll_offset: point(Pixels::ZERO, Pixels::ZERO),
|
||||
scroll_to_caret_pending: true,
|
||||
caret_epoch: 0,
|
||||
caret_epoch_started: Some(Instant::now()),
|
||||
layout: Rc::new(RefCell::new(None)),
|
||||
caret_epoch_started: Instant::now(),
|
||||
layout: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,12 +696,7 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which keystroke submits a multiline field.
|
||||
pub fn submit_on(&self) -> SubmitOn {
|
||||
self.submit_on
|
||||
}
|
||||
|
||||
/// Whether the painted line masks the buffer.
|
||||
// Whether the painted line masks the buffer.
|
||||
fn masked(&self) -> bool {
|
||||
self.password && !self.revealed
|
||||
}
|
||||
@@ -711,32 +706,43 @@ impl InputState {
|
||||
self.redo_stack.clear();
|
||||
|
||||
let now = Instant::now();
|
||||
if let Some(last) = self.undo_stack.last_mut()
|
||||
&& now.duration_since(last.timestamp) < self.group_interval
|
||||
{
|
||||
if let Some(last) = self.undo_stack.last_mut() {
|
||||
let last_end = last.range.start + last.new_len;
|
||||
|
||||
// Typing that continues right after the previous edit.
|
||||
if range.start == last_end {
|
||||
last.new_len += new_len;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// A deletion of text that sat right before the previous edit.
|
||||
if range.end == last.range.start && new_len == 0 {
|
||||
last.range.start = range.start;
|
||||
last.old_text = old_text + &last.old_text;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
if now.duration_since(last.timestamp) < self.group_interval {
|
||||
// Typing that continues right after the previous edit.
|
||||
if range.start == last_end {
|
||||
last.new_len += new_len;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// An edit fully inside the previous edit's replacement.
|
||||
if range.start >= last.range.start && range.end <= last_end {
|
||||
last.new_len += new_len;
|
||||
last.new_len -= range.len();
|
||||
last.timestamp = now;
|
||||
return;
|
||||
// A deletion of text that sat right before the previous edit.
|
||||
if range.end == last.range.start && new_len == 0 {
|
||||
last.range.start = range.start;
|
||||
last.old_text = old_text + &last.old_text;
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// An edit fully inside the previous edit's replacement.
|
||||
if range.start >= last.range.start && range.end <= last_end {
|
||||
last.new_len += new_len;
|
||||
last.new_len -= range.len();
|
||||
last.timestamp = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,7 +775,7 @@ impl InputState {
|
||||
self.scroll_to_caret_pending = true;
|
||||
self.goal_x = None;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -781,20 +787,38 @@ impl InputState {
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
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);
|
||||
if next_y == self.scroll_offset.y {
|
||||
return self.multiline_scrollable();
|
||||
}
|
||||
self.scroll_offset.y = next_y;
|
||||
} else {
|
||||
self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO);
|
||||
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
|
||||
.layout
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|layout| layout.content_height > layout.visible_height);
|
||||
|
||||
cx.notify();
|
||||
window.notify_text_input_changed(&self.focus_handle);
|
||||
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.
|
||||
fn sync_focus(&mut self, focused: bool, cx: &mut Context<Self>) {
|
||||
if self.focused == focused {
|
||||
@@ -802,11 +826,9 @@ impl InputState {
|
||||
}
|
||||
self.focused = focused;
|
||||
|
||||
// A fresh focus starts a fresh blink epoch, so the caret shows solid
|
||||
// for one interval before blinking resumes.
|
||||
if focused {
|
||||
self.caret_epoch += 1;
|
||||
self.caret_epoch_started = Some(Instant::now());
|
||||
self.caret_epoch_started = Instant::now();
|
||||
}
|
||||
|
||||
cx.emit(if focused {
|
||||
@@ -817,11 +839,8 @@ impl InputState {
|
||||
}
|
||||
|
||||
/// Whether the caret should paint.
|
||||
pub fn caret_visible(&mut self, focused: bool) -> bool {
|
||||
let elapsed = self
|
||||
.caret_epoch_started
|
||||
.map(|started| started.elapsed())
|
||||
.unwrap_or_default();
|
||||
pub fn caret_visible(&self, focused: bool) -> bool {
|
||||
let elapsed = self.caret_epoch_started.elapsed();
|
||||
self.caret_blink_visible_at(focused, elapsed)
|
||||
}
|
||||
|
||||
@@ -901,15 +920,21 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.cursor = to.clamp(0, self.text.len());
|
||||
if !extend {
|
||||
self.anchor = self.cursor;
|
||||
let cursor = to.clamp(0, self.text.len());
|
||||
let anchor = if extend { self.anchor } else { cursor };
|
||||
if cursor == self.cursor && anchor == self.anchor {
|
||||
return;
|
||||
}
|
||||
self.cursor = cursor;
|
||||
self.anchor = anchor;
|
||||
self.notify(window, cx);
|
||||
}
|
||||
|
||||
/// Selects `range`.
|
||||
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.cursor = range.end;
|
||||
self.notify(window, cx);
|
||||
@@ -1085,7 +1110,7 @@ impl InputState {
|
||||
);
|
||||
|
||||
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,
|
||||
rows,
|
||||
bounds,
|
||||
@@ -1107,8 +1132,7 @@ impl InputState {
|
||||
return;
|
||||
}
|
||||
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1138,8 +1162,7 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1155,17 +1178,16 @@ impl InputState {
|
||||
let mut line_top = Pixels::ZERO;
|
||||
for entry in &layout.lines {
|
||||
let line_origin = point(origin.x, origin.y + line_top);
|
||||
entry
|
||||
.line
|
||||
.paint(
|
||||
line_origin,
|
||||
layout.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.ok();
|
||||
if let Err(err) = entry.line.paint(
|
||||
line_origin,
|
||||
layout.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
eprintln!("painting input text failed: {err}");
|
||||
}
|
||||
line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32;
|
||||
}
|
||||
});
|
||||
@@ -1179,8 +1201,7 @@ impl InputState {
|
||||
color: gpui::Hsla,
|
||||
window: &mut Window,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
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 = Bounds::from_corners(
|
||||
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));
|
||||
@@ -1242,8 +1263,7 @@ impl InputState {
|
||||
|
||||
/// The text index at a window point, using the last painted layout.
|
||||
fn index_at(&self, position: gpui::Point<Pixels>) -> usize {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return 0;
|
||||
};
|
||||
real_index(&layout.mask_map, layout.index_at(position))
|
||||
@@ -1256,7 +1276,11 @@ impl InputState {
|
||||
window: &mut Window,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1268,14 +1292,13 @@ impl InputState {
|
||||
color: gpui::Hsla,
|
||||
window: &mut Window,
|
||||
) {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let start = display_index(&layout.mask_map, range.start);
|
||||
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) {
|
||||
let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness;
|
||||
@@ -1329,7 +1352,7 @@ impl InputState {
|
||||
|
||||
if event.click_count >= 3 {
|
||||
// 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);
|
||||
layout.row_range_at(display).map(|range| {
|
||||
real_index(&layout.mask_map, range.start)
|
||||
@@ -1462,15 +1485,16 @@ impl InputState {
|
||||
}
|
||||
|
||||
let target = {
|
||||
let layout = self.layout.borrow();
|
||||
let Some(layout) = layout.as_ref() else {
|
||||
let Some(layout) = self.layout.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let display = display_index(&layout.mask_map, self.cursor);
|
||||
|
||||
let Some((row_ix, caret_x)) = layout.row_for_index(display) else {
|
||||
return;
|
||||
};
|
||||
let target = row_ix as isize + delta;
|
||||
|
||||
let (display, goal_x) = if target < 0 {
|
||||
(0, None)
|
||||
} else if target as usize >= layout.rows.len() {
|
||||
@@ -1479,6 +1503,7 @@ impl InputState {
|
||||
let goal_x = self.goal_x.unwrap_or(caret_x);
|
||||
(layout.index_at_row_x(target as usize, goal_x), Some(goal_x))
|
||||
};
|
||||
|
||||
Some((real_index(&layout.mask_map, display), goal_x))
|
||||
};
|
||||
|
||||
@@ -1748,8 +1773,7 @@ impl EntityInputHandler for InputState {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let layout = self.layout.borrow();
|
||||
let layout = layout.as_ref()?;
|
||||
let layout = self.layout.as_ref()?;
|
||||
|
||||
let start = display_index(
|
||||
&layout.mask_map,
|
||||
@@ -1797,8 +1821,7 @@ impl EntityInputHandler for InputState {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
let layout = self.layout.borrow();
|
||||
let layout = layout.as_ref()?;
|
||||
let layout = self.layout.as_ref()?;
|
||||
let index =
|
||||
real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len());
|
||||
Some(utf8_range_to_utf16(&self.text, index..index).start)
|
||||
@@ -2178,23 +2201,13 @@ impl RenderOnce for Input {
|
||||
.when(!self.disabled, |input| {
|
||||
input
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let mouse_down_handler = self
|
||||
.state
|
||||
.read(cx)
|
||||
.mouse_down_handler
|
||||
.clone()
|
||||
.expect("input state always carries its mouse-down handler");
|
||||
let mouse_down_handler = self.state.read(cx).mouse_down_handler.clone();
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
mouse_down_handler(event, window, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down({
|
||||
let key_down_handler = self
|
||||
.state
|
||||
.read(cx)
|
||||
.key_down_handler
|
||||
.clone()
|
||||
.expect("input state always carries its key handler");
|
||||
let key_down_handler = self.state.read(cx).key_down_handler.clone();
|
||||
move |event: &KeyDownEvent, window, cx| {
|
||||
key_down_handler(event, window, cx);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::{
|
||||
ease_out_quint, point, px, relative,
|
||||
};
|
||||
use gpui_native_icons::{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::scroll_area::{ScrollArea, ScrollAxes};
|
||||
@@ -35,7 +35,7 @@ pub struct MenuItem {
|
||||
handler: Option<ItemHandler>,
|
||||
disabled: bool,
|
||||
destructive: bool,
|
||||
checked: Option<bool>,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -58,7 +58,7 @@ impl MenuItem {
|
||||
handler: None,
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
checked: None,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl MenuItem {
|
||||
handler: None,
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
checked: None,
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ impl MenuItem {
|
||||
|
||||
/// Shows a check slot before the label.
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
self.checked = Some(checked);
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ pub(crate) fn anchored_menu_panel(
|
||||
ScrollArea::new("menu-items")
|
||||
.handle(scroll.read(cx).clone())
|
||||
.axes(ScrollAxes::Vertical)
|
||||
.max_h(px(320.))
|
||||
.max_h(POPUP_MAX_HEIGHT)
|
||||
.child(v_flex().gap_0p5().children(rows)),
|
||||
)
|
||||
.child(
|
||||
@@ -596,7 +596,7 @@ fn render_item(
|
||||
.child(shortcut),
|
||||
)
|
||||
})
|
||||
.when(checked == Some(true), |row| {
|
||||
.when(checked, |row| {
|
||||
row.child(
|
||||
icon_glyph(IconRole::Check)
|
||||
.size(chrome.dense_icon_size)
|
||||
|
||||
+20
-301
@@ -1,17 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
//! A scrollable container with an overlay scrollbar.
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, Bounds, DispatchPhase, ElementId, Entity, EntityId, InteractiveElement as _, IntoElement,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollEvent,
|
||||
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, Task, Window, canvas, div,
|
||||
fill, point,
|
||||
App, ElementId, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement as _, Styled, Window, div,
|
||||
};
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
use crate::common::StyledExt;
|
||||
use crate::scrollbar::{Scrollbar, ScrollbarVisibility};
|
||||
|
||||
/// Which directions the content scrolls in.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
@@ -25,108 +21,6 @@ pub enum ScrollAxes {
|
||||
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.
|
||||
#[derive(IntoElement)]
|
||||
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 {
|
||||
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();
|
||||
pane_style.padding = Default::default();
|
||||
|
||||
@@ -252,175 +122,24 @@ impl RenderOnce for ScrollArea {
|
||||
|
||||
if let Some(handle) = self.handle {
|
||||
let id = self.id.clone();
|
||||
let autohide = match self.visibility {
|
||||
ScrollbarVisibility::Platform => cx.theme().scrollbar.autohide,
|
||||
ScrollbarVisibility::Always => false,
|
||||
ScrollbarVisibility::Autohide => true,
|
||||
};
|
||||
|
||||
wrapper.extend(std::iter::once(
|
||||
canvas(|bounds, _, _| bounds, {
|
||||
let handle = handle.clone();
|
||||
move |overlay, _, window, cx| {
|
||||
paint_scrollbar(&handle, &id, autohide, overlay, window, cx);
|
||||
}
|
||||
})
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.into_any_element(),
|
||||
));
|
||||
if self.axes != ScrollAxes::Horizontal {
|
||||
wrapper.extend(std::iter::once(
|
||||
Scrollbar::vertical((id.clone(), "v-scrollbar"), handle.clone())
|
||||
.visibility(self.visibility)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,18 +1,141 @@
|
||||
//! Overlay scrollbars for scroll surfaces.
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, DispatchPhase, ElementId, IntoElement, ListState, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent, ScrollHandle,
|
||||
Styled as _, UniformListScrollHandle, Window, canvas, fill, point,
|
||||
App, Bounds, DispatchPhase, ElementId, Entity, EntityId, IntoElement, ListState, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, RenderOnce, ScrollEvent,
|
||||
ScrollHandle, SharedString, Styled as _, Task, UniformListScrollHandle, Window, canvas, fill,
|
||||
point,
|
||||
};
|
||||
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.
|
||||
pub use crate::scroll_area::ScrollbarVisibility;
|
||||
/// 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)]
|
||||
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.
|
||||
pub trait ScrollbarHandle: 'static {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div, px};
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
use gpui::{App, InteractiveElement as _, IntoElement, RenderOnce, Styled as _, Window, div};
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH};
|
||||
|
||||
/// Which axis the rule runs along.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -69,8 +69,8 @@ impl RenderOnce for Separator {
|
||||
rule.debug_selector(move || selector())
|
||||
})
|
||||
.map(|rule| match self.orientation {
|
||||
SeparatorOrientation::Horizontal => rule.w_full().h(px(1.)),
|
||||
SeparatorOrientation::Vertical => rule.self_stretch().w(px(1.)),
|
||||
SeparatorOrientation::Horizontal => rule.w_full().h(BORDER_WIDTH),
|
||||
SeparatorOrientation::Vertical => rule.self_stretch().w(BORDER_WIDTH),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,6 @@ impl Slider {
|
||||
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.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::AppContext as _;
|
||||
use gpui::{
|
||||
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};
|
||||
|
||||
const PANEL_BOUNDS_ID: &str = "tooltip-panel-bounds";
|
||||
@@ -76,7 +76,7 @@ impl Render for Tooltip {
|
||||
.debug_selector(|| "tooltip-panel".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.max_w(px(360.))
|
||||
.max_w(TOOLTIP_MAX_WIDTH)
|
||||
.bg(surface)
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
@@ -89,7 +89,7 @@ impl Render for Tooltip {
|
||||
|
||||
let (wrapper, panel) = match self.anchor {
|
||||
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 height = anchor_bounds(
|
||||
|
||||
@@ -197,16 +197,31 @@ impl RenderOnce for Sidebar {
|
||||
|
||||
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
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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(
|
||||
cx,
|
||||
entry,
|
||||
index,
|
||||
entries.len(),
|
||||
position_in_set,
|
||||
size_of_set,
|
||||
self.selected == Some(index),
|
||||
material,
|
||||
&self.on_change,
|
||||
@@ -258,7 +273,8 @@ fn render_entry(
|
||||
cx: &App,
|
||||
entry: &SidebarEntry,
|
||||
index: usize,
|
||||
count: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
material: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
@@ -294,8 +310,8 @@ fn render_entry(
|
||||
.rounded(theme.radius.sm)
|
||||
.role(Role::ListItem)
|
||||
.aria_selected(selected)
|
||||
.aria_position_in_set(index + 1)
|
||||
.aria_size_of_set(count)
|
||||
.aria_position_in_set(position_in_set)
|
||||
.aria_size_of_set(size_of_set)
|
||||
.map(|row| {
|
||||
if material && !*row_disabled {
|
||||
if selected {
|
||||
|
||||
@@ -214,7 +214,6 @@ impl RenderOnce for Tabs {
|
||||
cx,
|
||||
tab,
|
||||
index,
|
||||
self.selected == index,
|
||||
self.selected,
|
||||
self.variant,
|
||||
&self.on_change,
|
||||
@@ -272,7 +271,6 @@ fn render_tab(
|
||||
cx: &App,
|
||||
tab: Tab,
|
||||
index: usize,
|
||||
selected: bool,
|
||||
selected_index: usize,
|
||||
variant: TabsVariant,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
@@ -280,6 +278,7 @@ fn render_tab(
|
||||
focus: &gpui::FocusHandle,
|
||||
) -> AnyElement {
|
||||
let mut tab = tab;
|
||||
let selected = index == selected_index;
|
||||
|
||||
let theme = cx.theme();
|
||||
let metrics = theme.sizes.resolve(Size::default());
|
||||
|
||||
+42
-6
@@ -166,14 +166,49 @@ impl RenderOnce for Tree {
|
||||
.clone()
|
||||
.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
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _item)| {
|
||||
let (position_in_set, size_of_set) = sibling_sets[index];
|
||||
render_row(
|
||||
cx,
|
||||
&items,
|
||||
&items[index],
|
||||
index,
|
||||
position_in_set,
|
||||
size_of_set,
|
||||
self.selected == Some(index),
|
||||
&self.on_change,
|
||||
&self.on_disclosure,
|
||||
@@ -228,21 +263,21 @@ impl RenderOnce for Tree {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_row(
|
||||
cx: &App,
|
||||
items: &[TreeItem],
|
||||
item: &TreeItem,
|
||||
index: usize,
|
||||
position_in_set: usize,
|
||||
size_of_set: usize,
|
||||
selected: bool,
|
||||
on_change: &Option<ChangeHandler>,
|
||||
on_disclosure: &Option<DisclosureHandler>,
|
||||
focus: &FocusHandle,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let item = &items[index];
|
||||
let count = items.len();
|
||||
let label_color = selectable_label_color(cx, selected, item.disabled);
|
||||
let disabled = item.disabled;
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -258,7 +293,8 @@ fn render_row(
|
||||
.role(Role::TreeItem)
|
||||
.aria_selected(selected)
|
||||
.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))
|
||||
.map(|row| paint_selectable_row(cx, row, selected, disabled))
|
||||
.when(!disabled, |row| {
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
actions, canvas, div, uniform_list,
|
||||
};
|
||||
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};
|
||||
use crate::utils::{gutter_cell, run_style};
|
||||
@@ -132,11 +132,6 @@ impl CodeViewState {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// The language the source highlights as.
|
||||
pub fn language(&self) -> Language {
|
||||
self.language
|
||||
}
|
||||
|
||||
/// The highlighted lines.
|
||||
pub fn code(&self) -> &HighlightedCode {
|
||||
&self.code
|
||||
@@ -209,13 +204,7 @@ impl CodeViewState {
|
||||
(!selected.is_empty()).then(|| selected.join("\n"))
|
||||
}
|
||||
|
||||
/// Drops the selection.
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.selection = None;
|
||||
self.selecting = false;
|
||||
}
|
||||
|
||||
/// Overrides the accessibility label of the view.
|
||||
// Overrides the accessibility label of the view.
|
||||
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
@@ -235,8 +224,7 @@ impl CodeViewState {
|
||||
text_x: Pixels,
|
||||
) -> SelectionPoint {
|
||||
let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize)
|
||||
.clamp(0, self.code.lines.len() as isize - 1)
|
||||
.max(0) as usize;
|
||||
.clamp(0, self.code.lines.len() as isize - 1) as usize;
|
||||
|
||||
let chars = self.code.lines[row]
|
||||
.spans
|
||||
@@ -244,9 +232,8 @@ impl CodeViewState {
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum::<usize>();
|
||||
|
||||
let col = (((position.x - text_x) / char_width).round() as isize)
|
||||
.clamp(0, chars as isize)
|
||||
.max(0) as usize;
|
||||
let col = (((position.x - text_x) / char_width).round() as isize).clamp(0, chars as isize)
|
||||
as usize;
|
||||
|
||||
SelectionPoint { row, col }
|
||||
}
|
||||
@@ -265,7 +252,7 @@ impl Render for CodeViewState {
|
||||
let mono_font = theme.mono_font;
|
||||
let text_size = theme.sizes.small.text_size;
|
||||
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 id = self.id.clone();
|
||||
|
||||
@@ -13,7 +13,7 @@ use gpui::{
|
||||
point, uniform_list,
|
||||
};
|
||||
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 std::ops::Range;
|
||||
@@ -399,7 +399,7 @@ fn render_row(
|
||||
let ramps = theme.ramps;
|
||||
let text_size = theme.sizes.small.text_size;
|
||||
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()
|
||||
.debug_selector(move || format!("diff-row-{row_ix}"))
|
||||
|
||||
@@ -101,8 +101,6 @@ pub enum InlineStyle {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct MarkdownTableCell {
|
||||
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.
|
||||
@@ -293,24 +291,6 @@ struct ListFrame {
|
||||
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.
|
||||
#[derive(Default)]
|
||||
struct InlineBuilder {
|
||||
@@ -403,8 +383,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
// Whether each open list item already flushed content rows.
|
||||
let mut items: Vec<bool> = Vec::new();
|
||||
let mut quote_depth = 0usize;
|
||||
let mut table = TableBuilder::None;
|
||||
let mut code: Option<CodeBuilder> = None;
|
||||
let mut table: Option<Vec<MarkdownTableRow>> = None;
|
||||
let mut code: Option<String> = None;
|
||||
// The fence info string arrives on Start, its text follows.
|
||||
let mut code_language: Option<SharedString> = None;
|
||||
|
||||
@@ -469,7 +449,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
&mut blocks,
|
||||
quote_depth,
|
||||
);
|
||||
code = Some(CodeBuilder::default());
|
||||
code = Some(String::new());
|
||||
if let CodeBlockKind::Fenced(info) = kind {
|
||||
let info = info.trim();
|
||||
code_language =
|
||||
@@ -484,10 +464,10 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
&mut blocks,
|
||||
quote_depth,
|
||||
);
|
||||
table = TableBuilder::Body { rows: Vec::new() }
|
||||
table = Some(Vec::new())
|
||||
}
|
||||
Tag::TableHead => {
|
||||
if let TableBuilder::Body { rows } = &mut table {
|
||||
if let Some(rows) = &mut table {
|
||||
rows.push(MarkdownTableRow {
|
||||
cells: Vec::new(),
|
||||
header: true,
|
||||
@@ -495,7 +475,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
}
|
||||
}
|
||||
Tag::TableRow => {
|
||||
if let TableBuilder::Body { rows } = &mut table {
|
||||
if let Some(rows) = &mut table {
|
||||
rows.push(MarkdownTableRow {
|
||||
cells: Vec::new(),
|
||||
header: false,
|
||||
@@ -583,7 +563,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
}
|
||||
TagEnd::CodeBlock => {
|
||||
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 lines = match highlight_code
|
||||
.then(|| language.as_deref().and_then(grammar_language))
|
||||
@@ -600,8 +580,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
});
|
||||
}
|
||||
TagEnd::Table => {
|
||||
let built = std::mem::replace(&mut table, TableBuilder::None);
|
||||
if let TableBuilder::Body { rows } = built {
|
||||
if let Some(rows) = table.take() {
|
||||
blocks.push(MarkdownBlock::Table {
|
||||
table: MarkdownTable { rows },
|
||||
indent: lists.len(),
|
||||
@@ -611,16 +590,13 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
}
|
||||
TagEnd::TableCell => {
|
||||
let spans = inline.finish();
|
||||
let TableBuilder::Body { rows } = &mut table else {
|
||||
let Some(rows) = &mut table else {
|
||||
continue;
|
||||
};
|
||||
let Some(row) = rows.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
row.cells.push(MarkdownTableCell {
|
||||
spans,
|
||||
column: row.cells.len() + 1,
|
||||
});
|
||||
row.cells.push(MarkdownTableCell { spans });
|
||||
}
|
||||
TagEnd::Emphasis
|
||||
| TagEnd::Strong
|
||||
@@ -630,8 +606,8 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
_ => {}
|
||||
},
|
||||
Event::Text(text) => {
|
||||
if let Some(built) = code.as_mut() {
|
||||
built.push(&text);
|
||||
if let Some(buffer) = code.as_mut() {
|
||||
buffer.push_str(&text);
|
||||
} else {
|
||||
inline.push_text(&text);
|
||||
}
|
||||
@@ -660,9 +636,9 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
frame.task = Some(checked);
|
||||
}
|
||||
}
|
||||
Event::FootnoteReference(label) => inline.push_text(&label),
|
||||
// HTML and math render nothing in this view.
|
||||
Event::InlineHtml(_)
|
||||
// Footnotes are disabled in the parser options.
|
||||
Event::FootnoteReference(_)
|
||||
| Event::InlineHtml(_)
|
||||
| Event::Html(_)
|
||||
| Event::InlineMath(_)
|
||||
| Event::DisplayMath(_) => {}
|
||||
|
||||
@@ -305,14 +305,6 @@ fn tables_parse_header_first_with_column_positions() {
|
||||
.map(|cell| spans_text(&cell.spans))
|
||||
.collect();
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ default = ["full"]
|
||||
full = ["dock", "resize_handle", "resizable", "shell", "window_controls"]
|
||||
dock = [
|
||||
"resize_handle",
|
||||
"resizable",
|
||||
"dep:gpui-native-core",
|
||||
"dep:gpui-native-data",
|
||||
"dep:gpui-native-icons",
|
||||
|
||||
@@ -192,10 +192,8 @@ impl DockState {
|
||||
}
|
||||
|
||||
/// Registers a panel descriptor, replacing any earlier one with its id.
|
||||
pub fn register_panel(&mut self, panel: PanelDescriptor, cx: &mut Context<Self>) {
|
||||
if self.panels.insert(panel.id.clone(), panel).is_none() {
|
||||
cx.notify();
|
||||
}
|
||||
pub fn register_panel(&mut self, panel: PanelDescriptor) {
|
||||
self.panels.insert(panel.id.clone(), panel);
|
||||
}
|
||||
|
||||
/// Opens a panel into the active pane and activates its tab.
|
||||
@@ -330,6 +328,17 @@ impl DockState {
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -354,10 +363,14 @@ impl DockState {
|
||||
}
|
||||
|
||||
self.detach_pane(pane_key);
|
||||
|
||||
if self.active_pane == pane_key {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -526,6 +539,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> {
|
||||
Self::pane_mut_in(&mut self.root, key)
|
||||
}
|
||||
@@ -1071,7 +1097,7 @@ fn render_pane(
|
||||
.h(titlebar.height)
|
||||
.items_center()
|
||||
.pl(if titlebar.traffic_light_pad {
|
||||
px(TRAFFIC_LIGHT_PAD)
|
||||
TRAFFIC_LIGHT_PAD
|
||||
} else {
|
||||
padding_x
|
||||
})
|
||||
|
||||
@@ -10,11 +10,11 @@ use gpui::{
|
||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
|
||||
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};
|
||||
|
||||
/// 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 ResetHandler = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
@@ -180,10 +180,10 @@ impl RenderOnce for ResizeHandle {
|
||||
let on_resize = self.on_resize.clone();
|
||||
move |event: &KeyDownEvent, window, cx| {
|
||||
let delta = match (axis, event.keystroke.key.as_str()) {
|
||||
(Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)),
|
||||
(Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)),
|
||||
(Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)),
|
||||
(Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)),
|
||||
(Axis::Horizontal, "left") => Some(-KEYBOARD_STEP),
|
||||
(Axis::Horizontal, "right") => Some(KEYBOARD_STEP),
|
||||
(Axis::Vertical, "up") => Some(-KEYBOARD_STEP),
|
||||
(Axis::Vertical, "down") => Some(KEYBOARD_STEP),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(delta) = delta {
|
||||
|
||||
+20
-40
@@ -4,8 +4,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, Styled as _, Window, div,
|
||||
px,
|
||||
IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px,
|
||||
};
|
||||
use gpui_native_core::button::{Button, ButtonVariant};
|
||||
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
||||
@@ -108,20 +107,6 @@ impl Side {
|
||||
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.
|
||||
@@ -204,6 +189,7 @@ impl RenderOnce for Shell {
|
||||
let width = state.read(cx).left_sidebar_width();
|
||||
layout
|
||||
.child(sidebar_region(
|
||||
(id.clone(), "left"),
|
||||
width,
|
||||
left.take()
|
||||
.map(|sidebar| sidebar.into_any_element())
|
||||
@@ -242,6 +228,7 @@ impl RenderOnce for Shell {
|
||||
}
|
||||
}))
|
||||
.child(sidebar_region(
|
||||
(id.clone(), "right"),
|
||||
width,
|
||||
right.take().unwrap_or_else(|| div().into_any_element()),
|
||||
Side::Right,
|
||||
@@ -262,6 +249,7 @@ impl RenderOnce for Shell {
|
||||
/// One sidebar: a header strip with the hide toggle over the app's content.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sidebar_region(
|
||||
id: impl Into<ElementId>,
|
||||
width: Pixels,
|
||||
content: AnyElement,
|
||||
side: Side,
|
||||
@@ -272,6 +260,7 @@ fn sidebar_region(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let id = id.into();
|
||||
let theme = cx.theme();
|
||||
let background = theme.colors.background;
|
||||
let strip_background = match (side, material) {
|
||||
@@ -280,7 +269,7 @@ fn sidebar_region(
|
||||
};
|
||||
|
||||
let strip = h_flex()
|
||||
.id(SharedString::from(format!("sidebar-strip-{}", side.name())))
|
||||
.id((id.clone(), "strip"))
|
||||
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
||||
.flex_none()
|
||||
.h(strip_height)
|
||||
@@ -288,40 +277,31 @@ fn sidebar_region(
|
||||
.items_center()
|
||||
.map(|strip| match side {
|
||||
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
|
||||
strip.pl(px(TRAFFIC_LIGHT_PAD))
|
||||
strip.pl(TRAFFIC_LIGHT_PAD)
|
||||
}),
|
||||
Side::Right => strip,
|
||||
})
|
||||
.px(padding_x)
|
||||
.child(titlebar_strip(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"sidebar-strip-drag-{}",
|
||||
side.name()
|
||||
)))
|
||||
.flex_1()
|
||||
.self_stretch(),
|
||||
div().id((id.clone(), "strip-drag")).flex_1().self_stretch(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.map(|strip| match side {
|
||||
Side::Left => strip.child(
|
||||
Button::new(SharedString::from(format!(
|
||||
"sidebar-toggle-{}",
|
||||
side.name()
|
||||
)))
|
||||
.icon(side.toggle_icon())
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.accessibility_label(side.toggle_label())
|
||||
.on_click({
|
||||
let state = state.clone();
|
||||
move |_, _, cx| {
|
||||
state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx))
|
||||
}
|
||||
}),
|
||||
Button::new((id, "sidebar-toggle"))
|
||||
.icon(IconRole::PanelLeft)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.small()
|
||||
.accessibility_label("Hide left sidebar")
|
||||
.on_click({
|
||||
let state = state.clone();
|
||||
move |_, _, cx| {
|
||||
state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx))
|
||||
}
|
||||
}),
|
||||
),
|
||||
Side::Right => strip.child(WindowControls::new("sidebar-window-controls")),
|
||||
Side::Right => strip.child(WindowControls::new((id, "window-controls"))),
|
||||
});
|
||||
|
||||
v_flex()
|
||||
|
||||
@@ -23,13 +23,9 @@ use gpui::Decorations;
|
||||
use gpui::{SharedString, px, rgba};
|
||||
|
||||
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
||||
pub const TRAFFIC_LIGHT_PAD: f32 = 78.0;
|
||||
/// The width of a Windows caption button.
|
||||
pub use gpui_native_theme::TRAFFIC_LIGHT_PAD;
|
||||
#[cfg(target_os = "windows")]
|
||||
const CAPTION_WIDTH: f32 = 36.0;
|
||||
/// 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);
|
||||
use gpui_native_theme::{WINDOWS_CAPTION_WIDTH as CAPTION_WIDTH, WINDOWS_CLOSE_HOVER_HEX};
|
||||
|
||||
/// The window controls for the trailing end of a titlebar strip.
|
||||
#[derive(IntoElement)]
|
||||
@@ -150,7 +146,7 @@ impl RenderOnce for WindowControls {
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
if event.click_count() != 2 {
|
||||
return;
|
||||
@@ -210,16 +206,9 @@ fn caption_button(
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
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 {
|
||||
close_hover
|
||||
rgba(WINDOWS_CLOSE_HOVER_HEX)
|
||||
} else {
|
||||
theme.colors.muted_hover
|
||||
};
|
||||
|
||||
@@ -7,12 +7,8 @@ use gpui::{
|
||||
IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _,
|
||||
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.
|
||||
const INACTIVE_SHADOW_OPACITY: f32 = 0.7;
|
||||
|
||||
@@ -28,7 +24,7 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
|
||||
tiling
|
||||
};
|
||||
|
||||
let inset = px(RESIZE_INSET);
|
||||
let inset = WINDOW_RESIZE_INSET;
|
||||
let border = cx.theme().colors.border;
|
||||
let radius = cx.theme().radius.overlay;
|
||||
|
||||
@@ -75,10 +71,10 @@ pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.border_color(border)
|
||||
.when(!tiling.top, |surface| surface.border_t(px(BORDER_WIDTH)))
|
||||
.when(!tiling.bottom, |surface| surface.border_b(px(BORDER_WIDTH)))
|
||||
.when(!tiling.left, |surface| surface.border_l(px(BORDER_WIDTH)))
|
||||
.when(!tiling.right, |surface| surface.border_r(px(BORDER_WIDTH)))
|
||||
.when(!tiling.top, |surface| surface.border_t(BORDER_WIDTH))
|
||||
.when(!tiling.bottom, |surface| surface.border_b(BORDER_WIDTH))
|
||||
.when(!tiling.left, |surface| surface.border_l(BORDER_WIDTH))
|
||||
.when(!tiling.right, |surface| surface.border_r(BORDER_WIDTH))
|
||||
.when(!(tiling.top || tiling.left), |surface| {
|
||||
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) {
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.update(cx, |dock, _cx| {
|
||||
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 (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
||||
dock.register_panel(descriptor("inbox"), cx);
|
||||
dock.update(cx, |dock, _cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"));
|
||||
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 (dock, opened, mut visual) = sidebar_window(&mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"), cx);
|
||||
dock.register_panel(descriptor_with_right_sidebar("explore"), cx);
|
||||
dock.update(cx, |dock, _cx| {
|
||||
dock.register_panel(descriptor_with_right_sidebar("overview"));
|
||||
dock.register_panel(descriptor_with_right_sidebar("explore"));
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
|
||||
@@ -74,9 +74,7 @@ fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("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;
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("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 traffic lights.
|
||||
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:?})"
|
||||
);
|
||||
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:?})"
|
||||
);
|
||||
}
|
||||
@@ -115,9 +111,7 @@ fn window_controls_render_only_when_the_flag_is_set() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("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 (dock, mut visual) = dock_window(true, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
dock.update(cx, |dock, _cx| dock.register_panel(descriptor("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 std::time::Duration;
|
||||
|
||||
/// The hairline width of the control border every button and field paints;
|
||||
/// sized outside the stated metrics so control sizes match across platforms.
|
||||
/// The hairline width of the control border every button and field paints.
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
|
||||
|
||||
+48
-5
@@ -65,6 +65,8 @@ gpui scopes `use_keyed_state` by the element-id path; a `RenderOnce` renders und
|
||||
|
||||
All verified by workspace-wide greps with **zero call sites** outside their own definitions/tests.
|
||||
|
||||
> **Re-checked 2026-11-09** (post-P1): several entries below are superseded by the policy "visual/a11y surface stays even when dead" — see the current P2 plan at the bottom of this document for the authoritative delete/keep split. Notably kept: `PanelDescriptor::icon`, `DropdownMenu::icon()`/`.tab_stop()`, `Alert::icon()`/`.no_icon()`, `markdown plain_code()`, the `accessibility_label` convention, `ResizeHandle::disabled()`, `Resizable::sizes()`/`set_sizes()` (test fixture), `Separator::orientation()`. Still deleted: everything non-UI in the list. Already resolved by review passes: `Tab::lead` (deleted), `ComboboxChanged` re-verified still present.
|
||||
|
||||
| Item | Location | Note |
|
||||
|---|---|---|
|
||||
| `Slider::value()` | `slider.rs:83-86` | `new` already takes `value` |
|
||||
@@ -175,9 +177,50 @@ Rule: `crates/theme` owns every color and metric — no raw `rgb`/`rgba`/`hsla`
|
||||
|
||||
---
|
||||
|
||||
## Recommended fix order (before migrating `signed`)
|
||||
## Progress
|
||||
|
||||
1. **P0 — shared keyed state sweep** (findings 1-4): ~15 lines total; add a two-sibling regression test per component, since single-instance tests provably mask this class.
|
||||
2. **P0 — user-facing bugs**: IME UTF-16 conversion (5), combobox chevron (6), palette z-order (7), code_view click (8) + incremental misuse (9), resize capture loss (10), tab element ids (14).
|
||||
3. **P1 — performance**: registry (11), caret blink (12), widest_row (13), the `debug_selector` batch, the per-frame clone batch.
|
||||
4. **P2 — deletions**: the param/dead-API table and the over-engineering list — this is where "no over-engineering" actually gets enforced, and it shrinks the API surface `signed` will migrate onto.
|
||||
### P0 — done (commit `73f7dc3`, reviewed)
|
||||
|
||||
- Shared keyed-state sweep (findings 1-4): all `use_keyed_state` keys carry the instance id; `Scrollbar::vertical/horizontal(id, handle)` now require an id, `.key()` deleted.
|
||||
- Correctness bugs (5, 6, 7, 8, 9, 10, 14): IME UTF-16 conversion fixed, combobox chevron lists all options, palette modal deferred + dead bounds machinery removed, code_view click honors scroll, tree-sitter incremental misuse deleted (`highlight_with_tree` folded into `highlight`), resize drag ends on capture loss, `Tab::new(key, label)` with stable element ids.
|
||||
- Regression tests added: two-sibling independence for slider / dropdown / scrollbar, resize capture-loss, multibyte UTF-16 conversions.
|
||||
|
||||
### P1 — done (in working tree, uncommitted)
|
||||
|
||||
- Registry (11): keyed by `(WindowId, ElementId)`, mutated in place via `cx.default_global` — no whole-map clone, no cross-window bleed. `anchor_bounds` takes the window id now.
|
||||
- Caret blink (12): `with_max_fps(4.0)`; `field_bench` updated to probe the capped contract (executor-timer redraw, no next-frame demand).
|
||||
- widest_row (13): cached in `set_rows` (called by `new`/`set_versions`).
|
||||
- `debug_selector` batch: all eager formats moved into closures (switch, field ×3, combobox ×2, command_palette ×2, tabs ×2, sidebar, code_view, markdown, dock ×2 reusing the `.id()` format).
|
||||
- Per-frame clone batch: segmented options are `Rc<[SegmentedOption<T>]>` (closures capture bumps; `selectable` Vec → index math), select trigger text computed in render, menu/dropdown items passed as `Rc<[MenuItem]>`, alert borrows entries, code_view borrows the row `Line`.
|
||||
- Skipped deliberately: the caret animation-id `format!` (removing it needs id plumbing through `InputState` for a ~30-byte alloc — over-engineering).
|
||||
- Decision: registry stays a gpui `Global`, not a `OnceLock`. `WindowId`s come from a per-`App` slotmap, so a process-static map would leak state across tests (every `TestAppContext` mints the same early window ids); a Global dies with its `App` and needs no locking.
|
||||
|
||||
Validation after P1: `cargo check --workspace --all-targets` clean, `cargo test --workspace` 64 binaries 0 failures, `cargo fmt --check` clean.
|
||||
|
||||
### P2 — done (in working tree, uncommitted)
|
||||
|
||||
Re-verified the whole P2 list against the tree first. Policy recorded in the plan below: visual/a11y surface stays even when dead; only non-UI dead code was deleted.
|
||||
|
||||
**1a — deleted (non-UI dead API):** `Slider::value()`, `ComboboxChanged` + its `EventEmitter` impl + emit, `CodeView::language()`, `CodeView::clear_selection()`, `Input::submit_on()` getter, `MarkdownTableCell.column` (+ bookkeeping + test assertion), `Side::toggle_icon`/`toggle_label` (left values inlined at the call site), `render_tab`'s duplicate `selected` param (derived from `selected_index`), `register_panel`'s first-insert-only notify and its `cx` parameter (all call sites updated), `Menu checked` field → plain `bool`, `refilter` → `()`. Visibility narrowed: `scrollbar_state_key` → `pub(crate)`, `maximize_on_double_click` → private, `into_bindings` → `pub(crate)`.
|
||||
|
||||
**1b — kept (UI/a11y surface, dead is fine):** `PanelDescriptor::icon`, `DropdownMenu::icon()`/`.tab_stop()`, `Alert::icon()`/`.no_icon()`, `markdown plain_code()`, the whole `accessibility_label` convention, `ResizeHandle::disabled()`, `Resizable::sizes()`/`set_sizes()`, `Separator::orientation()`, `data` closure `&mut Window` params. One remaining fix moved to follow-ups: `Segmented`'s unlabeled aria fallback announcing internal ids.
|
||||
|
||||
**2 — over-engineering deletions:** dialog's `BoundsCell` + capture canvas + bounds check deleted (panel `stop_propagation` is the one mechanism); scroll_area's ~150-line drifted painter deleted — it now composes the real `Scrollbar` (`ScrollbarVisibility`, `Autohide`, and the geometry helpers moved into `scrollbar.rs`), and `ScrollAxes::Both` now renders a horizontal thumb too, resolving the promised-but-unimplemented `Horizontal`; input's `Rc<RefCell<Option<InputLayout>>>` → plain `Option<InputLayout>` field, both always-`Some` handler `Option`s → non-optional (two `expect`s deleted), `caret_epoch_started: Option<Instant>` → `Instant`; markdown's `CodeBuilder` and `TableBuilder` deleted (`String` / `Option<Vec<_>>`), the unreachable footnote arm folded into the render-nothing group; avatar's `PixelAvatar.style` + `Styled` impl + duplicate clipping deleted; combobox `refilter` clamps the highlight and the identity round-trip bounds check became `highlight.filter(...)`; code_view's two no-op `.max(0)`s deleted; layout `Cargo.toml`: `dock` now depends on `resize_handle` directly instead of `resizable`.
|
||||
|
||||
**3 — theme-token hoists:** new theme consts: `POPUP_MAX_HEIGHT` (the three `px(320.)` popup sites), `TOOLTIP_MAX_WIDTH`/`TOOLTIP_OFFSET`, `CARET_WIDTH`, `UNDERLINE_WIDTH`, `WHEEL_LINE_HEIGHT`, `RESIZE_KEYBOARD_STEP`, `WINDOW_RESIZE_INSET`, `TRAFFIC_LIGHT_PAD` (moved out of window_controls; dock/shell/tests import it from the theme), `WINDOWS_CAPTION_WIDTH`, `WINDOWS_CLOSE_HOVER_HEX`, `MONO_ADVANCE_RATIO` (the duplicated `0.62` estimate), `PIXEL_AVATAR_*` tint/shade factors. Button's Ghost border now paints `transparent_black()` instead of raw `hsla(0,0,0,0)` (kept painted for uniform control metrics), dropdown fallback uses `transparent_black()`, separator hairline uses `BORDER_WIDTH`.
|
||||
|
||||
**4 — smaller cleanups:** input early-returns when a cursor/selection/wheel change is a no-op (no notify, no IME re-query, no blink-epoch reset); IME pre-edit steps merge into the previous undo entry unconditionally while a composition is active; `line.paint` errors logged instead of `.ok()`; `caret_visible` takes `&self`; `Alerts::clear` runs every entry's `on_dismiss` handler (matches `Toasts::clear`); dock `close_others`/`close_pane` now reconcile the displayed right view (`close_panel` already did); combobox arms row 0 unconditionally on open, so empty results show "No matches" instead of closing (palette parity); palette rows keyed `("command", command.id)` — stable domain ids, not per-keystroke indexes; shell element ids composed from the shell id (`(id, "strip")`, `(id, "strip-drag")`, `(id, "sidebar-toggle")`, `(id, "window-controls")`); sidebar a11y sets count sibling rows only (headers excluded); tree a11y position/size computed among same-depth siblings in one pass.
|
||||
|
||||
Validation after P2: `cargo fmt --check` clean, `cargo check --workspace --all-targets` 0 errors/0 warnings, `cargo test --workspace` 64 binaries 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## Next step — `signed` migration
|
||||
|
||||
The audit backlog (P0-P2) is complete. Remaining small follow-ups, none blocking: `Segmented`'s unlabeled aria fallback; `markdown plain_code()`'s double parse if a consumer ever uses it; `Separator::orientation()` fold if strictness is ever wanted.
|
||||
|
||||
Kickoff:
|
||||
|
||||
1. Clone `signed` (git.reya.info/reya/signed) beside this workspace.
|
||||
2. Inventory its views against the component list; mark each usage as: direct fit / needs a small adapter / needs a new component.
|
||||
3. Bring missing components in-tree here (per repo rules: components are written here, not ported), then migrate view-by-view.
|
||||
|
||||
+177
-267
@@ -424,294 +424,204 @@ impl Gallery {
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("button", "Button", |_, cx| {
|
||||
cx.new(|_| ButtonDemo { clicks: 0 }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("input", "Input", |_, cx| cx.new(InputDemo::new).into()),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("field", "Field", |window, cx| {
|
||||
cx.new(|cx| FieldDemo::new(window, cx)).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("combobox", "Combobox", |window, cx| {
|
||||
cx.new(|cx| ComboboxDemo::new(window, cx)).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
|
||||
cx.new(|_| StatusDotDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("checkbox", "Checkbox", |_, cx| {
|
||||
cx.new(|_| CheckboxDemo { checked: false }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("radio", "Radio", |_, cx| {
|
||||
cx.new(|_| RadioDemo {
|
||||
plan: SharedString::from("starter"),
|
||||
dock.register_panel(PanelDescriptor::new("button", "Button", |_, cx| {
|
||||
cx.new(|_| ButtonDemo { clicks: 0 }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("input", "Input", |_, cx| {
|
||||
cx.new(InputDemo::new).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("field", "Field", |window, cx| {
|
||||
cx.new(|cx| FieldDemo::new(window, cx)).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"combobox",
|
||||
"Combobox",
|
||||
|window, cx| cx.new(|cx| ComboboxDemo::new(window, cx)).into(),
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("status-dot", "Status dot", |_, cx| {
|
||||
cx.new(|_| StatusDotDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("checkbox", "Checkbox", |_, cx| {
|
||||
cx.new(|_| CheckboxDemo { checked: false }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("radio", "Radio", |_, cx| {
|
||||
cx.new(|_| RadioDemo {
|
||||
plan: SharedString::from("starter"),
|
||||
})
|
||||
.into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("switch", "Switch", |_, cx| {
|
||||
cx.new(|_| SwitchDemo { checked: true }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("toggle", "Toggle", |_, cx| {
|
||||
cx.new(|_| ToggleDemo { pressed: false }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("slider", "Slider", |_, cx| {
|
||||
cx.new(|_| SliderDemo { volume: 40.0 }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("segmented", "Segmented", |_, cx| {
|
||||
cx.new(|_| SegmentedDemo {
|
||||
filter: ExploreFilter::default(),
|
||||
})
|
||||
.into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("select", "Select", |_, cx| {
|
||||
cx.new(|_| SelectDemo {
|
||||
strategy: Some(Strategy::Curated),
|
||||
})
|
||||
.into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("link", "Link", |_, cx| {
|
||||
cx.new(|_| LinkDemo { clicks: 0 }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("clipboard", "Clipboard", |_, cx| {
|
||||
cx.new(|_| ClipboardDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("badge", "Badge", |_, cx| {
|
||||
cx.new(|_| BadgeDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("avatar", "Avatar", |_, cx| {
|
||||
cx.new(|_| AvatarDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("progress", "Progress", |_, cx| {
|
||||
cx.new(|_| ProgressDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("spinner", "Spinner", |_, cx| {
|
||||
cx.new(|_| SpinnerDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("list", "List", |_, cx| {
|
||||
cx.new(|_| ListDemo { selected: None }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("tree", "Tree", |_, cx| {
|
||||
cx.new(|_| TreeDemo {
|
||||
nodes: TreeDemo::demo(),
|
||||
selected: None,
|
||||
})
|
||||
.into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("tabs", "Tabs", |_, cx| {
|
||||
cx.new(|_| TabsDemo {
|
||||
selected: 0,
|
||||
pill_tabs: vec![
|
||||
SharedString::from("Explorer"),
|
||||
SharedString::from("Syncing"),
|
||||
SharedString::from("Blame"),
|
||||
],
|
||||
pill_selected: 0,
|
||||
})
|
||||
.into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("alert", "Alert", {
|
||||
let alerts = alerts.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| AlertDemo {
|
||||
alerts: alerts.clone(),
|
||||
dismissed: false,
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("switch", "Switch", |_, cx| {
|
||||
cx.new(|_| SwitchDemo { checked: true }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("toggle", "Toggle", |_, cx| {
|
||||
cx.new(|_| ToggleDemo { pressed: false }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("slider", "Slider", |_, cx| {
|
||||
cx.new(|_| SliderDemo { volume: 40.0 }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("segmented", "Segmented", |_, cx| {
|
||||
cx.new(|_| SegmentedDemo {
|
||||
filter: ExploreFilter::default(),
|
||||
}
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("toast", "Toast", {
|
||||
let toasts = toasts.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| ToastDemo {
|
||||
toasts: toasts.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("select", "Select", |_, cx| {
|
||||
cx.new(|_| SelectDemo {
|
||||
strategy: Some(Strategy::Curated),
|
||||
}
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("dialog", "Dialog", {
|
||||
let gallery = gallery.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| DialogDemo {
|
||||
gallery: gallery.clone(),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("link", "Link", |_, cx| {
|
||||
cx.new(|_| LinkDemo { clicks: 0 }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("clipboard", "Clipboard", |_, cx| {
|
||||
cx.new(|_| ClipboardDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("badge", "Badge", |_, cx| cx.new(|_| BadgeDemo).into()),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("avatar", "Avatar", |_, cx| cx.new(|_| AvatarDemo).into()),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("progress", "Progress", |_, cx| {
|
||||
cx.new(|_| ProgressDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("spinner", "Spinner", |_, cx| cx.new(|_| SpinnerDemo).into()),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("list", "List", |_, cx| {
|
||||
cx.new(|_| ListDemo { selected: None }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("tree", "Tree", |_, cx| {
|
||||
cx.new(|_| TreeDemo {
|
||||
nodes: TreeDemo::demo(),
|
||||
selected: None,
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("tabs", "Tabs", |_, cx| {
|
||||
cx.new(|_| TabsDemo {
|
||||
selected: 0,
|
||||
pill_tabs: vec![
|
||||
SharedString::from("Explorer"),
|
||||
SharedString::from("Syncing"),
|
||||
SharedString::from("Blame"),
|
||||
],
|
||||
pill_selected: 0,
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("alert", "Alert", {
|
||||
let alerts = alerts.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| AlertDemo {
|
||||
alerts: alerts.clone(),
|
||||
dismissed: false,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("toast", "Toast", {
|
||||
let toasts = toasts.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| ToastDemo {
|
||||
toasts: toasts.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("dialog", "Dialog", {
|
||||
let gallery = gallery.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| DialogDemo {
|
||||
gallery: gallery.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("command-palette", "Command palette", {
|
||||
}
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"command-palette",
|
||||
"Command palette",
|
||||
{
|
||||
let palette = palette.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx))
|
||||
.into()
|
||||
}
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("menu", "Menu", |_, cx| {
|
||||
cx.new(|_| MenuDemo { open: false }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("dropdown-menu", "Dropdown menu", |_, cx| {
|
||||
},
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("menu", "Menu", |_, cx| {
|
||||
cx.new(|_| MenuDemo { open: false }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"dropdown-menu",
|
||||
"Dropdown menu",
|
||||
|_, cx| {
|
||||
cx.new(|_| DropdownMenuDemo {
|
||||
open_menu: None,
|
||||
status: None,
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("context-menu", "Context menu", |_, cx| {
|
||||
},
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"context-menu",
|
||||
"Context menu",
|
||||
|_, cx| {
|
||||
cx.new(|_| ContextMenuDemo {
|
||||
action: SharedString::from("Right-click the card"),
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("popover", "Popover", |_, cx| {
|
||||
cx.new(|_| PopoverDemo { open: false }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("tooltip", "Tooltip", |_, cx| cx.new(|_| TooltipDemo).into()),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("empty-state", "Empty state", |_, cx| {
|
||||
cx.new(|_| EmptyStateDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("scroll-area", "Scroll area", |_, cx| {
|
||||
cx.new(|_| ScrollAreaDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| {
|
||||
cx.new(|_| ScrollbarDemo::new()).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("code-view", "Code view", |_, cx| {
|
||||
cx.new(CodeViewDemo::new).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("diff-view", "Diff view", |_, cx| {
|
||||
cx.new(DiffViewDemo::new).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("markdown-view", "Markdown view", |_, cx| {
|
||||
cx.new(MarkdownViewDemo::new).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
|
||||
cx.new(|_| SidebarDemo { selected: None }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("resize-handle", "Resize handle", |_, cx| {
|
||||
cx.new(|_| ResizeDemo { width: px(200.) }).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("resizable", "Resizable", |_, cx| {
|
||||
cx.new(ResizableDemo::new).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("separator", "Separator", |_, cx| {
|
||||
cx.new(|_| SeparatorDemo).into()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
dock.register_panel(
|
||||
PanelDescriptor::new("theme", "Theme", |_, cx| cx.new(|_| ThemeDemo).into()),
|
||||
cx,
|
||||
);
|
||||
},
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("popover", "Popover", |_, cx| {
|
||||
cx.new(|_| PopoverDemo { open: false }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("tooltip", "Tooltip", |_, cx| {
|
||||
cx.new(|_| TooltipDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"empty-state",
|
||||
"Empty state",
|
||||
|_, cx| cx.new(|_| EmptyStateDemo).into(),
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"scroll-area",
|
||||
"Scroll area",
|
||||
|_, cx| cx.new(|_| ScrollAreaDemo).into(),
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("scrollbar", "Scrollbar", |_, cx| {
|
||||
cx.new(|_| ScrollbarDemo::new()).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("code-view", "Code view", |_, cx| {
|
||||
cx.new(CodeViewDemo::new).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("diff-view", "Diff view", |_, cx| {
|
||||
cx.new(DiffViewDemo::new).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"markdown-view",
|
||||
"Markdown view",
|
||||
|_, cx| cx.new(MarkdownViewDemo::new).into(),
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
|
||||
cx.new(|_| SidebarDemo { selected: None }).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new(
|
||||
"resize-handle",
|
||||
"Resize handle",
|
||||
|_, cx| cx.new(|_| ResizeDemo { width: px(200.) }).into(),
|
||||
));
|
||||
dock.register_panel(PanelDescriptor::new("resizable", "Resizable", |_, cx| {
|
||||
cx.new(ResizableDemo::new).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("separator", "Separator", |_, cx| {
|
||||
cx.new(|_| SeparatorDemo).into()
|
||||
}));
|
||||
dock.register_panel(PanelDescriptor::new("theme", "Theme", |_, cx| {
|
||||
cx.new(|_| ThemeDemo).into()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user