add scrollarea
This commit is contained in:
@@ -27,7 +27,7 @@ full = [
|
|||||||
"avatar",
|
"avatar",
|
||||||
"input",
|
"input",
|
||||||
"popover",
|
"popover",
|
||||||
"tooltip",
|
"scroll_area",
|
||||||
"menu",
|
"menu",
|
||||||
"dialog",
|
"dialog",
|
||||||
"sheet",
|
"sheet",
|
||||||
@@ -44,6 +44,7 @@ progress = []
|
|||||||
avatar = []
|
avatar = []
|
||||||
input = []
|
input = []
|
||||||
popover = []
|
popover = []
|
||||||
|
scroll_area = []
|
||||||
tooltip = []
|
tooltip = []
|
||||||
menu = []
|
menu = []
|
||||||
dialog = []
|
dialog = []
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ pub mod input;
|
|||||||
#[cfg(feature = "popover")]
|
#[cfg(feature = "popover")]
|
||||||
pub mod popover;
|
pub mod popover;
|
||||||
|
|
||||||
|
#[cfg(feature = "scroll_area")]
|
||||||
|
pub mod scroll_area;
|
||||||
|
|
||||||
/// Initializes everything the components need at startup.
|
/// Initializes everything the components need at startup.
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
gpui_native_theme::Theme::init(cx);
|
gpui_native_theme::Theme::init(cx);
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
App, Bounds, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, MouseDownEvent,
|
||||||
|
MouseMoveEvent, ParentElement, Pixels, Refineable as _, RenderOnce, ScrollHandle, SharedString,
|
||||||
|
StatefulInteractiveElement as _, Styled, Window, canvas, div, fill, point,
|
||||||
|
};
|
||||||
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
|
|
||||||
|
/// Which directions the content scrolls in.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub enum ScrollAxes {
|
||||||
|
/// Vertical only; the default.
|
||||||
|
#[default]
|
||||||
|
Vertical,
|
||||||
|
/// Horizontal only.
|
||||||
|
Horizontal,
|
||||||
|
/// Both directions.
|
||||||
|
Both,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scrollable container with an overlay scrollbar.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ScrollArea {
|
||||||
|
id: ElementId,
|
||||||
|
handle: Option<ScrollHandle>,
|
||||||
|
axes: ScrollAxes,
|
||||||
|
a11y_label: Option<SharedString>,
|
||||||
|
style: gpui::StyleRefinement,
|
||||||
|
children: Vec<gpui::AnyElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ScrollArea {
|
||||||
|
fn extend(&mut self, children: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||||
|
self.children.extend(children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for ScrollArea {
|
||||||
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollArea {
|
||||||
|
/// Creates a vertically scrolling area.
|
||||||
|
pub fn new<I>(id: I) -> Self
|
||||||
|
where
|
||||||
|
I: Into<ElementId>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
handle: None,
|
||||||
|
axes: ScrollAxes::Vertical,
|
||||||
|
a11y_label: None,
|
||||||
|
style: gpui::StyleRefinement::default(),
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets which directions the content scrolls in.
|
||||||
|
pub fn axes(mut self, axes: ScrollAxes) -> Self {
|
||||||
|
self.axes = axes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrolls with a handle held on the owning view.
|
||||||
|
pub fn handle(mut self, handle: ScrollHandle) -> Self {
|
||||||
|
self.handle = Some(handle);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the accessibility label of the container.
|
||||||
|
pub fn accessibility_label<L>(mut self, label: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.a11y_label = Some(label.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
let mut pane = div()
|
||||||
|
.id(self.id.clone())
|
||||||
|
.overflow_hidden()
|
||||||
|
.when(self.axes != ScrollAxes::Horizontal, |area| {
|
||||||
|
area.overflow_y_scroll()
|
||||||
|
})
|
||||||
|
.when(self.axes != ScrollAxes::Vertical, |area| {
|
||||||
|
area.overflow_x_scroll()
|
||||||
|
})
|
||||||
|
.when_some(self.handle.clone(), |area, handle| {
|
||||||
|
area.track_scroll(&handle)
|
||||||
|
})
|
||||||
|
.when_some(self.a11y_label, |area, label| area.aria_label(label))
|
||||||
|
.on_scroll(|_, _, cx| {
|
||||||
|
// Keep wheel events from scrolling an outer scroll area too.
|
||||||
|
cx.stop_propagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
pane.style().refine(&self.style);
|
||||||
|
pane.extend(self.children);
|
||||||
|
|
||||||
|
let mut wrapper = div().relative();
|
||||||
|
wrapper.extend(std::iter::once(pane.into_any_element()));
|
||||||
|
|
||||||
|
if let Some(handle) = self.handle {
|
||||||
|
wrapper.extend(std::iter::once(
|
||||||
|
canvas(|_, _, _| {}, {
|
||||||
|
let handle = handle.clone();
|
||||||
|
move |_, _, window, cx| {
|
||||||
|
paint_scrollbar(&handle, window, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.absolute()
|
||||||
|
.inset_0()
|
||||||
|
.into_any_element(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paints the overlay scrollbar thumb.
|
||||||
|
fn paint_scrollbar(handle: &ScrollHandle, window: &mut Window, cx: &mut App) {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let color = theme.colors.scrollbar_thumb;
|
||||||
|
let width = theme.scrollbar.width;
|
||||||
|
let inset = theme.scrollbar.inset;
|
||||||
|
let min_thumb = theme.scrollbar.min_thumb;
|
||||||
|
|
||||||
|
let max_offset = handle.max_offset().y;
|
||||||
|
let bounds = handle.bounds();
|
||||||
|
if max_offset <= Pixels::ZERO || bounds.size.height <= Pixels::ZERO {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let track = bounds.size.height - inset * 2.0;
|
||||||
|
let (thumb_length, thumb_top) = thumb_geometry(
|
||||||
|
bounds.size.height,
|
||||||
|
bounds.size.height + max_offset,
|
||||||
|
handle.offset().y,
|
||||||
|
track,
|
||||||
|
min_thumb,
|
||||||
|
);
|
||||||
|
|
||||||
|
let thumb_x = bounds.right() - width - inset;
|
||||||
|
let thumb_bounds = Bounds::from_corners(
|
||||||
|
point(thumb_x, bounds.top() + inset + thumb_top),
|
||||||
|
point(
|
||||||
|
thumb_x + width,
|
||||||
|
bounds.top() + inset + thumb_top + thumb_length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.paint_quad(fill(thumb_bounds, color).corner_radii(width / 2.0));
|
||||||
|
|
||||||
|
let drag = window.use_keyed_state("scrollbar-thumb-drag", cx, |_, _| {
|
||||||
|
Option::<(Pixels, Pixels)>::None
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on_mouse_event({
|
||||||
|
let handle = handle.clone();
|
||||||
|
let drag = drag.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));
|
||||||
|
});
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on_mouse_event({
|
||||||
|
let handle = handle.clone();
|
||||||
|
let drag = drag.clone();
|
||||||
|
move |event: &MouseMoveEvent, phase, _window, cx| {
|
||||||
|
if phase != DispatchPhase::Bubble
|
||||||
|
|| 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use gpui::{
|
||||||
|
Context, IntoElement, Modifiers, ParentElement as _, Render, ScrollDelta, ScrollEvent,
|
||||||
|
ScrollHandle, SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
|
||||||
|
px,
|
||||||
|
};
|
||||||
|
use gpui_native_core::scroll_area::ScrollArea;
|
||||||
|
use gpui_native_theme::Theme;
|
||||||
|
|
||||||
|
struct Harness {
|
||||||
|
scroll: ScrollHandle,
|
||||||
|
rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Harness {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().size_full().child(
|
||||||
|
ScrollArea::new("test-scroll")
|
||||||
|
.handle(self.scroll.clone())
|
||||||
|
.h(px(200.))
|
||||||
|
.w(px(300.))
|
||||||
|
.child(div().flex().flex_col().children((0..self.rows).map(|row| {
|
||||||
|
div()
|
||||||
|
.h(px(20.))
|
||||||
|
.child(SharedString::from(format!("row {row}")))
|
||||||
|
}))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 300×200 scroll area over `rows` 20px rows, with the handle the test
|
||||||
|
/// can inspect directly.
|
||||||
|
fn scroll_area_window(rows: usize) -> (TestAppContext, VisualTestContext, ScrollHandle) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let handle = ScrollHandle::new();
|
||||||
|
let handle_for_view = handle.clone();
|
||||||
|
let window = cx.add_window(move |_, _| Harness {
|
||||||
|
scroll: handle_for_view,
|
||||||
|
rows,
|
||||||
|
});
|
||||||
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
(cx, visual, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wheel_scrolling_moves_the_offset_and_stays_inside_the_content() {
|
||||||
|
let (_cx, mut visual, handle) = scroll_area_window(30);
|
||||||
|
|
||||||
|
// 30 rows × 20px = 600px of content in a 200px viewport.
|
||||||
|
visual.simulate_event(ScrollEvent {
|
||||||
|
position: point(px(100.), px(100.)),
|
||||||
|
delta: ScrollDelta::Pixels(point(px(0.), px(-60.))),
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let offset = handle.offset().y;
|
||||||
|
assert_eq!(offset, px(-60.));
|
||||||
|
|
||||||
|
// Wheeling far past the end clamps to the content.
|
||||||
|
for _ in 0..10 {
|
||||||
|
visual.simulate_event(ScrollEvent {
|
||||||
|
position: point(px(100.), px(100.)),
|
||||||
|
delta: ScrollDelta::Pixels(point(px(0.), px(-60.))),
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// max_offset = 600 - 200 = 400.
|
||||||
|
assert_eq!(handle.offset().y, px(-400.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_content_starts_unscrolled_and_horizontal_axes_scroll_x() {
|
||||||
|
let (_cx, mut visual, handle) = scroll_area_window(30);
|
||||||
|
assert_eq!(handle.offset(), gpui::point(px(0.), px(0.)));
|
||||||
|
|
||||||
|
// A vertical-only area ignores horizontal wheel deltas.
|
||||||
|
visual.simulate_event(ScrollEvent {
|
||||||
|
position: point(px(100.), px(100.)),
|
||||||
|
delta: ScrollDelta::Pixels(point(px(-60.), px(0.))),
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
assert_eq!(handle.offset().x, px(0.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_thumb_paints_when_the_content_overflows() {
|
||||||
|
let (_cx, _visual, handle) = scroll_area_window(30);
|
||||||
|
// 30 rows × 20px in a 200px viewport: the thumb has something to report.
|
||||||
|
assert!(handle.max_offset().y > px(0.));
|
||||||
|
}
|
||||||
@@ -33,8 +33,10 @@ pub struct ColorTokens {
|
|||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
pub selection: Hsla,
|
pub selection: Hsla,
|
||||||
/// The scrim behind modal layers; translucent in every theme.
|
/// The scrim behind modal layers, translucent in every theme.
|
||||||
pub overlay: Hsla,
|
pub overlay: Hsla,
|
||||||
|
/// The overlay scrollbar thumb, translucent over scrolling content.
|
||||||
|
pub scrollbar_thumb: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorTokens {
|
impl ColorTokens {
|
||||||
@@ -60,6 +62,8 @@ impl ColorTokens {
|
|||||||
ring: hex(0x93c5fd),
|
ring: hex(0x93c5fd),
|
||||||
selection: hex(0xdbeafe),
|
selection: hex(0xdbeafe),
|
||||||
overlay: scrim(0.25),
|
overlay: scrim(0.25),
|
||||||
|
// macOS light overlay thumbs are translucent black.
|
||||||
|
scrollbar_thumb: hsla(0., 0., 0., 0.25),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +89,8 @@ impl ColorTokens {
|
|||||||
ring: hex(0x1d4ed8),
|
ring: hex(0x1d4ed8),
|
||||||
selection: hex(0x1e3a8a),
|
selection: hex(0x1e3a8a),
|
||||||
overlay: scrim(0.45),
|
overlay: scrim(0.45),
|
||||||
|
// macOS dark overlay thumbs are translucent light gray.
|
||||||
|
scrollbar_thumb: hsla(0., 0., 1., 0.3),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub struct ThemeDefinition {
|
|||||||
pub ring: Option<String>,
|
pub ring: Option<String>,
|
||||||
pub selection: Option<String>,
|
pub selection: Option<String>,
|
||||||
pub overlay: Option<String>,
|
pub overlay: Option<String>,
|
||||||
|
pub scrollbar_thumb: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeDefinition {
|
impl ThemeDefinition {
|
||||||
@@ -112,6 +113,10 @@ impl ThemeDefinition {
|
|||||||
pub fn overlay(&self) -> Option<Hsla> {
|
pub fn overlay(&self) -> Option<Hsla> {
|
||||||
parse_hex(self.overlay.as_deref()?)
|
parse_hex(self.overlay.as_deref()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scrollbar_thumb(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.scrollbar_thumb.as_deref()?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses `#rgb` or `#rrggbb` into an opaque color.
|
/// Parses `#rgb` or `#rrggbb` into an opaque color.
|
||||||
|
|||||||
+27
-4
@@ -51,6 +51,8 @@ pub struct Theme {
|
|||||||
pub elevations: Elevations,
|
pub elevations: Elevations,
|
||||||
/// Overlay transition timing; gpui's animation primitives do the rest.
|
/// Overlay transition timing; gpui's animation primitives do the rest.
|
||||||
pub motion: Motion,
|
pub motion: Motion,
|
||||||
|
/// Overlay scrollbar geometry.
|
||||||
|
pub scrollbar: Scrollbar,
|
||||||
/// The platform's native root font size; rem-based sizing scales with it.
|
/// The platform's native root font size; rem-based sizing scales with it.
|
||||||
pub root_font_size: Pixels,
|
pub root_font_size: Pixels,
|
||||||
pub sizes: SizeTable,
|
pub sizes: SizeTable,
|
||||||
@@ -82,8 +84,8 @@ impl Default for Radius {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One drop shadow of a floating layer, transcribed from the platform's
|
/// One drop shadow of a floating layer,
|
||||||
/// native overlay shadows.
|
/// transcribed from the platform's native overlay shadows.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Elevation {
|
pub struct Elevation {
|
||||||
pub offset_y: Pixels,
|
pub offset_y: Pixels,
|
||||||
@@ -149,6 +151,27 @@ impl Default for Motion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Overlay scrollbar geometry, transcribed from the platform's overlay crollbars.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Scrollbar {
|
||||||
|
/// The painted thumb's thickness.
|
||||||
|
pub width: Pixels,
|
||||||
|
/// Edge padding between the thumb and the viewport it floats over.
|
||||||
|
pub inset: Pixels,
|
||||||
|
/// The thumb never shrinks below this length, so it stays grabbable.
|
||||||
|
pub min_thumb: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Scrollbar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
width: px(8.),
|
||||||
|
inset: px(2.),
|
||||||
|
min_thumb: px(24.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A named control size class.
|
/// A named control size class.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum Size {
|
pub enum Size {
|
||||||
@@ -176,8 +199,7 @@ pub struct ControlMetrics {
|
|||||||
pub avatar_size: Pixels,
|
pub avatar_size: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The control metrics for every size class, derived from the theme's root
|
/// The control metrics for every size class.
|
||||||
/// font size so controls scale with the platform's native type.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct SizeTable {
|
pub struct SizeTable {
|
||||||
pub small: ControlMetrics,
|
pub small: ControlMetrics,
|
||||||
@@ -246,6 +268,7 @@ impl Theme {
|
|||||||
radius: resolved.radius,
|
radius: resolved.radius,
|
||||||
elevations: resolved.elevations,
|
elevations: resolved.elevations,
|
||||||
motion: resolved.motion,
|
motion: resolved.motion,
|
||||||
|
scrollbar: resolved.scrollbar,
|
||||||
root_font_size: resolved.root_font_size,
|
root_font_size: resolved.root_font_size,
|
||||||
sizes: resolved.sizes,
|
sizes: resolved.sizes,
|
||||||
accessibility: resolved.accessibility,
|
accessibility: resolved.accessibility,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::definition::ThemeDefinition;
|
use crate::definition::ThemeDefinition;
|
||||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||||
use crate::system::SystemTheme;
|
use crate::system::SystemTheme;
|
||||||
use crate::{ColorTokens, Elevations, Motion, Radius, SizeTable, ThemeVariant};
|
use crate::{ColorTokens, Elevations, Motion, Radius, Scrollbar, SizeTable, ThemeVariant};
|
||||||
use gpui::px;
|
use gpui::px;
|
||||||
|
|
||||||
/// Derived state shades for the colors that get pressed and hovered.
|
/// Derived state shades for the colors that get pressed and hovered.
|
||||||
@@ -21,6 +21,7 @@ pub struct ResolvedTheme {
|
|||||||
pub radius: Radius,
|
pub radius: Radius,
|
||||||
pub elevations: Elevations,
|
pub elevations: Elevations,
|
||||||
pub motion: Motion,
|
pub motion: Motion,
|
||||||
|
pub scrollbar: Scrollbar,
|
||||||
/// The platform's native root font size; the fallback theme uses 16.
|
/// The platform's native root font size; the fallback theme uses 16.
|
||||||
pub root_font_size: gpui::Pixels,
|
pub root_font_size: gpui::Pixels,
|
||||||
pub sizes: SizeTable,
|
pub sizes: SizeTable,
|
||||||
@@ -130,6 +131,11 @@ pub(crate) fn resolve(
|
|||||||
system_palette.overlay,
|
system_palette.overlay,
|
||||||
base.overlay,
|
base.overlay,
|
||||||
),
|
),
|
||||||
|
scrollbar_thumb: pick(
|
||||||
|
def_color(|d| d.scrollbar_thumb()),
|
||||||
|
system_palette.scrollbar_thumb,
|
||||||
|
base.scrollbar_thumb,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let accessibility = system.accessibility;
|
let accessibility = system.accessibility;
|
||||||
@@ -181,6 +187,7 @@ pub(crate) fn resolve(
|
|||||||
radius,
|
radius,
|
||||||
elevations: Elevations::default(),
|
elevations: Elevations::default(),
|
||||||
motion: Motion::default(),
|
motion: Motion::default(),
|
||||||
|
scrollbar: Scrollbar::default(),
|
||||||
root_font_size,
|
root_font_size,
|
||||||
sizes,
|
sizes,
|
||||||
accessibility,
|
accessibility,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ impl MacosSource {
|
|||||||
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
||||||
selection: Self::color_to_hsla(&NSColor::selectedTextBackgroundColor()),
|
selection: Self::color_to_hsla(&NSColor::selectedTextBackgroundColor()),
|
||||||
overlay: None,
|
overlay: None,
|
||||||
|
scrollbar_thumb: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ pub struct PartialPalette {
|
|||||||
pub selection: Option<Hsla>,
|
pub selection: Option<Hsla>,
|
||||||
/// No platform states a scrim color; preset and user files override it.
|
/// No platform states a scrim color; preset and user files override it.
|
||||||
pub overlay: Option<Hsla>,
|
pub overlay: Option<Hsla>,
|
||||||
|
/// No platform states a scrollbar thumb color; presets override it.
|
||||||
|
pub scrollbar_thumb: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A normalized snapshot of the running system's theme.
|
/// A normalized snapshot of the running system's theme.
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ Omarchy `colors.toml` maps almost 1:1: `lighter_background` → `surface`, `dark
|
|||||||
The set stays role-minimal and grows one concept at a time, driven by the unit that needs it. Known planned additions:
|
The set stays role-minimal and grows one concept at a time, driven by the unit that needs it. Known planned additions:
|
||||||
|
|
||||||
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
||||||
|
- ~~**Scrollbar thumb token + metrics**~~ — ✅ landed with the ScrollArea unit (2026-10-29): `colors.scrollbar_thumb` (translucent over content: black in light, light gray in dark; TOML-overridable) and `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
||||||
- **`success` / `warning` roles** — with the first severity-bearing component (Toast, badges, validation); the Omarchy mapping above already anticipates them.
|
- **`success` / `warning` roles** — with the first severity-bearing component (Toast, badges, validation); the Omarchy mapping above already anticipates them.
|
||||||
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
||||||
|
|
||||||
@@ -208,6 +209,7 @@ Each row is one review unit: build it, show it in the gallery, review, then move
|
|||||||
| 8 | core | Progress | Determinate + indeterminate. |
|
| 8 | core | Progress | Determinate + indeterminate. |
|
||||||
| 9 | core | Avatar | Initials; image slot later. |
|
| 9 | core | Avatar | Initials; image slot later. |
|
||||||
| 10 | core | Input (single-line) then multiline, NumberInput | String buffer + `unicode-segmentation` word movement; the field owns its own selection. May seed `crates/editor`. Single-line finishing pass: §6.2. |
|
| 10 | core | Input (single-line) then multiline, NumberInput | String buffer + `unicode-segmentation` word movement; the field owns its own selection. May seed `crates/editor`. Single-line finishing pass: §6.2. |
|
||||||
|
| 11.5 | core | ScrollArea + Scrollbar | ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (`overflow_*_scroll` + caller-held `ScrollHandle`); the component paints an overlay thumb (`scrollbar_thumb` token, `Scrollbar` metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred |
|
||||||
| 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app |
|
| 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app |
|
||||||
| 11+ | core | Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. Menu reuses the popover shell + `Radius.overlay`/`Elevations.popover`; Dialog consumes `colors.overlay` (scrim) + `Elevations.dialog` |
|
| 11+ | core | Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. Menu reuses the popover shell + `Radius.overlay`/`Elevations.popover`; Dialog consumes `colors.overlay` (scrim) + `Elevations.dialog` |
|
||||||
| then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. |
|
| then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use gpui_native_core::link::Link;
|
|||||||
use gpui_native_core::popover::Popover;
|
use gpui_native_core::popover::Popover;
|
||||||
use gpui_native_core::progress::Progress;
|
use gpui_native_core::progress::Progress;
|
||||||
use gpui_native_core::radio::{Radio, RadioGroup};
|
use gpui_native_core::radio::{Radio, RadioGroup};
|
||||||
|
use gpui_native_core::scroll_area::ScrollArea;
|
||||||
use gpui_native_core::slider::Slider;
|
use gpui_native_core::slider::Slider;
|
||||||
use gpui_native_core::switch::Switch;
|
use gpui_native_core::switch::Switch;
|
||||||
use gpui_native_core::toggle::Toggle;
|
use gpui_native_core::toggle::Toggle;
|
||||||
@@ -29,6 +30,7 @@ struct Gallery {
|
|||||||
volume: f32,
|
volume: f32,
|
||||||
input_state: Entity<InputState>,
|
input_state: Entity<InputState>,
|
||||||
popover_open: bool,
|
popover_open: bool,
|
||||||
|
scroll: gpui::ScrollHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Gallery {
|
impl Render for Gallery {
|
||||||
@@ -98,8 +100,7 @@ impl Render for Gallery {
|
|||||||
("destructive", ButtonVariant::Destructive),
|
("destructive", ButtonVariant::Destructive),
|
||||||
];
|
];
|
||||||
|
|
||||||
div()
|
let content = div()
|
||||||
.size_full()
|
|
||||||
.font_family(".SystemUIFont")
|
.font_family(".SystemUIFont")
|
||||||
.bg(colors.background)
|
.bg(colors.background)
|
||||||
.text_color(colors.foreground)
|
.text_color(colors.foreground)
|
||||||
@@ -582,7 +583,14 @@ impl Render for Gallery {
|
|||||||
.map(|role| icon(*role).size(20.0).color(colors.foreground)),
|
.map(|role| icon(*role).size(20.0).color(colors.foreground)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
// The whole gallery scrolls, demonstrating the scroll area at the
|
||||||
|
// page level: the thumb floats over the window's content.
|
||||||
|
ScrollArea::new("gallery-scroll")
|
||||||
|
.handle(self.scroll.clone())
|
||||||
|
.size_full()
|
||||||
|
.child(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +679,7 @@ fn main() {
|
|||||||
volume: 40.0,
|
volume: 40.0,
|
||||||
input_state,
|
input_state,
|
||||||
popover_open: false,
|
popover_open: false,
|
||||||
|
scroll: gpui::ScrollHandle::new(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user