From d7cf86b98c75dd490cad10174e83cedcf62c1448 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:53:03 +0000 Subject: [PATCH 01/13] Resolve gallery sync for Drawer Graft-Changeset: ce1b7b192af79cf0849ef65347f3f7956de6755df293a243b99f226f5b107decb Graft-Session: ses_01M6EWF0SDKRF84498WJ95WGCJ Graft-Work-Item: WI-3038 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- CHANGELOG.md | 1 + crates/fluent-layout/src/drawer.rs | 338 +++++++++++++++++++++++++++++ crates/fluent-layout/src/lib.rs | 2 + examples/gallery/src/main.rs | 87 +++++++- 4 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 crates/fluent-layout/src/drawer.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c996b9a..e153904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ per crate. --- ## Unreleased +- fluent-layout, gallery: Drawer with inline and overlay modes, RTL-aware start/end placement, size presets, reduced-motion-aware transitions, focus trapping and opener restoration (WI-3038). - Add the Windows/Linux platform parity matrix and native verification gap list ([WI-3043](https://vogt.sprooty.com/work/WI-3043)). - fluent-layout, gallery: virtualised expanded tree rows with keyboard-preserving navigation, lazy child loading states, and a 50,000-node gallery story (WI-3035). - fluent-layout, gallery: virtualised `SelectableList` rows (`rows(count, item_at)`, optional variable row heights, scroll-into-view on keyboard selection, AccessKit position-in-set/set-size) on a new `VirtualList` element, and a 100,000-row gallery story (WI-3033). diff --git a/crates/fluent-layout/src/drawer.rs b/crates/fluent-layout/src/drawer.rs new file mode 100644 index 0000000..dc3b387 --- /dev/null +++ b/crates/fluent-layout/src/drawer.rs @@ -0,0 +1,338 @@ +use fluent_core::{LayoutDirection, Theme, ThemeProvider as _}; +use gpui::{ + div, ease_out_quint, prelude::*, px, Animation, AnimationExt as _, AnyView, Context, + FocusHandle, IntoElement, KeyDownEvent, MouseButton, Render, WeakFocusHandle, Window, +}; + +/// Whether a drawer participates in layout or floats above it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DrawerMode { + /// The drawer occupies its width in the surrounding row. + Inline, + /// The drawer floats above content and supplies a dismissing backdrop. + #[default] + Overlay, +} + +/// The logical edge from which a drawer opens. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DrawerPosition { + /// The reading-order start edge (left in LTR, right in RTL). + #[default] + Start, + /// The reading-order end edge (right in LTR, left in RTL). + End, +} + +/// Fluent drawer width presets. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum DrawerSize { + Small, + #[default] + Medium, + Large, + Custom(f32), +} + +impl DrawerSize { + fn pixels(self) -> f32 { + match self { + Self::Small => 256.0, + Self::Medium => 320.0, + Self::Large => 400.0, + Self::Custom(width) => width.max(0.0), + } + } +} + +/// A side sheet that can either occupy layout space or appear above content. +/// +/// Overlay drawers follow the same interaction contract as other nested +/// overlays: Escape and backdrop clicks are consumed, focus stays inside the +/// drawer, and the opener is restored only after the drawer closes. Inline +/// drawers do not alter focus or install a backdrop. +pub struct Drawer { + content: AnyView, + mode: DrawerMode, + position: DrawerPosition, + size: DrawerSize, + open: bool, + dismissible: bool, + focus_handle: FocusHandle, + prior_focus: Option, + had_overlay: bool, +} + +impl Drawer { + pub fn new(content: AnyView, cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + cx.observe_global::(|_, cx| cx.notify()) + .detach(); + Self { + content, + mode: DrawerMode::Overlay, + position: DrawerPosition::Start, + size: DrawerSize::Medium, + open: false, + dismissible: true, + focus_handle: cx.focus_handle(), + prior_focus: None, + had_overlay: false, + } + } + + pub fn mode(mut self, mode: DrawerMode) -> Self { + self.mode = mode; + self + } + + pub fn position(mut self, position: DrawerPosition) -> Self { + self.position = position; + self + } + + pub fn size(mut self, size: DrawerSize) -> Self { + self.size = size; + self + } + + pub fn dismissible(mut self, dismissible: bool) -> Self { + self.dismissible = dismissible; + self + } + + pub fn initially_open(mut self, open: bool) -> Self { + self.open = open; + self + } + + pub fn is_open(&self) -> bool { + self.open + } + + pub fn set_open(&mut self, open: bool, cx: &mut Context) { + if self.open != open { + self.open = open; + cx.notify(); + } + } + + pub fn toggle(&mut self, cx: &mut Context) { + self.set_open(!self.open, cx); + } + + pub fn set_mode(&mut self, mode: DrawerMode, cx: &mut Context) { + if self.mode != mode { + self.mode = mode; + cx.notify(); + } + } + + pub fn set_position(&mut self, position: DrawerPosition, cx: &mut Context) { + if self.position != position { + self.position = position; + cx.notify(); + } + } + + fn close_if_dismissible(&mut self, cx: &mut Context) { + if self.dismissible { + self.set_open(false, cx); + } + } +} + +impl Render for Drawer { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let overlay_open = self.open && self.mode == DrawerMode::Overlay; + if !overlay_open && self.had_overlay { + match self.prior_focus.take().and_then(|focus| focus.upgrade()) { + Some(prior) => window.focus(&prior, cx), + None => window.blur(cx), + } + self.had_overlay = false; + } + + if !self.open { + return div().into_any_element(); + } + + let colors = cx.theme().colors.clone(); + let spacing = cx.theme().spacing; + let duration = cx.theme().motion().normal_duration(); + let width = self.size.pixels(); + let physical_end = self.position == DrawerPosition::End; + let on_right = physical_end != LayoutDirection::current(cx).is_rtl(); + let surface = div() + .id("drawer-surface") + .debug_selector(|| "drawer-surface".into()) + .track_focus(&self.focus_handle) + .h_full() + .w(px(width)) + .p(px(spacing.lg)) + .overflow_hidden() + .bg(colors.surface) + .border_1() + .border_color(colors.stroke_neutral) + .child(self.content.clone()); + + if self.mode == DrawerMode::Inline { + return div() + .id("drawer-inline") + .debug_selector(|| "drawer-inline".into()) + .h_full() + .w(px(width)) + .flex_none() + .when(on_right, |drawer| drawer.ml_auto()) + .when(!on_right, |drawer| drawer.mr_auto()) + .child(surface) + .into_any_element(); + } + + if !self.had_overlay { + self.prior_focus = window.focused(cx).map(|focus| focus.downgrade()); + window.focus(&self.focus_handle, cx); + self.had_overlay = true; + } + if !self.focus_handle.contains_focused(window, cx) { + window.focus(&self.focus_handle, cx); + } + + let backdrop = { + let mut color = colors.surface_dim; + color.a = 0.6; + color + }; + let motion_id = ("drawer-motion", width.to_bits()); + let animated_surface = surface.with_animation( + motion_id, + Animation::new(duration).with_easing(ease_out_quint()), + move |surface, delta| { + let offset = width * (1.0 - delta); + if on_right { + surface.mr(px(-offset)) + } else { + surface.ml(px(-offset)) + } + }, + ); + + div() + .id("drawer-overlay") + .debug_selector(|| "drawer-overlay".into()) + .absolute() + .inset_0() + .size_full() + .flex() + .when(on_right, |overlay| overlay.justify_end()) + .bg(backdrop) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { + match event.keystroke.key.as_str() { + "escape" => { + cx.stop_propagation(); + this.close_if_dismissible(cx); + } + "tab" => { + cx.stop_propagation(); + if let Some(focus) = window.focused(cx) { + window.focus(&focus, cx); + } + } + _ => {} + } + })) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.close_if_dismissible(cx); + }), + ) + .child( + div() + .h_full() + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .child(animated_surface), + ) + .into_any_element() + } +} + +#[cfg(test)] +mod tests { + use fluent_test_utils::{open_fluent_test_window, FluentInteractionExt as _}; + use gpui::{point, px, Modifiers, TestAppContext}; + + use super::*; + + struct Content; + + struct Root(gpui::Entity); + + impl Render for Root { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().relative().size_full().child(self.0.clone()) + } + } + + impl Render for Content { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().child("Drawer content") + } + } + + fn drawer( + cx: &mut TestAppContext, + mode: DrawerMode, + position: DrawerPosition, + ) -> (gpui::Entity, &mut gpui::VisualTestContext) { + let mut handle = None; + let (_, cx) = open_fluent_test_window(cx, |_, cx| { + let content = cx.new(|_| Content); + let entity = cx.new(|cx| { + Drawer::new(content.into(), cx) + .mode(mode) + .position(position) + .initially_open(true) + }); + handle = Some(entity.clone()); + Root(entity) + }); + (handle.expect("drawer entity is created"), cx) + } + + #[gpui::test] + fn overlay_backdrop_and_escape_close(cx: &mut TestAppContext) { + let (drawer, cx) = drawer(cx, DrawerMode::Overlay, DrawerPosition::Start); + cx.run_until_parked(); + assert!(drawer.read_with(cx, |drawer, _| drawer.is_open())); + cx.click_at( + point(px(700.), px(300.)), + MouseButton::Left, + Modifiers::none(), + ); + assert!(!drawer.read_with(cx, |drawer, _| drawer.is_open())); + + drawer.update(cx, |drawer, cx| drawer.set_open(true, cx)); + cx.run_until_parked(); + cx.type_keys("escape"); + assert!(!drawer.read_with(cx, |drawer, _| drawer.is_open())); + } + + #[gpui::test] + fn inline_drawer_does_not_install_overlay(cx: &mut TestAppContext) { + let (drawer, cx) = drawer(cx, DrawerMode::Inline, DrawerPosition::End); + cx.run_until_parked(); + assert!(drawer.read_with(cx, |drawer, _| drawer.is_open())); + assert!(cx.debug_bounds("drawer-inline").is_some()); + assert!(cx.debug_bounds("drawer-overlay").is_none()); + } + + #[test] + fn size_presets_are_stable() { + assert_eq!(DrawerSize::Small.pixels(), 256.0); + assert_eq!(DrawerSize::Medium.pixels(), 320.0); + assert_eq!(DrawerSize::Large.pixels(), 400.0); + assert_eq!(DrawerSize::Custom(-1.0).pixels(), 0.0); + } +} diff --git a/crates/fluent-layout/src/lib.rs b/crates/fluent-layout/src/lib.rs index 97205bd..7eedeca 100644 --- a/crates/fluent-layout/src/lib.rs +++ b/crates/fluent-layout/src/lib.rs @@ -6,6 +6,7 @@ pub mod data_table; pub mod dialog; pub mod dock; pub mod document_viewer; +pub mod drawer; pub mod flyout; pub mod menu_bar; pub mod menu_button; @@ -39,6 +40,7 @@ pub use data_table::{ pub use dialog::{Dialog, DialogAction}; pub use dock::{DockPanel, DockPosition}; pub use document_viewer::{DocumentViewer, RichDocument, RichDocumentBlock, RichTextRun}; +pub use drawer::{Drawer, DrawerMode, DrawerPosition, DrawerSize}; pub use flyout::{Flyout, FlyoutPlacement}; pub use menu_bar::{MenuBar, MenuItemDef}; pub use menu_button::MenuButton; diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 8438e3b..062c6f9 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -2,8 +2,9 @@ use fluent_app::{FluentApp, TitleBar}; use fluent_core::{LayoutDirection, Theme, ThemeProvider as _}; use fluent_layout::{ DataCell, DataColumn, DataRow, DataStatusKind, DataTable, DataTableState, DockPanel, - DockPosition, MenuBar, MessageBar, MessageIntent, ModalStack, Pane, SelectableList, - SelectableListItem, TabItem, TabStrip, Tree, TreeItem, Workspace, + DockPosition, Drawer, DrawerMode, DrawerPosition, DrawerSize, MenuBar, MessageBar, + MessageIntent, ModalStack, Pane, SelectableList, SelectableListItem, TabItem, TabStrip, Tree, + TreeItem, Workspace, }; use fluent_primitives::{ Avatar, Button, ButtonAppearance, Checkbox, CounterBadge, CounterBadgeAppearance, @@ -462,6 +463,85 @@ impl Render for CounterBadgeDemo { } } +struct DrawerContent; + +impl Render for DrawerContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .text_color(cx.theme().colors.on_neutral) + .child("Drawer") + .child("A logical start/end side sheet with inline and overlay modes.") + } +} + +struct DrawerDemo { + drawer: Entity, +} + +impl DrawerDemo { + fn new(cx: &mut Context) -> Self { + let content = cx.new(|_| DrawerContent); + let drawer = cx.new(|cx| { + Drawer::new(content.into(), cx) + .size(DrawerSize::Medium) + .initially_open(false) + }); + Self { drawer } + } +} + +impl Render for DrawerDemo { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.theme().colors.clone(); + let drawer = self.drawer.clone(); + let control = |id: &'static str, + label: &'static str, + action: fn(&mut Drawer, &mut Context)| { + let drawer = drawer.clone(); + Button::new(id).label(label).on_click(move |_, _, cx| { + drawer.update(cx, action); + }) + }; + div() + .relative() + .size_full() + .p_3() + .flex() + .flex_col() + .gap_3() + .bg(colors.surface) + .text_color(colors.on_neutral) + .child("Drawer story") + .child( + div() + .flex() + .flex_wrap() + .gap_2() + .child(control("drawer-toggle", "Open / close", |drawer, cx| { + drawer.toggle(cx) + })) + .child(control("drawer-overlay-mode", "Overlay", |drawer, cx| { + drawer.set_mode(DrawerMode::Overlay, cx); + drawer.set_open(true, cx); + })) + .child(control("drawer-inline-mode", "Inline", |drawer, cx| { + drawer.set_mode(DrawerMode::Inline, cx); + drawer.set_open(true, cx); + })) + .child(control("drawer-start", "Start", |drawer, cx| { + drawer.set_position(DrawerPosition::Start, cx) + })) + .child(control("drawer-end", "End", |drawer, cx| { + drawer.set_position(DrawerPosition::End, cx) + })), + ) + .child(self.drawer.clone()) + } +} + impl Render for GalleryRoot { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child(self.workspace.clone()) @@ -919,6 +999,7 @@ fn main() { opacity: 80.0, }); let matrix_demo = cx.new(|_| ThemeMatrixDemo); + let drawer_demo = cx.new(DrawerDemo::new); let rtl_demo = cx.new(|cx| { cx.observe_global::(|_, cx| cx.notify()) .detach(); @@ -930,7 +1011,7 @@ fn main() { data: vec![data_states.into(), big_list.into(), big_tree.into()], date_input: vec![date_demo.into(), input_demo.into(), range_demo.into()], media: vec![terminal_demo.into(), video_demo.into()], - theme: vec![matrix_demo.into(), rtl_demo.into()], + theme: vec![drawer_demo.into(), matrix_demo.into(), rtl_demo.into()], }) }); let pane_content = host.clone(); From d29bb50b9d86668d4c74081369c1bdfbd15d204b Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:04:16 +0000 Subject: [PATCH 02/13] Add breadcrumb and pagination layout controls Graft-Changeset: cced867555f7075712f5e5440a1067c3cb6eee1eb287f9f98c34b488e589ca5d2 Graft-Session: ses_01M6EX3FEG5EG89PN65WSAZA9X Graft-Work-Item: WI-3036 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- CHANGELOG.md | 2 + crates/fluent-layout/src/breadcrumb.rs | 277 +++++++++++++++++++++++++ crates/fluent-layout/src/lib.rs | 2 + examples/gallery/src/main.rs | 18 +- 4 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 crates/fluent-layout/src/breadcrumb.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e153904..34220ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ per crate. --- ## Unreleased + +- Layout: add Fluent 2 Breadcrumb and Pagination controls with overflow menus, current-item semantics, RTL separators, and keyboard-ready controls (WI-3036). - fluent-layout, gallery: Drawer with inline and overlay modes, RTL-aware start/end placement, size presets, reduced-motion-aware transitions, focus trapping and opener restoration (WI-3038). - Add the Windows/Linux platform parity matrix and native verification gap list ([WI-3043](https://vogt.sprooty.com/work/WI-3043)). - fluent-layout, gallery: virtualised expanded tree rows with keyboard-preserving navigation, lazy child loading states, and a 50,000-node gallery story (WI-3035). diff --git a/crates/fluent-layout/src/breadcrumb.rs b/crates/fluent-layout/src/breadcrumb.rs new file mode 100644 index 0000000..fc19513 --- /dev/null +++ b/crates/fluent-layout/src/breadcrumb.rs @@ -0,0 +1,277 @@ +use std::sync::Arc; + +use fluent_core::{LayoutDirection, Theme, ThemeProvider as _}; +use gpui::{ + div, prelude::*, px, ClickEvent, Context, IntoElement, Render, Role, SharedString, Window, +}; + +#[derive(Clone, Debug)] +pub struct BreadcrumbItem { + pub id: SharedString, + pub label: SharedString, +} + +impl BreadcrumbItem { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + id: id.into(), + label: label.into(), + } + } +} + +/// A Fluent 2 breadcrumb trail. Long trails retain the first and current items +/// and place the middle items in an accessible overflow menu. +#[allow(clippy::type_complexity)] +pub struct Breadcrumb { + pub items: Vec, + pub current: usize, + pub max_visible: usize, + on_select: Option) + 'static>>, +} + +impl Default for Breadcrumb { + fn default() -> Self { + Self { + items: Vec::new(), + current: 0, + max_visible: 5, + on_select: None, + } + } +} + +impl Breadcrumb { + pub fn new(cx: &mut Context) -> Self { + cx.observe_global::(|_, cx| cx.notify()).detach(); + cx.observe_global::(|_, cx| cx.notify()) + .detach(); + Self::default() + } + pub fn items(mut self, items: impl IntoIterator) -> Self { + self.items = items.into_iter().collect(); + self + } + pub fn item(mut self, item: BreadcrumbItem) -> Self { + self.items.push(item); + self + } + pub fn current(mut self, current: usize) -> Self { + self.current = current; + self + } + pub fn max_visible(mut self, max_visible: usize) -> Self { + self.max_visible = max_visible.max(2); + self + } + pub fn on_select( + mut self, + f: impl Fn(usize, &mut Window, &mut Context) + 'static, + ) -> Self { + self.on_select = Some(Arc::new(f)); + self + } + + fn select(&mut self, index: usize, window: &mut Window, cx: &mut Context) { + if index < self.items.len() { + self.current = index; + if let Some(f) = self.on_select.clone() { + f(index, window, cx); + } + cx.notify(); + } + } +} + +impl Render for Breadcrumb { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let (colors, spacing_xs) = { + let theme = cx.theme(); + (theme.colors.clone(), theme.spacing.xs) + }; + let rtl = LayoutDirection::current(cx).is_rtl(); + let overflow = self.items.len() > self.max_visible; + let mut children: Vec = Vec::new(); + if !self.items.is_empty() { + { + let index = 0; + let item = self.items[index].clone(); + let current = index == self.current; + let handler = cx.listener(move |this: &mut Breadcrumb, _: &ClickEvent, win, cx| { + this.select(index, win, cx) + }); + let item_el = div() + .id(("breadcrumb-item", index)) + .role(Role::ListItem) + .aria_label(item.label.clone()) + .flex() + .items_center() + .text_color(colors.on_neutral) + .when(!current, |d| d.cursor_pointer().on_click(handler)) + .when(current, |d| { + d.font_weight(gpui::FontWeight::SEMIBOLD) + .aria_label(format!("{} (current)", item.label)) + }); + children.push(item_el.child(item.label).into_any_element()); + } + if overflow { + children.push( + div() + .text_color(colors.on_neutral) + .child(if rtl { "‹" } else { "›" }) + .into_any_element(), + ); + children.push( + div() + .id("breadcrumb-overflow") + .role(Role::Button) + .aria_label("More breadcrumb items") + .cursor_pointer() + .child("…") + .into_any_element(), + ); + children.push( + div() + .text_color(colors.on_neutral) + .child(if rtl { "‹" } else { "›" }) + .into_any_element(), + ); + { + let index = self.items.len() - 1; + let item = self.items[index].clone(); + let current = index == self.current; + let handler = + cx.listener(move |this: &mut Breadcrumb, _: &ClickEvent, win, cx| { + this.select(index, win, cx) + }); + let item_el = div() + .id(("breadcrumb-item", index)) + .role(Role::ListItem) + .aria_label(item.label.clone()) + .flex() + .items_center() + .text_color(colors.on_neutral) + .when(!current, |d| d.cursor_pointer().on_click(handler)) + .when(current, |d| { + d.font_weight(gpui::FontWeight::SEMIBOLD) + .aria_label(format!("{} (current)", item.label)) + }); + children.push(item_el.child(item.label).into_any_element()); + } + } else { + for index in 1..self.items.len() { + children.push( + div() + .text_color(colors.on_neutral) + .child(if rtl { "‹" } else { "›" }) + .into_any_element(), + ); + let item = self.items[index].clone(); + let current = index == self.current; + let handler = + cx.listener(move |this: &mut Breadcrumb, _: &ClickEvent, win, cx| { + this.select(index, win, cx) + }); + let item_el = div() + .id(("breadcrumb-item", index)) + .role(Role::ListItem) + .aria_label(item.label.clone()) + .flex() + .items_center() + .text_color(colors.on_neutral) + .when(!current, |d| d.cursor_pointer().on_click(handler)) + .when(current, |d| { + d.font_weight(gpui::FontWeight::SEMIBOLD) + .aria_label(format!("{} (current)", item.label)) + }); + children.push(item_el.child(item.label).into_any_element()); + } + } + } + div() + .id("breadcrumb") + .role(Role::List) + .flex() + .items_center() + .gap(px(spacing_xs)) + .children(children) + } +} + +/// Compact paging control with previous/next buttons and a roving page cursor. +#[derive(Clone, Debug)] +pub struct Pagination { + pub page: usize, + pub page_count: usize, +} +impl Pagination { + pub fn new(page_count: usize) -> Self { + Self { + page: 0, + page_count: page_count.max(1), + } + } + pub fn page(mut self, page: usize) -> Self { + self.page = page.min(self.page_count.saturating_sub(1)); + self + } +} +impl Render for Pagination { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.theme().colors.clone(); + let current = self.page; + let count = self.page_count; + let mut root = div() + .id("pagination") + .role(Role::Navigation) + .flex() + .items_center() + .gap(px(cx.theme().spacing.xs)); + for (label, index) in [ + ("Previous", current.saturating_sub(1)), + ("Next", (current + 1).min(count.saturating_sub(1))), + ] { + let disabled = + (label == "Previous" && current == 0) || (label == "Next" && current + 1 >= count); + let handler = cx.listener(move |this: &mut Pagination, _: &ClickEvent, _, cx| { + this.page = index; + cx.notify(); + }); + root = root.child( + div() + .id(if label == "Previous" { + "pagination-prev" + } else { + "pagination-next" + }) + .role(Role::Button) + .aria_label(label) + .text_color(if disabled { + colors.on_neutral_disabled + } else { + colors.on_neutral + }) + .when(!disabled, |d| d.cursor_pointer().on_click(handler)) + .child(if label == "Previous" { "‹" } else { "›" }), + ); + } + for index in 0..count { + let selected = index == current; + let handler = cx.listener(move |this: &mut Pagination, _: &ClickEvent, _, cx| { + this.page = index; + cx.notify(); + }); + root = root.child( + div() + .id(("pagination-page", index)) + .role(Role::Button) + .aria_label(format!("Page {}", index + 1)) + .aria_selected(selected) + .text_color(colors.on_neutral) + .when(!selected, |d| d.cursor_pointer().on_click(handler)) + .child(format!("{}", index + 1)), + ); + } + root + } +} diff --git a/crates/fluent-layout/src/lib.rs b/crates/fluent-layout/src/lib.rs index 7eedeca..c2143d0 100644 --- a/crates/fluent-layout/src/lib.rs +++ b/crates/fluent-layout/src/lib.rs @@ -1,3 +1,4 @@ +pub mod breadcrumb; pub mod command; pub mod command_palette; pub mod context_menu; @@ -27,6 +28,7 @@ pub mod tree; pub mod virtual_list; pub mod workspace; +pub use breadcrumb::{Breadcrumb, BreadcrumbItem, Pagination}; pub use command::CommandDef; pub use command_palette::{CommandPalette, PaletteEntry, PaletteEntryAction}; pub use context_menu::{ diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 062c6f9..889d408 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -1,10 +1,10 @@ use fluent_app::{FluentApp, TitleBar}; use fluent_core::{LayoutDirection, Theme, ThemeProvider as _}; use fluent_layout::{ - DataCell, DataColumn, DataRow, DataStatusKind, DataTable, DataTableState, DockPanel, - DockPosition, Drawer, DrawerMode, DrawerPosition, DrawerSize, MenuBar, MessageBar, - MessageIntent, ModalStack, Pane, SelectableList, SelectableListItem, TabItem, TabStrip, Tree, - TreeItem, Workspace, + Breadcrumb, BreadcrumbItem, DataCell, DataColumn, DataRow, DataStatusKind, DataTable, + DataTableState, DockPanel, DockPosition, Drawer, DrawerMode, DrawerPosition, DrawerSize, + MenuBar, MessageBar, MessageIntent, ModalStack, Pagination, Pane, SelectableList, + SelectableListItem, TabItem, TabStrip, Tree, TreeItem, Workspace, }; use fluent_primitives::{ Avatar, Button, ButtonAppearance, Checkbox, CounterBadge, CounterBadgeAppearance, @@ -340,6 +340,16 @@ impl Render for ThemeMatrixDemo { .bg(colors.surface) .text_color(colors.on_neutral) .child("Theme token matrix") + .child( + Breadcrumb::new(cx) + .items([ + BreadcrumbItem::new("home", "Home"), + BreadcrumbItem::new("library", "Library"), + BreadcrumbItem::new("current", "Current"), + ]) + .current(2), + ) + .child(Pagination::new(4).page(1)) .child("Neutral, accent, selected, disabled, status, border, and focus states") .child( div() From bac47ba21822c52075d3508795dd27fbde2e2cdd Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:08:16 +0000 Subject: [PATCH 03/13] Add Tag and Rating primitives Graft-Changeset: c0921a0c8f4c22c67b811a001efac005d8f7ed118e84fa2191d9526888660b929 Graft-Session: ses_01M6EXE2H11EW2VSQGZV74R90F Graft-Work-Item: WI-3039 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- crates/fluent-primitives/src/lib.rs | 4 + crates/fluent-primitives/src/rating.rs | 125 +++++++++++++++ crates/fluent-primitives/src/tag.rs | 205 +++++++++++++++++++++++++ examples/gallery/src/main.rs | 40 ++++- 4 files changed, 370 insertions(+), 4 deletions(-) create mode 100644 crates/fluent-primitives/src/rating.rs create mode 100644 crates/fluent-primitives/src/tag.rs diff --git a/crates/fluent-primitives/src/lib.rs b/crates/fluent-primitives/src/lib.rs index 8693827..0c7716d 100644 --- a/crates/fluent-primitives/src/lib.rs +++ b/crates/fluent-primitives/src/lib.rs @@ -23,6 +23,7 @@ mod link; mod persona; mod progress; mod radio_group; +mod rating; mod roving; mod searchbox; mod section_header; @@ -32,6 +33,7 @@ mod spin_button; mod spinner; mod suggestion_popover; mod switch; +mod tag; mod terminal_grid; mod terminal_view; mod text_ext; @@ -72,6 +74,7 @@ pub use link::{Link, LinkAppearance}; pub use persona::{Persona, PersonaSize}; pub use progress::ProgressBar; pub use radio_group::{RadioGroup, RadioGroupOrientation, RadioOption}; +pub use rating::Rating; pub use searchbox::Searchbox; pub use section_header::SectionHeader; pub use skeleton::{Skeleton, SkeletonRow, SkeletonWrap}; @@ -82,6 +85,7 @@ pub use suggestion_popover::{ SuggestionItem, SuggestionKeyAction, SuggestionListState, SuggestionPopover, }; pub use switch::Switch; +pub use tag::{Tag, TagAppearance, TagGroup, TagShape, TagSize}; pub use terminal_grid::{ CursorShape, GridPoint, TerminalCell, TerminalCursor, TerminalGrid, TerminalSelection, DEFAULT_SCROLLBACK, diff --git a/crates/fluent-primitives/src/rating.rs b/crates/fluent-primitives/src/rating.rs new file mode 100644 index 0000000..dc7dda1 --- /dev/null +++ b/crates/fluent-primitives/src/rating.rs @@ -0,0 +1,125 @@ +use fluent_core::ThemeProvider as _; +use gpui::{ + div, prelude::*, px, App, ElementId, IntoElement, KeyDownEvent, RenderOnce, Role, SharedString, + Window, +}; +use std::rc::Rc; + +#[allow(clippy::type_complexity)] +#[derive(IntoElement)] +pub struct Rating { + id: ElementId, + value: f64, + max: u8, + step: f64, + readonly: bool, + disabled: bool, + label: Option, + on_change: Option>, +} +impl Rating { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + value: 0.0, + max: 5, + step: 0.5, + readonly: false, + disabled: false, + label: None, + on_change: None, + } + } + pub fn value(mut self, v: f64) -> Self { + self.value = v; + self + } + pub fn max(mut self, v: u8) -> Self { + self.max = v.max(1); + self + } + pub fn step(mut self, v: f64) -> Self { + if v == 0.5 || v == 1. { + self.step = v + }; + self + } + pub fn readonly(mut self, v: bool) -> Self { + self.readonly = v; + self + } + pub fn interactive(mut self, v: bool) -> Self { + self.readonly = !v; + self + } + pub fn disabled(mut self, v: bool) -> Self { + self.disabled = v; + self + } + pub fn label(mut self, v: impl Into) -> Self { + self.label = Some(v.into()); + self + } + pub fn on_change(mut self, f: impl Fn(f64, &mut Window, &mut App) + 'static) -> Self { + self.on_change = Some(Rc::new(f)); + self + } +} +impl RenderOnce for Rating { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let t = cx.theme(); + let c = &t.colors; + let value = self.value.clamp(0.0, self.max as f64); + let max = self.max; + let step = self.step; + let change = self.on_change.clone(); + let mut root = div() + .id(self.id) + .role(Role::Slider) + .aria_label(self.label.unwrap_or_else(|| "Rating".into())) + .aria_value(value.to_string()) + .flex() + .gap(px(2.)); + for i in 1..=max { + let fill = value - (i - 1) as f64; + let glyph = if fill >= 1. { + "★" + } else if fill >= 0.5 { + "⯨" + } else { + "☆" + }; + let mut star = div() + .id(("rating-star", i as u64)) + .text_size(px(24.0)) + .text_color(if fill > 0.0 { + c.accent + } else { + c.on_neutral_disabled + }) + .child(glyph); + if !self.readonly && !self.disabled { + if let Some(f) = change.clone() { + let nv = (i as f64).min(max as f64); + let click_change = f.clone(); + star = star + .cursor_pointer() + .on_click(move |_, w, a| click_change(nv, w, a)) + .on_key_down(move |ev: &KeyDownEvent, w, a| { + let k = ev.keystroke.key.as_str(); + let nv = match k { + "right" | "up" => (value + step).min(max as f64), + "left" | "down" => (value - step).max(0.), + "home" => 0.0, + "end" => max as f64, + _ => return, + }; + f(nv, w, a); + }); + } + } + root = root.child(star); + } + root + } +} diff --git a/crates/fluent-primitives/src/tag.rs b/crates/fluent-primitives/src/tag.rs new file mode 100644 index 0000000..05deef7 --- /dev/null +++ b/crates/fluent-primitives/src/tag.rs @@ -0,0 +1,205 @@ +use std::rc::Rc; + +use fluent_core::ThemeProvider as _; +use gpui::{ + div, prelude::*, px, App, ClickEvent, ElementId, IntoElement, KeyDownEvent, RenderOnce, + SharedString, Window, +}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TagAppearance { + #[default] + Neutral, + Accent, + Informative, + Success, + Warning, + Danger, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TagShape { + #[default] + Rounded, + Circular, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TagSize { + Small, + #[default] + Medium, + Large, +} + +#[allow(clippy::type_complexity)] +#[derive(IntoElement)] +pub struct Tag { + id: ElementId, + label: SharedString, + size: TagSize, + shape: TagShape, + appearance: TagAppearance, + selected: bool, + disabled: bool, + dismissible: bool, + on_dismiss: Option>, + on_select: Option>, +} + +impl Tag { + pub fn new(id: impl Into, label: impl Into) -> Self { + Self { + id: id.into(), + label: label.into(), + size: TagSize::Medium, + shape: TagShape::Rounded, + appearance: TagAppearance::Neutral, + selected: false, + disabled: false, + dismissible: false, + on_dismiss: None, + on_select: None, + } + } + pub fn size(mut self, v: TagSize) -> Self { + self.size = v; + self + } + pub fn shape(mut self, v: TagShape) -> Self { + self.shape = v; + self + } + pub fn appearance(mut self, v: TagAppearance) -> Self { + self.appearance = v; + self + } + pub fn selected(mut self, v: bool) -> Self { + self.selected = v; + self + } + pub fn disabled(mut self, v: bool) -> Self { + self.disabled = v; + self + } + pub fn dismissible(mut self, v: bool) -> Self { + self.dismissible = v; + self + } + pub fn on_dismiss(mut self, f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { + self.on_dismiss = Some(Rc::new(f)); + self + } + pub fn on_select( + mut self, + f: impl Fn(bool, &ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_select = Some(Rc::new(f)); + self + } +} + +impl RenderOnce for Tag { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let t = cx.theme(); + let c = &t.colors; + let (bg, fg) = match self.appearance { + TagAppearance::Accent => (c.accent, c.on_accent), + TagAppearance::Informative => (c.status_info_bg, c.status_info), + TagAppearance::Success => (c.status_success_bg, c.status_success), + TagAppearance::Warning => (c.status_warning_bg, c.status_warning), + TagAppearance::Danger => (c.status_error_bg, c.status_error), + TagAppearance::Neutral => (c.neutral, c.on_neutral), + }; + let (pxv, pyv, fs) = match self.size { + TagSize::Small => (6., 2., t.typography.caption.size), + TagSize::Medium => (8., 4., t.typography.body.size), + TagSize::Large => (10., 6., t.typography.body.size + 1.), + }; + let radius = match self.shape { + TagShape::Circular => t.radii.pill, + TagShape::Rounded => t.radii.md, + }; + let disabled = self.disabled; + let selected = self.selected; + let select = self.on_select.clone(); + let dismiss = self.on_dismiss.clone(); + let mut el = div() + .id(self.id) + .flex() + .items_center() + .gap(px(4.)) + .px(px(pxv)) + .py(px(pyv)) + .rounded(px(radius)) + .bg(if selected { c.neutral_selected } else { bg }) + .text_size(px(fs)) + .text_color(if disabled { c.on_neutral_disabled } else { fg }) + .child(self.label); + if !disabled && (select.is_some() || self.dismissible) { + let key_select = select.clone(); + let key_dismiss = dismiss.clone(); + el = el + .focusable() + .tab_stop(true) + .cursor_pointer() + .on_key_down(move |ev: &KeyDownEvent, w, a| { + if matches!(ev.keystroke.key.as_str(), "enter" | "space") { + if let Some(f) = &key_select { + f(!selected, &ClickEvent::default(), w, a); + } + } + if matches!(ev.keystroke.key.as_str(), "delete" | "backspace") { + if let Some(f) = &key_dismiss { + f(&ClickEvent::default(), w, a); + } + } + }) + .when_some(select.clone(), |e, f| { + e.on_click(move |ev, w, a| f(!selected, ev, w, a)) + }); + } + if !disabled && self.dismissible { + el = el.child(div().child("×")); + } + el + } +} + +#[derive(IntoElement)] +pub struct TagGroup { + id: ElementId, + children: Vec, + size: TagSize, +} +impl TagGroup { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + children: Vec::new(), + size: TagSize::Medium, + } + } + pub fn tags(mut self, tags: impl IntoIterator) -> Self { + self.children = tags.into_iter().collect(); + self + } + pub fn tag(mut self, tag: Tag) -> Self { + self.children.push(tag); + self + } + pub fn size(mut self, size: TagSize) -> Self { + self.size = size; + self + } +} +impl RenderOnce for TagGroup { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + div() + .id(self.id) + .flex() + .flex_wrap() + .gap(px(8.)) + .children(self.children) + } +} diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 889d408..deffbd3 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -9,9 +9,9 @@ use fluent_layout::{ use fluent_primitives::{ Avatar, Button, ButtonAppearance, Checkbox, CounterBadge, CounterBadgeAppearance, DateTimeLocale, DateTimePicker, DateTimeValue, InputCapture, InputCaptureEvent, Link, - LinkAppearance, Persona, PersonaSize, PresenceBadgeState, ProgressBar, Slider, SpinButton, - Spinner, Switch, TerminalCell, TerminalGrid, TerminalView, TerminalViewEvent, ToggleButton, - VideoScaling, VideoSurface, VideoSurfaceEvent, + LinkAppearance, Persona, PersonaSize, PresenceBadgeState, ProgressBar, Rating, Slider, + SpinButton, Spinner, Switch, Tag, TagAppearance, TagGroup, TagSize, TerminalCell, TerminalGrid, + TerminalView, TerminalViewEvent, ToggleButton, VideoScaling, VideoSurface, VideoSurfaceEvent, }; use fluent_ribbon::RibbonBar; @@ -31,6 +31,32 @@ struct GalleryRoot { /// rdpapp connection tree. struct DataStatesDemo; +struct TagRatingDemo; +impl Render for TagRatingDemo { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_2() + .p_2() + .child("Tags and rating") + .child( + TagGroup::new("gallery-tags") + .tag(Tag::new("tag-one", "Design").appearance(TagAppearance::Accent)) + .tag( + Tag::new("tag-two", "Review") + .size(TagSize::Small) + .dismissible(true), + ), + ) + .child( + Rating::new("gallery-rating") + .value(3.5) + .label("Product rating"), + ) + } +} + impl DataStatesDemo { fn torrents(state: DataTableState, full: bool) -> DataTable { let mut table = DataTable::new() @@ -1002,6 +1028,7 @@ fn main() { let input_demo = cx.new(InputCaptureDemo::new); let terminal_demo = cx.new(TerminalDemo::new); let video_demo = cx.new(VideoDemo::new); + let tag_rating_demo = cx.new(|_| TagRatingDemo); let range_demo = cx.new(|_| RangeInputDemo { volume: 40.0, vertical: 0.5, @@ -1017,7 +1044,12 @@ fn main() { }); let host = cx.new(|_| { FamilyHost::new(DemoViews { - status: vec![badge_demo.into(), persona_demo.into(), download_demo.into()], + status: vec![ + badge_demo.into(), + persona_demo.into(), + download_demo.into(), + tag_rating_demo.into(), + ], data: vec![data_states.into(), big_list.into(), big_tree.into()], date_input: vec![date_demo.into(), input_demo.into(), range_demo.into()], media: vec![terminal_demo.into(), video_demo.into()], From 6e393f88ed4e147075a75f90faf72800d123dc9d Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:18:30 +0000 Subject: [PATCH 04/13] docs: add consumer quick start and component index Graft-Changeset: c4f302dd414b11aae95735ed6baffcbd821c7ad999696bbe14008fa4ccad011dc Graft-Session: ses_01M6EY4Y08JMQ25BGCN6NV3DYR Graft-Work-Item: WI-3044 Graft-Batch: 20 --- README.md | 5 ++ docs/components.md | 81 +++++++++++++++++++++++++++++++++ docs/quick-start.md | 86 +++++++++++++++++++++++++++++++++++ examples/demo_app/src/main.rs | 26 +++++++++-- 4 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 docs/components.md create mode 100644 docs/quick-start.md diff --git a/README.md b/README.md index ce74eb6..e65039a 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,11 @@ primitive gets its states added to the matching family page in --- +## Consumer guides + +- [Quick start](docs/quick-start.md) — dependency setup, themed window, form, ribbon, and data table. +- [Component index](docs/components.md) — public exports, states, keyboard contracts, and gallery routes. + ## 5. Versioning and cadence - Every tag records the Zed commit it was cut from (`+zed.`). diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 0000000..4979e08 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,81 @@ +# Component index + +One line per public component export, sorted alphabetically. “States” names the +important visual or interaction variants; “keyboard” records the contract that +consumers can rely on; “gallery” is the route family in `examples/gallery`. + +- Accordion — primitives; expanded/collapsed, disabled; Enter/Space toggles, arrows move focus; **Status and feedback** +- AppDot — primitives; online/offline/connecting; focus follows the host; **Status and feedback** +- Avatar — primitives; image/initials/fallback; no internal keyboard interaction; **Text and labels** +- Badge — primitives; neutral/accent/danger; no internal keyboard interaction; **Status and feedback** +- Breadcrumb — layout; current/link/separator; Tab follows links; **Data and lists** +- Button — primitives; appearances, shapes, sizes, disabled; Enter/Space activates; **Buttons and links** +- Card — primitives; default/interactive; Tab only when an action is supplied; **Status and feedback** +- Checkbox — primitives; checked/unchecked/indeterminate/disabled; Space toggles; **Selection controls** +- Chip — primitives; default/selected/dismissible; Enter/Space activates, Delete dismisses; **Buttons and links** +- Combobox — primitives; open/closed, selected, disabled; arrows navigate, Enter selects, Escape closes; **Selection controls** +- CommandPalette — layout; hidden/open, grouped results; arrows navigate, Enter runs, Escape closes; **Buttons and links** +- ConnectionBadge — primitives; connected/disconnected/connecting; no internal keyboard interaction; **Status and feedback** +- ContextMenu — layout; hidden/open, enabled/disabled items; arrows navigate, Enter activates, Escape closes; **Buttons and links** +- CounterBadge — primitives; count/zero/overflow/dot; no internal keyboard interaction; **Status and feedback** +- DataTable — layout; ready/loading/empty/error/no-results, selected/sorted; arrows/Home/End/PageUp/PageDown navigate rows; **Data and lists** +- DateTimePicker — primitives; segments, popup, validation; arrows/Home/End move and Up/Down adjust segments; **Date and input** +- Dialog — layout; modal/open/closed; Tab is trapped, Escape dismisses; **Status and feedback** +- Divider — primitives; horizontal/vertical; no keyboard interaction; **Status and feedback** +- DockPanel — layout; docked/floating, collapsed; keyboard follows its child; **Data and lists** +- DocumentViewer — layout; loading/ready/error, rich blocks; keyboard follows document content; **Data and lists** +- Drawer — layout; overlay/push, left/right/top/bottom, sizes; Tab is trapped, Escape closes; **Data and lists** +- Dropdown — primitives; open/closed, selected, disabled; arrows navigate, Enter selects, Escape closes; **Selection controls** +- Expander — primitives; expanded/collapsed; Enter/Space toggles; **Status and feedback** +- Field — primitives; required/helper/validating/error; Tab enters its control; **Forms** +- FilePickerField — app; empty/selected/loading/error; Tab focuses the picker action, Enter/Space opens; **Date and input** +- Flyout — layout; hidden/open, placements; Tab follows content, Escape closes; **Status and feedback** +- Icon — primitives; sizes; not focusable by itself; **Buttons and links** +- IconButton — primitives; appearances, disabled, pressed; Enter/Space activates; **Buttons and links** +- InfoLabel — primitives; default/with tooltip; Tab focuses the info affordance; **Text and labels** +- InputCapture — primitives; active/inactive; Escape exits capture, keys are reported to the callback; **Date and input** +- Label — primitives; body/caption/subtitle; not focusable by itself; **Text and labels** +- Link — primitives; default/subtle/disabled; Enter activates; **Buttons and links** +- MenuBar — layout; menus/open/disabled items; arrows navigate, Enter opens, Escape closes; **Buttons and links** +- MenuButton — layout; closed/open/disabled; Enter/Space opens, arrows navigate, Escape closes; **Buttons and links** +- MessageBar — layout; info/success/warning/error; Tab reaches actions, Escape dismisses when enabled; **Status and feedback** +- MessageDialog — layout; open/closed, confirmation; Tab reaches actions, Escape dismisses; **Status and feedback** +- ModalHost — layout; empty/active modal; Tab is trapped, Escape dismisses; **Status and feedback** +- Nav — layout; selected/expanded/disabled categories; arrows move, Enter selects; **Data and lists** +- Pagination — layout; first/previous/current/next/last; Tab reaches page buttons, Enter activates; **Data and lists** +- Pane — layout; active/empty; keyboard belongs to its child; **Data and lists** +- PaneGroup — layout; split/resizable, collapsed; keyboard follows the active pane; **Data and lists** +- Persona — primitives; sizes, image/initials, presence; no internal keyboard interaction; **Text and labels** +- Popover — layout; open/closed; Tab is trapped while open, Escape closes; **Status and feedback** +- ProgressBar — primitives; determinate/indeterminate; no keyboard interaction; **Status and feedback** +- RadioGroup — primitives; selected/unselected/disabled; arrows change selection, Space selects; **Selection controls** +- Rating — primitives; empty/partial/full, readonly; arrows change value when interactive; **Status and feedback** +- RibbonBar — ribbon; tabs/contextual tabs/collapsed/overflow; arrows move tabs, Enter activates; **Buttons and links** +- Searchbox — primitives; empty/filled/error/disabled; typing edits, Escape clears or dismisses; **Date and input** +- SelectableList — layout; selected/disabled/loading/empty; arrows/Home/End navigate, Enter selects; **Data and lists** +- SelectableListItem — layout; selected/disabled; arrows navigate, Enter selects; **Data and lists** +- SettingsNav — layout; active/disabled sections; arrows move, Enter selects; **Data and lists** +- Slider — primitives; horizontal/vertical, disabled; arrows/Home/End adjust value; **Selection controls** +- SpinButton — primitives; min/max/step/disabled; arrows/Home/End adjust value; **Selection controls** +- Spinner — primitives; small/medium/large; no keyboard interaction; **Status and feedback** +- Switch — primitives; on/off/disabled; Space toggles; **Selection controls** +- TabItem — layout; active/closable; Enter activates, Delete closes when enabled; **Buttons and links** +- TabStrip — layout; active/closable; arrows move, Enter activates, Delete closes when enabled; **Buttons and links** +- Tag — primitives; appearances, sizes, shapes, dismissible; Enter/Space activates, Delete dismisses; **Status and feedback** +- TagGroup — primitives; single/multiple/dismissible; arrows move, Delete dismisses; **Status and feedback** +- TerminalGrid — primitives; focused/unfocused, selection; arrows/Home/End move the terminal cursor; **Terminal and video** +- TerminalView — primitives; focused/unfocused, selection; arrows/Home/End move the terminal cursor; **Terminal and video** +- Textarea — primitives; empty/filled/error/disabled; arrows/Home/End edit, Ctrl+A selects; **Date and input** +- TextInput — primitives; empty/filled/masked/error/disabled; arrows/Home/End edit and move caret, Ctrl+A selects; **Date and input** +- Toast — layout; info/success/warning/error, timeout; Tab reaches actions, Escape dismisses; **Status and feedback** +- ToggleButton — primitives; on/off/disabled; Enter/Space toggles; **Buttons and links** +- TokenInput — primitives; empty/tokens/suggestions; arrows navigate, Enter commits, Backspace removes; **Date and input** +- Toolbar — layout; horizontal/vertical, disabled items; arrows move, Enter activates; **Buttons and links** +- Tooltip — primitives; hidden/visible; appears on hover/focus, no internal keyboard interaction; **Status and feedback** +- Tree — layout; expanded/collapsed/checked/selected; arrows navigate, Enter/Space activates; **Data and lists** +- TreeItem — layout; expanded/collapsed/checked/selected; arrows navigate, Enter/Space activates; **Data and lists** +- VideoSurface — primitives; loading/ready/error, scaling; Space toggles playback when enabled; **Terminal and video** +- VirtualList — layout; empty/large/variable-height; arrows/Home/End navigate the focused item; **Data and lists** +- WindowChrome — app; active/inactive, maximized; platform title-bar commands use standard window keys; **Theme and direction** +- WindowTitle — app; active/inactive; standard platform window keys apply; **Theme and direction** +- Workspace — layout; panes/sidebar/tabs; keyboard follows the active child; **Data and lists** diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..90899fd --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,86 @@ +# Consumer quick start + +This guide builds a small Windows or Linux desktop app with a Fluent theme, +form controls, a ribbon, and a data table. The complete version of this example +is compiled as `demo_app` in `examples/demo_app`. + +## 1. Add the crates + +`fluent-gpui` is consumed from an exact git tag. Use one tag for every crate; +the placeholder below is replaced with the release tag you adopt. + +```toml +[dependencies] +fluent-app = { git = "https://github.com/thedancingdeveloper/fluent-gpui", tag = "v3.0.0+zed." } +fluent-core = { git = "https://github.com/thedancingdeveloper/fluent-gpui", tag = "v3.0.0+zed." } +fluent-layout = { git = "https://github.com/thedancingdeveloper/fluent-gpui", tag = "v3.0.0+zed." } +fluent-primitives = { git = "https://github.com/thedancingdeveloper/fluent-gpui", tag = "v3.0.0+zed." } +fluent-ribbon = { git = "https://github.com/thedancingdeveloper/fluent-gpui", tag = "v3.0.0+zed." } +``` + +The first 3.x tag is not yet published in this repository. Until the tag work +lands, a workspace checkout (as used by `demo_app`) can use the same crates as +path dependencies. Apps should depend on `fluent-*`, not directly on `gpui`. + +## 2. Create a themed window + +`FluentApp` installs the theme, title bar, window chrome, modal and toast hosts, +and bundled assets. `ThemeProvider` exposes the active tokens while rendering. + +```rust +use fluent_app::FluentApp; +use fluent_core::ThemeProvider as _; +use fluent_primitives::{Button, ButtonAppearance}; +use gpui::{div, prelude::*, ClickEvent, Context, IntoElement, Render, Window}; + +struct Hello; +impl Render for Hello { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div().size_full().bg(cx.theme().colors.surface).child( + Button::new("hello") + .label("Create project") + .appearance(ButtonAppearance::Accent) + .on_click(|_: &ClickEvent, _, _| {}), + ) + } +} + +fn main() { + FluentApp::new("Project tracker") + .window_size(960.0, 640.0) + .run(|cx| cx.new(|_| Hello)); +} +``` + +## 3. Add a form, ribbon, and table + +Use `Field` with a control such as `TextInput`, compose commands with +`RibbonBar`, and describe a table with `DataColumn` and `DataRow`: + +```rust,no_run +use fluent_layout::{DataCell, DataColumn, DataRow, DataTable}; +use fluent_primitives::{Field, TextInput}; +use fluent_ribbon::RibbonBar; + +let name = TextInput::new("name").placeholder("Project name"); +let form_field = Field::new("project-name").label("Project name").control(name); +let table = DataTable::new() + .columns([DataColumn::new("name", "Name"), DataColumn::new("owner", "Owner")]) + .rows([DataRow::new("p1", [DataCell::new("Design system"), DataCell::new("Alex")])]); +// A RibbonBar is configured in an entity with RibbonBar::new(cx).tab(...). +let _ = (form_field, table); +``` + +For a runnable composition, see `examples/demo_app/src/main.rs`: it includes a +`SettingsNav` form, a `RibbonBar` with contextual commands, and a `DataTable` +backed by the demo project model. Build it with: + +```sh +cargo check -p demo_app +``` + +## 4. Find the right control + +See [the component index](components.md) for exports, states, keyboard +contracts, and gallery routes. The gallery is the visual regression matrix: +run `cargo run -p gallery` and choose a family in its navigation. diff --git a/examples/demo_app/src/main.rs b/examples/demo_app/src/main.rs index f08e61e..0eeb60a 100644 --- a/examples/demo_app/src/main.rs +++ b/examples/demo_app/src/main.rs @@ -1,9 +1,9 @@ use fluent_app::FluentApp; use fluent_core::ThemeProvider as _; use fluent_layout::{ - CommandPalette, ConfirmModal, ContextMenu, DockPanel, DockPosition, MenuBar, MenuItemDef, - ModalHost, ModalStack, PaletteEntry, Pane, SettingsNav, SettingsNavItem, SettingsNavSection, - TabItem, TabStrip, Workspace, + CommandPalette, ConfirmModal, ContextMenu, DataColumn, DataRow, DataTable, DockPanel, + DockPosition, MenuBar, MenuItemDef, ModalHost, ModalStack, PaletteEntry, Pane, SettingsNav, + SettingsNavItem, SettingsNavSection, TabItem, TabStrip, Workspace, }; use fluent_ribbon::RibbonBar; use gpui::{ @@ -322,6 +322,26 @@ impl Render for DashboardView { .text_color(colors.on_neutral) .child("Starred"), ) + .child( + div().px(px(spacing.xl)).pb(px(spacing.md)).child( + DataTable::new() + .columns([ + DataColumn::new("project", "Project"), + DataColumn::new("owner", "Owner"), + DataColumn::new("status", "Status"), + ]) + .rows(all_projects.iter().map(|project| { + DataRow::new( + project.id.clone(), + [ + project.name.clone(), + project.owner.clone(), + project.status.label().into(), + ], + ) + })), + ), + ) .child( div() .px(px(spacing.xl)) From a0d834230ebc393403e11cffbd8dca8f6a235582 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:21:16 +0000 Subject: [PATCH 05/13] Add Calendar and ColorPicker primitives Graft-Changeset: c61cb3cb5e6b86b260388548725b01dceafdd9435f87e6d55bb70fd751db14f30 Graft-Session: ses_01M6EY4WCC2B4WMAPJDAHSZP4Z Graft-Work-Item: WI-3040 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- crates/fluent-primitives/src/calendar.rs | 239 +++++++++++++++++++ crates/fluent-primitives/src/color_picker.rs | 170 +++++++++++++ crates/fluent-primitives/src/lib.rs | 4 + 3 files changed, 413 insertions(+) create mode 100644 crates/fluent-primitives/src/calendar.rs create mode 100644 crates/fluent-primitives/src/color_picker.rs diff --git a/crates/fluent-primitives/src/calendar.rs b/crates/fluent-primitives/src/calendar.rs new file mode 100644 index 0000000..64241e1 --- /dev/null +++ b/crates/fluent-primitives/src/calendar.rs @@ -0,0 +1,239 @@ +use fluent_core::ThemeProvider as _; + +type CalendarChange = std::rc::Rc; +use gpui::{ + div, prelude::*, px, App, ClickEvent, Context, ElementId, IntoElement, KeyDownEvent, Render, + Role, SharedString, Window, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CalendarDate { + pub year: i32, + pub month: u8, + pub day: u8, +} +impl CalendarDate { + pub fn new(year: i32, month: u8, day: u8) -> Option { + let max = days_in_month(year, month)?; + (day > 0 && day <= max).then_some(Self { year, month, day }) + } +} + +#[derive(Clone)] +pub struct Calendar { + id: ElementId, + value: CalendarDate, + month: CalendarDate, + first_day_of_week: u8, + min: Option, + max: Option, + label: SharedString, + on_change: Option, +} +impl Calendar { + pub fn new(id: impl Into, value: CalendarDate) -> Self { + Self { + id: id.into(), + value, + month: CalendarDate { day: 1, ..value }, + first_day_of_week: 0, + min: None, + max: None, + label: "Calendar".into(), + on_change: None, + } + } + pub fn value(mut self, value: CalendarDate) -> Self { + self.value = value; + self.month = CalendarDate { day: 1, ..value }; + self + } + pub fn first_day_of_week(mut self, day: u8) -> Self { + self.first_day_of_week = day % 7; + self + } + pub fn locale_first_day_of_week(self, day: u8) -> Self { + self.first_day_of_week(day) + } + pub fn min(mut self, date: CalendarDate) -> Self { + self.min = Some(date); + self + } + pub fn max(mut self, date: CalendarDate) -> Self { + self.max = Some(date); + self + } + pub fn label(mut self, label: impl Into) -> Self { + self.label = label.into(); + self + } + pub fn on_change(mut self, f: impl Fn(CalendarDate, &mut App) + 'static) -> Self { + self.on_change = Some(std::rc::Rc::new(f)); + self + } + /// Move the active date by a keyboard grid step. Returns `None` when the move is outside bounds. + pub fn keyboard_move(&self, key: &str) -> Option { + let delta = match key { + "left" => -1, + "right" => 1, + "up" => -7, + "down" => 7, + _ => return None, + }; + let candidate = add_days(self.value, delta); + self.allowed(candidate).then_some(candidate) + } + + pub fn selected(&self) -> CalendarDate { + self.value + } + fn allowed(&self, date: CalendarDate) -> bool { + self.min.is_none_or(|d| date >= d) && self.max.is_none_or(|d| date <= d) + } +} +impl Render for Calendar { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let colors = theme.colors.clone(); + let spacing = theme.spacing; + let month = self.month; + let first = self.first_day_of_week; + let selected = self.value; + let change = self.on_change.clone(); + let names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let mut weekdays = div().flex().gap_1(); + for i in 0..7 { + weekdays = weekdays.child( + div() + .w(px(32.)) + .text_center() + .text_size(px(11.)) + .text_color(colors.on_subtle) + .child(names[((i + first) % 7) as usize]), + ); + } + let offset = (weekday(month.year, month.month) + 7 - first) % 7; + let count = days_in_month(month.year, month.month).unwrap_or(28); + let mut grid = div().flex().flex_wrap().gap_1(); + for slot in 0..(offset + count) { + if slot < offset { + grid = grid.child(div().size(px(32.))); + continue; + } + let day = slot - offset + 1; + let date = CalendarDate { day, ..month }; + let enabled = self.allowed(date); + let is_selected = date == selected; + let handler = change.clone(); + let mut cell = div() + .id(("calendar-day", day as u64)) + .size(px(32.)) + .flex() + .items_center() + .justify_center() + .role(Role::GridCell); + if enabled { + cell = cell + .cursor_pointer() + .when(is_selected, |d| { + d.bg(colors.accent).text_color(colors.on_accent) + }) + .on_click(move |_: &ClickEvent, _, app| { + if let Some(f) = &handler { + f(date, app); + } + }); + } else { + cell = cell.text_color(colors.on_neutral_disabled); + } + grid = grid.child(cell.child(day.to_string())); + } + div() + .id(self.id.clone()) + .role(Role::Grid) + .flex() + .flex_col() + .gap(px(spacing.sm)) + .child( + div() + .text_size(px(14.)) + .child(format!("{} / {}", month.month, month.year)), + ) + .child(weekdays) + .child(grid) + .on_key_down(move |ev: &KeyDownEvent, _, app| { + if matches!(ev.keystroke.key.as_str(), "left" | "right" | "up" | "down") { + app.stop_propagation(); + } + }) + } +} +fn add_days(mut date: CalendarDate, delta: i32) -> CalendarDate { + let step = if delta < 0 { -1 } else { 1 }; + for _ in 0..delta.unsigned_abs() { + if step > 0 { + if date.day < days_in_month(date.year, date.month).unwrap_or(28) { + date.day += 1; + } else if date.month == 12 { + date = CalendarDate { + year: date.year + 1, + month: 1, + day: 1, + }; + } else { + date = CalendarDate { + year: date.year, + month: date.month + 1, + day: 1, + }; + } + } else if date.day > 1 { + date.day -= 1; + } else if date.month == 1 { + date = CalendarDate { + year: date.year - 1, + month: 12, + day: 31, + }; + } else { + let month = date.month - 1; + date = CalendarDate { + year: date.year, + month, + day: days_in_month(date.year, month).unwrap_or(28), + }; + } + } + date +} +fn days_in_month(year: i32, month: u8) -> Option { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31), + 4 | 6 | 9 | 11 => Some(30), + 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => Some(29), + 2 => Some(28), + _ => None, + } +} +fn weekday(year: i32, month: u8) -> u8 { + let (y, m) = if month < 3 { + (year - 1, month as i32 + 12) + } else { + (year, month as i32) + }; + ((y + y / 4 - y / 100 + y / 400 + (13 * m + 8) / 5 + 1) % 7) as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn calendar_date_rejects_invalid_days() { + assert!(CalendarDate::new(2024, 2, 29).is_some()); + assert!(CalendarDate::new(2023, 2, 29).is_none()); + } + #[test] + fn calendar_week_starts_with_locale_offset() { + assert_eq!(weekday(2024, 1), 1); + } +} diff --git a/crates/fluent-primitives/src/color_picker.rs b/crates/fluent-primitives/src/color_picker.rs new file mode 100644 index 0000000..7913408 --- /dev/null +++ b/crates/fluent-primitives/src/color_picker.rs @@ -0,0 +1,170 @@ +use fluent_core::ThemeProvider as _; + +type ColorChange = std::rc::Rc; +use gpui::{ + div, prelude::*, px, App, Context, ElementId, IntoElement, KeyDownEvent, Render, Role, + SharedString, Window, +}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, +} +impl Color { + pub const WHITE: Self = Self { + red: 255, + green: 255, + blue: 255, + }; + pub fn hex(self) -> String { + format!("#{:02X}{:02X}{:02X}", self.red, self.green, self.blue) + } + pub fn parse(s: &str) -> Option { + let s = s.trim().strip_prefix('#').unwrap_or(s); + (s.len() == 6) + .then(|| u32::from_str_radix(s, 16).ok()) + .flatten() + .map(|n| Self { + red: (n >> 16) as u8, + green: (n >> 8) as u8, + blue: n as u8, + }) + } +} +#[derive(Clone)] +pub struct ColorPicker { + id: ElementId, + color: Color, + swatches: Vec, + label: SharedString, + on_change: Option, +} +impl ColorPicker { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + color: Color::WHITE, + swatches: vec![ + Color { + red: 0, + green: 120, + blue: 212, + }, + Color { + red: 16, + green: 124, + blue: 16, + }, + Color { + red: 196, + green: 49, + blue: 75, + }, + Color::WHITE, + ], + label: "Color".into(), + on_change: None, + } + } + pub fn value(mut self, c: Color) -> Self { + self.color = c; + self + } + pub fn swatches(mut self, s: impl IntoIterator) -> Self { + self.swatches = s.into_iter().collect(); + self + } + pub fn label(mut self, s: impl Into) -> Self { + self.label = s.into(); + self + } + pub fn on_change(mut self, f: impl Fn(Color, &mut App) + 'static) -> Self { + self.on_change = Some(std::rc::Rc::new(f)); + self + } + pub fn set_hex(&mut self, value: &str) -> bool { + if let Some(color) = Color::parse(value) { + self.color = color; + true + } else { + false + } + } + pub fn nudge(&mut self, channel: &str, delta: i16) { + let adjust = |v: u8| (v as i16).saturating_add(delta).clamp(0, 255) as u8; + self.color = match channel { + "red" => Color { + red: adjust(self.color.red), + ..self.color + }, + "green" => Color { + green: adjust(self.color.green), + ..self.color + }, + "blue" => Color { + blue: adjust(self.color.blue), + ..self.color + }, + _ => self.color, + }; + } + + pub fn color(&self) -> Color { + self.color + } +} +impl Render for ColorPicker { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let colors = theme.colors.clone(); + let selected = self.color; + let handler = self.on_change.clone(); + let mut sw = div().flex().gap_1(); + for (i, color) in self.swatches.iter().copied().enumerate() { + let h = handler.clone(); + sw = sw.child( + div() + .id(("color-swatch", i as u64)) + .size(px(28.)) + .rounded(px(4.)) + .bg(gpui::rgb( + (u32::from(color.red) << 16) + | (u32::from(color.green) << 8) + | u32::from(color.blue), + )) + .when(color == selected, |d| { + d.border_2().border_color(colors.focus_ring) + }) + .on_click(move |_, _, app| { + if let Some(f) = &h { + f(color, app) + } + }), + ); + } + div() + .id(self.id.clone()) + .role(Role::Group) + .flex() + .flex_col() + .gap(px(8.)) + .child( + div() + .h(px(80.)) + .w(px(180.)) + .bg(gpui::rgb( + (u32::from(selected.red) << 16) + | (u32::from(selected.green) << 8) + | u32::from(selected.blue), + )) + .on_key_down(move |ev: &KeyDownEvent, _, app| { + if matches!(ev.keystroke.key.as_str(), "left" | "right" | "up" | "down") { + app.stop_propagation() + } + }), + ) + .child(sw) + .child(div().text_size(px(13.)).child(selected.hex())) + } +} diff --git a/crates/fluent-primitives/src/lib.rs b/crates/fluent-primitives/src/lib.rs index 0c7716d..56e6ece 100644 --- a/crates/fluent-primitives/src/lib.rs +++ b/crates/fluent-primitives/src/lib.rs @@ -3,9 +3,11 @@ mod app_dot; mod avatar; mod badge; mod button; +mod calendar; mod card; mod checkbox; mod chip; +mod color_picker; mod combobox; mod connection_badge; mod date_time_picker; @@ -51,9 +53,11 @@ pub use badge::{ Badge, BadgeVariant, CounterBadge, CounterBadgeAppearance, PresenceBadge, PresenceBadgeState, }; pub use button::{Button, ButtonAppearance, ButtonShape, ButtonSize}; +pub use calendar::{Calendar, CalendarDate}; pub use card::Card; pub use checkbox::{Checkbox, CheckboxState}; pub use chip::Chip; +pub use color_picker::{Color, ColorPicker}; pub use combobox::{Combobox, ComboboxOption}; pub use connection_badge::{ConnectionBadge, ConnectionBadgeState}; pub use date_time_picker::{ From 3e46b69b78d3ee95306215bfe99e40015ab2d924 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:32:03 +0000 Subject: [PATCH 06/13] docs: verify DPI scaling parity matrix Graft-Changeset: ce5b3d5065555acc8502b917d8d258387ff6743e3b4d3b9d2dda8dc5902f758e6 Graft-Session: ses_01M6EZ4R8V8CYM17TY1TGGS8FD Graft-Work-Item: WI-3061 Graft-Batch: 20 --- CHANGELOG.md | 1 + docs/platform-parity.md | 47 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34220ee..a5c9c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ per crate. ## Unreleased +- docs: document DPI scaling conversion and resize behavior across Windows DirectX, Linux X11, and Linux Wayland, with native-run evidence requirements (WI-3061). - Layout: add Fluent 2 Breadcrumb and Pagination controls with overflow menus, current-item semantics, RTL separators, and keyboard-ready controls (WI-3036). - fluent-layout, gallery: Drawer with inline and overlay modes, RTL-aware start/end placement, size presets, reduced-motion-aware transitions, focus trapping and opener restoration (WI-3038). - Add the Windows/Linux platform parity matrix and native verification gap list ([WI-3043](https://vogt.sprooty.com/work/WI-3043)). diff --git a/docs/platform-parity.md b/docs/platform-parity.md index c1fa308..24c2a55 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -10,10 +10,11 @@ platform policy. **Evidence labels:** **verified-by-test** means a repository test exercises the behavior (usually on GPUI's test platform); **verified-manually** means an -operator has exercised the native backend and recorded the result; **unverified** -means the source contract is documented but native behavior still needs a -backend run. A row marked **divergent** is an intentional platform difference, -not a failure. +operator has exercised the native backend and recorded the result; **source- +audited** means the backend implementation and the shared conversion contract +were inspected, but no native desktop run is claimed; **unverified** means the +source contract is documented but native behavior still needs a backend run. A +row marked **divergent** is an intentional platform difference, not a failure. ## Matrix @@ -27,11 +28,47 @@ not a failure. | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message; the key-scan-code patch is not verified on real Windows. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key, including repeats. | **divergent, Windows unverified**. Follow [WI-3058](https://vogt.sprooty.com/work/WI-3058); implementation notes are in [`PATCHES.md`](../vendor/zed/PATCHES.md). | | Clipboard | GPUI platform clipboard API provides text and image read/write; native integration is not manually verified here. | Clipboard ownership and persistence depend on the X11 selection/clipboard owner. | Clipboard access depends on compositor/data-device support and may be asynchronous. | **divergent, unverified**. Follow [WI-3059](https://vogt.sprooty.com/work/WI-3059). | | File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | -| DPI scaling and device pixels | Scale and resize events are backend-provided; the DirectX path has no recorded native run in this repository. | Scale behavior depends on the X11 display and compositor configuration. | Scale behavior is compositor/output dependent and can differ from X11. | **divergent, unverified**. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061). | +| DPI scaling and device pixels | `WM_DPICHANGED` updates the window scale factor; device-pixel resize sizes are converted to logical pixels before the resize callback. Text and layout continue to use logical pixels, while renderer bounds use device pixels. | The display scale is selected from `GPUI_X11_SCALE_FACTOR`, RandR, `Xft.dpi`, or the default `1.0` (in that order); X11 resize callbacks receive logical size and scale. | The compositor-provided output scale is applied to the surface; configure/resize handling is throttled during interactive resizing and callbacks receive logical size and scale. | **source-audited, unverified manually**. The shared contract is `logical = physical / scale` and `physical = round(logical * scale)`; see the manual evidence and test plan below. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061) for native runs. | | X11 vs Wayland window/input differences | Not applicable. | X11 exposes server-side selections and X11 event details; window-manager policy is external. | Wayland delegates more policy to the compositor and uses evdev/data-device protocols; server-side assumptions must not be made. | **divergent** by design and **unverified** as a complete matrix. Follow [WI-3062](https://vogt.sprooty.com/work/WI-3062). | ## Audit notes +### DPI audit and manual evidence (WI-3061) + +The source audit confirms one shared conversion contract across the three +backends: + +- window and text/layout dimensions are represented as logical `Pixels`; +- renderer/window-system dimensions are represented as integer `DevicePixels`; +- conversion from logical to device pixels rounds `logical * scale_factor`, and + conversion back divides by the same scale factor; +- a backend resize callback carries the logical size and the current scale + factor, so consumers do not need to perform a second conversion. + +Backend-specific observations are also recorded in the matrix: Windows reacts +to `WM_DPICHANGED`, X11 has explicit environment/RandR/Xft fallbacks, and +Wayland applies compositor output scale while handling configure-driven +resizes. These are intentionally not treated as identical native behavior. + +Manual evidence collected on 2026-11-02 UTC: this worker environment has no +`DISPLAY`, `WAYLAND_DISPLAY`, or `XDG_SESSION_TYPE`, and it is not a Windows +host. Consequently, no native X11, Wayland, or DirectX interaction run is +claimed. The remaining native evidence needed to close this row is: + +1. Windows DirectX: move a window between 100% and 150% monitors, resize it, + and verify that the reported scale changes, logical content size remains + stable, device-pixel renderer size changes, and text does not double-scale. +2. Linux X11: repeat at two RandR/Xft scales and with an explicit + `GPUI_X11_SCALE_FACTOR`, verifying resize callback sizes and rendered text. +3. Linux Wayland: repeat across outputs with different compositor scales, + including an interactive resize, verifying configure/scale ordering and + text size. + +For each run, record backend, scale settings, logical window size, device +window/render-target size, callback scale factor, and a screenshot of the same +text sample. Do not mark this row **verified-manually** until those observations +are attached to the work item. + - The source audit covered [`title_bar.rs`](../crates/fluent-app/src/title_bar.rs), [`pickers.rs`](../crates/fluent-app/src/pickers.rs), and the related [`window_title.rs`](../crates/fluent-app/src/window_title.rs). These files From 6660c6598d043738258f0cfbcfffda9966060176 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:33:01 +0000 Subject: [PATCH 07/13] docs: verify clipboard platform parity Graft-Changeset: c7f0dabf3ddefd33ebcd52c7f6749352ab5ba3d868a890273dd20f17613bb86ae Graft-Session: ses_01M6EZ4R4KDB6RFWYX63K8DW1G Graft-Work-Item: WI-3059 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- docs/platform-parity.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/platform-parity.md b/docs/platform-parity.md index 24c2a55..27522f1 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -26,7 +26,7 @@ row marked **divergent** is an intentional platform difference, not a failure. | High-contrast detection | The `high-contrast` patch exposes OS state and change notifications, but this repository has no real-Windows run recorded. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; desktop coverage is not recorded here. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; compositor/portal availability varies. | **unverified** natively. Follow [WI-3056](https://vogt.sprooty.com/work/WI-3056). See [`PATCHES.md`](../vendor/zed/PATCHES.md) for the implementation boundary. | | IME composition and committed text | GPUI separates composition/committed text from physical key events; native IME coverage is not recorded. | Same contract; toolkit/desktop IME setup is environment-dependent. | Same contract; toolkit/desktop IME setup is environment-dependent. | **unverified** natively. Follow [WI-3057](https://vogt.sprooty.com/work/WI-3057). | | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message; the key-scan-code patch is not verified on real Windows. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key, including repeats. | **divergent, Windows unverified**. Follow [WI-3058](https://vogt.sprooty.com/work/WI-3058); implementation notes are in [`PATCHES.md`](../vendor/zed/PATCHES.md). | -| Clipboard | GPUI platform clipboard API provides text and image read/write; native integration is not manually verified here. | Clipboard ownership and persistence depend on the X11 selection/clipboard owner. | Clipboard access depends on compositor/data-device support and may be asynchronous. | **divergent, unverified**. Follow [WI-3059](https://vogt.sprooty.com/work/WI-3059). | +| Clipboard | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | | File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | | DPI scaling and device pixels | `WM_DPICHANGED` updates the window scale factor; device-pixel resize sizes are converted to logical pixels before the resize callback. Text and layout continue to use logical pixels, while renderer bounds use device pixels. | The display scale is selected from `GPUI_X11_SCALE_FACTOR`, RandR, `Xft.dpi`, or the default `1.0` (in that order); X11 resize callbacks receive logical size and scale. | The compositor-provided output scale is applied to the surface; configure/resize handling is throttled during interactive resizing and callbacks receive logical size and scale. | **source-audited, unverified manually**. The shared contract is `logical = physical / scale` and `physical = round(logical * scale)`; see the manual evidence and test plan below. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061) for native runs. | | X11 vs Wayland window/input differences | Not applicable. | X11 exposes server-side selections and X11 event details; window-manager policy is external. | Wayland delegates more policy to the compositor and uses evdev/data-device protocols; server-side assumptions must not be made. | **divergent** by design and **unverified** as a complete matrix. Follow [WI-3062](https://vogt.sprooty.com/work/WI-3062). | @@ -81,3 +81,28 @@ are attached to the work item. check is useful build evidence, but it is not a native interaction test. - No macOS, web, or wasm behavior is included because those targets are outside this repository's support contract. + +## Clipboard audit + +The vendored GPUI platform implementations were inspected on 2026-11-02. The +repository's platform test backend verifies clipboard round trips, and the +Windows backend has a native test covering Unicode text, metadata, and NUL +normalization. The Linux backends expose text and image MIME formats: X11 uses +selection requests and a clipboard-manager handover, while Wayland reads the +data offer through a bounded pipe and caches successful reads. + +The asynchronous API is a ready task on these native backends and returns +`Ok(None)` when no item is available. The `ClipboardReadError` variants +(`Unavailable`, `Denied`, and `UnsupportedContent`) remain part of the shared +API for permission-gated platforms; no native Windows, X11, or Wayland failure +run was possible in this headless session. + +| Target | Text | Image | Async/failure handling | Evidence | +|---|---|---|---|---| +| Windows DirectX 11 | source + backend test | source (PNG/GIF/JPEG/SVG) | synchronous native read wrapped as ready task; no native failure run | source/test verified | +| Linux X11 | source (UTF-8 selection targets) | source (GPUI image MIME targets) | selection timeout/error returns no item; no X server in session | source verified | +| Linux Wayland | source (text MIME offer) | source (image MIME offer) | pipe transfer errors return no item; no compositor in session | source verified | + +Native host execution for all three targets remains follow-up work because this +session has no Windows desktop, X11 display, or Wayland compositor (`DISPLAY` +and `WAYLAND_DISPLAY` are unset). From be567fc03e213bc21c2c1dbff353923b96cd40a1 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:34:14 +0000 Subject: [PATCH 08/13] docs: resolve parity audit updates Graft-Changeset: cdb545ff925a3f57b27fa70896aa41f6a1fe3049cdedfb3c09d1c1f0974d66972 Graft-Session: ses_01M6EZ4QXPDXVQV9ARTYTA5D43 Graft-Work-Item: WI-3062 Graft-Batch: 20 --- docs/platform-parity.md | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/platform-parity.md b/docs/platform-parity.md index 27522f1..8e0540e 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -20,17 +20,50 @@ row marked **divergent** is an intentional platform difference, not a failure. | Behavior | Windows DirectX 11 | Linux X11 | Linux Wayland | Evidence and follow-up | |---|---|---|---|---| -| Native title bar and window controls | `TitleBar` can replace native decorations in client/frameless mode; drag uses `start_window_move`; minimize/maximize/close are app controls. | Same Fluent custom-title-bar contract; native decoration policy remains compositor/window-manager controlled. | Same Fluent custom-title-bar contract; compositor controls native decoration policy. | **unverified** natively. The wrapper behavior is covered by source-level/test-platform contracts; run [WI-3055](https://vogt.sprooty.com/work/WI-3055). | -| Snap and caption buttons | Windows snap layouts and caption-button hit testing are OS behaviors; custom chrome must preserve the caption-button affordance and leave controls accessible. | No Windows snap-layout equivalent is promised; tiling/window-manager behavior is external to Fluent. | Tiling and server-side/client-side decoration behavior are compositor-specific; no Windows snap-layout equivalent is promised. | **divergent, unverified**. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055) for a native interaction pass. | +| Window movement and native decorations | `TitleBar` can replace native decorations in client/frameless mode; drag uses `start_window_move`; minimize/maximize/close are app controls. Native frame behavior is a Windows policy. | A move request is handled by the X11 window manager; the app must not assume it can reposition a top-level window directly. Whether the native frame is shown is window-manager policy. | A move request is handled by the compositor through the xdg-shell protocol; the app must not assume it can reposition a top-level surface directly. Server-side/client-side decoration support varies by compositor. | **divergent, unverified** natively. The wrapper behavior is covered by source-level/test-platform contracts; run [WI-3055](https://vogt.sprooty.com/work/WI-3055). | +| Snap and caption buttons | Windows snap layouts and caption-button hit testing are OS behaviors; custom chrome must preserve the caption-button affordance and leave controls accessible. | No Windows snap-layout equivalent is promised; tiling/window-manager behavior is external to Fluent. | Tiling and decoration behavior are compositor-specific; no Windows snap-layout equivalent is promised. | **divergent, unverified**. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055) for a native interaction pass. | | System theme and accent | Fluent theme state supplies colors; `TitleBar` uses the Fluent accent colors. Automatic OS theme/accent synchronization is not claimed by the wrapper. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | **unverified** for native synchronization. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055). | | High-contrast detection | The `high-contrast` patch exposes OS state and change notifications, but this repository has no real-Windows run recorded. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; desktop coverage is not recorded here. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; compositor/portal availability varies. | **unverified** natively. Follow [WI-3056](https://vogt.sprooty.com/work/WI-3056). See [`PATCHES.md`](../vendor/zed/PATCHES.md) for the implementation boundary. | | IME composition and committed text | GPUI separates composition/committed text from physical key events; native IME coverage is not recorded. | Same contract; toolkit/desktop IME setup is environment-dependent. | Same contract; toolkit/desktop IME setup is environment-dependent. | **unverified** natively. Follow [WI-3057](https://vogt.sprooty.com/work/WI-3057). | | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message; the key-scan-code patch is not verified on real Windows. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key, including repeats. | **divergent, Windows unverified**. Follow [WI-3058](https://vogt.sprooty.com/work/WI-3058); implementation notes are in [`PATCHES.md`](../vendor/zed/PATCHES.md). | -| Clipboard | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | +| Clipboard (text and image) | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | | File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | | DPI scaling and device pixels | `WM_DPICHANGED` updates the window scale factor; device-pixel resize sizes are converted to logical pixels before the resize callback. Text and layout continue to use logical pixels, while renderer bounds use device pixels. | The display scale is selected from `GPUI_X11_SCALE_FACTOR`, RandR, `Xft.dpi`, or the default `1.0` (in that order); X11 resize callbacks receive logical size and scale. | The compositor-provided output scale is applied to the surface; configure/resize handling is throttled during interactive resizing and callbacks receive logical size and scale. | **source-audited, unverified manually**. The shared contract is `logical = physical / scale` and `physical = round(logical * scale)`; see the manual evidence and test plan below. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061) for native runs. | | X11 vs Wayland window/input differences | Not applicable. | X11 exposes server-side selections and X11 event details; window-manager policy is external. | Wayland delegates more policy to the compositor and uses evdev/data-device protocols; server-side assumptions must not be made. | **divergent** by design and **unverified** as a complete matrix. Follow [WI-3062](https://vogt.sprooty.com/work/WI-3062). | +## Manual evidence record + +A native run must record the session type, desktop/compositor, scale factor, +portal availability, and the result for each row. Use the same build and +scenario in both sessions; do not treat a test-platform result as native proof. +The following checklist is the minimum evidence to attach to a work-item +comment or release note: + +1. **Window movement/decorations:** launch the gallery, drag the custom title + bar, invoke minimize/maximize/close, and repeat with native decorations + enabled where the desktop allows it. Record whether the move request is + accepted and which component owns the frame. +2. **Clipboard:** copy and paste text and an image, then close the source + application and paste again. On X11 also test primary selection; on Wayland + record whether the data offer resolves asynchronously and which formats are + offered. +3. **Pickers:** open/save a file and pick a folder, cancel each dialog, and + test an extension filter. Record whether the portal/toolkit dialog appears, + the returned path, and cancellation/error behavior. +4. **Input:** exercise key repeat, IME composition, pointer motion, wheel, + drag, and title-bar buttons. Record physical key/scancode behavior and any + compositor-specific limitations. +5. **Scaling:** run at 100% and a non-100% scale, move the window between + outputs if available, and compare logical layout dimensions with device + pixel resize events. + +**Evidence for this documentation land (2026-11-02):** source and test +contracts were audited, but no native evidence is claimed. This worker has no +X11 or Wayland session (`DISPLAY` and `WAYLAND_DISPLAY` are unset), so the +manual scenarios above remain **unverified**. The follow-up items linked in the +matrix are the owners of those desktop runs. This explicit negative record +prevents a headless cargo/test run from being mistaken for manual evidence. + ## Audit notes ### DPI audit and manual evidence (WI-3061) From dadc03a76b6cca47e8cfe8bce1e6c60b58e4aba5 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:35:49 +0000 Subject: [PATCH 09/13] Resolve parity matrix sync conflict Graft-Changeset: c627f1621632d705b49f4165424ab15ad24fdce5510c2ecda75f9304c85bfcd97 Graft-Session: ses_01M6EZ4REF8DXQVGTWB30ZZQSA Graft-Work-Item: WI-3058 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- crates/fluent-primitives/src/input_capture.rs | 73 +++++++++++++++++++ docs/platform-parity.md | 26 ++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/fluent-primitives/src/input_capture.rs b/crates/fluent-primitives/src/input_capture.rs index 7e6d80b..933ddd9 100644 --- a/crates/fluent-primitives/src/input_capture.rs +++ b/crates/fluent-primitives/src/input_capture.rs @@ -884,6 +884,35 @@ mod tests { }); } + fn key_down_with_scan_code( + vcx: &mut VisualTestContext, + logical_key: &str, + scan_code: u32, + is_held: bool, + ) { + vcx.simulate_event(KeyDownEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: logical_key.into(), + key_char: Some(logical_key.into()), + }, + is_held, + prefer_character_input: false, + scan_code: Some(scan_code), + }); + } + + fn key_up_with_scan_code(vcx: &mut VisualTestContext, logical_key: &str, scan_code: u32) { + vcx.simulate_event(KeyUpEvent { + keystroke: Keystroke { + modifiers: Modifiers::default(), + key: logical_key.into(), + key_char: Some(logical_key.into()), + }, + scan_code: Some(scan_code), + }); + } + #[gpui::test] fn click_grabs_and_is_consumed(cx: &mut TestAppContext) { let (capture, log, vcx) = setup(cx); @@ -929,6 +958,50 @@ mod tests { assert!(grabbed(&capture, vcx)); } + #[gpui::test] + fn physical_scan_code_survives_press_repeat_and_release(cx: &mut TestAppContext) { + let (_capture, log, vcx) = setup(cx); + vcx.simulate_click(at(10.0, 10.0), Modifiers::default()); + log.borrow_mut().clear(); + + // Use a deliberately different logical key to prove that `code` is derived from the + // physical position. Linux receives evdev 30; Windows receives set-1 0x1e. Both are A. + let scan_code = 30; + key_down_with_scan_code(vcx, "q", scan_code, false); + key_down_with_scan_code(vcx, "q", scan_code, true); + key_up_with_scan_code(vcx, "q", scan_code); + + let events = log.borrow(); + let physical: Vec<_> = events + .iter() + .filter_map(|event| match event { + InputCaptureEvent::Key { + key, + code, + scan_code, + pressed, + is_held, + .. + } => Some(( + key.as_str(), + code.as_deref(), + *scan_code, + *pressed, + *is_held, + )), + _ => None, + }) + .collect(); + assert_eq!( + physical, + vec![ + ("q", Some("KeyA"), Some(scan_code), true, false), + ("q", Some("KeyA"), Some(scan_code), true, true), + ("q", Some("KeyA"), Some(scan_code), false, false), + ] + ); + } + #[gpui::test] fn not_grabbed_lets_keys_through(cx: &mut TestAppContext) { let (capture, log, vcx) = setup(cx); diff --git a/docs/platform-parity.md b/docs/platform-parity.md index 8e0540e..18d5321 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -25,7 +25,7 @@ row marked **divergent** is an intentional platform difference, not a failure. | System theme and accent | Fluent theme state supplies colors; `TitleBar` uses the Fluent accent colors. Automatic OS theme/accent synchronization is not claimed by the wrapper. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | **unverified** for native synchronization. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055). | | High-contrast detection | The `high-contrast` patch exposes OS state and change notifications, but this repository has no real-Windows run recorded. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; desktop coverage is not recorded here. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; compositor/portal availability varies. | **unverified** natively. Follow [WI-3056](https://vogt.sprooty.com/work/WI-3056). See [`PATCHES.md`](../vendor/zed/PATCHES.md) for the implementation boundary. | | IME composition and committed text | GPUI separates composition/committed text from physical key events; native IME coverage is not recorded. | Same contract; toolkit/desktop IME setup is environment-dependent. | Same contract; toolkit/desktop IME setup is environment-dependent. | **unverified** natively. Follow [WI-3057](https://vogt.sprooty.com/work/WI-3057). | -| Physical scancodes and key repeat | Set-1 make code is derived from the Windows message; the key-scan-code patch is not verified on real Windows. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key, including repeats. | **divergent, Windows unverified**. Follow [WI-3058](https://vogt.sprooty.com/work/WI-3058); implementation notes are in [`PATCHES.md`](../vendor/zed/PATCHES.md). | +| Physical scancodes and key repeat | Set-1 make code is derived from the Windows message, with `0xE000` marking an extended key. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key on the initial event and repeat timer. | **verified-by-test** for the shared patch/InputCapture press-repeat-release contract; **unverified natively** on all three backends. See [scancode evidence](#physical-scancode-evidence) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | | Clipboard (text and image) | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | | File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | | DPI scaling and device pixels | `WM_DPICHANGED` updates the window scale factor; device-pixel resize sizes are converted to logical pixels before the resize callback. Text and layout continue to use logical pixels, while renderer bounds use device pixels. | The display scale is selected from `GPUI_X11_SCALE_FACTOR`, RandR, `Xft.dpi`, or the default `1.0` (in that order); X11 resize callbacks receive logical size and scale. | The compositor-provided output scale is applied to the surface; configure/resize handling is throttled during interactive resizing and callbacks receive logical size and scale. | **source-audited, unverified manually**. The shared contract is `logical = physical / scale` and `physical = round(logical * scale)`; see the manual evidence and test plan below. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061) for native runs. | @@ -115,6 +115,30 @@ are attached to the work item. - No macOS, web, or wasm behavior is included because those targets are outside this repository's support contract. +## Physical scancode evidence + +WI-3058 audited the `key-scan-code` patch end to end and added a regression test +that injects a physical scancode whose logical key deliberately differs. The +test verifies that the physical code wins, the unchanged scancode is delivered +on press, repeat, and release, and only the repeated press has `is_held: true`. + +The backend source contract is: + +- Windows extracts the set-1 make code from `WM_KEYDOWN`/`WM_KEYUP` `lParam` + bits 16–23 and adds `0xE000` when the extended-key bit is set. +- X11 converts the XKB keycode to evdev with `event.detail - 8` for both key + directions. +- Wayland forwards the compositor's evdev key on both key directions and keeps + that key on the repeat-timer event. +- IME-committed text, replayed keystrokes, and simulated input may carry + `scan_code: None`; consumers must retain that fallback. + +This session did not have native Windows, X11, and Wayland desktops available, +so it does not label any backend **verified-manually**. A native pass should +capture one ordinary key, one extended key, a held repeat, and its release on +each backend, confirming that the press/repeat/release scancode is stable and +that repeat is reported only on key-down. + ## Clipboard audit The vendored GPUI platform implementations were inspected on 2026-11-02. The From fd6a05810eb955667804eca6caed3bd22df73860 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:48:12 +0000 Subject: [PATCH 10/13] docs: record high-contrast backend parity audit Graft-Changeset: cbc6500fb77a8068d3a5637dc00eca9929bf41321df761d2dcc364530c57ca49d Graft-Session: ses_01M6F02AP8Q0JDSR4TWDRYR5V0 Graft-Work-Item: WI-3056 Graft-Batch: 20 --- docs/platform-parity.md | 52 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/platform-parity.md b/docs/platform-parity.md index 18d5321..3df61e0 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -23,7 +23,7 @@ row marked **divergent** is an intentional platform difference, not a failure. | Window movement and native decorations | `TitleBar` can replace native decorations in client/frameless mode; drag uses `start_window_move`; minimize/maximize/close are app controls. Native frame behavior is a Windows policy. | A move request is handled by the X11 window manager; the app must not assume it can reposition a top-level window directly. Whether the native frame is shown is window-manager policy. | A move request is handled by the compositor through the xdg-shell protocol; the app must not assume it can reposition a top-level surface directly. Server-side/client-side decoration support varies by compositor. | **divergent, unverified** natively. The wrapper behavior is covered by source-level/test-platform contracts; run [WI-3055](https://vogt.sprooty.com/work/WI-3055). | | Snap and caption buttons | Windows snap layouts and caption-button hit testing are OS behaviors; custom chrome must preserve the caption-button affordance and leave controls accessible. | No Windows snap-layout equivalent is promised; tiling/window-manager behavior is external to Fluent. | Tiling and decoration behavior are compositor-specific; no Windows snap-layout equivalent is promised. | **divergent, unverified**. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055) for a native interaction pass. | | System theme and accent | Fluent theme state supplies colors; `TitleBar` uses the Fluent accent colors. Automatic OS theme/accent synchronization is not claimed by the wrapper. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | **unverified** for native synchronization. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055). | -| High-contrast detection | The `high-contrast` patch exposes OS state and change notifications, but this repository has no real-Windows run recorded. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; desktop coverage is not recorded here. | Portal-backed `org.freedesktop.appearance` contrast is implemented by the Linux platform layer; compositor/portal availability varies. | **unverified** natively. Follow [WI-3056](https://vogt.sprooty.com/work/WI-3056). See [`PATCHES.md`](../vendor/zed/PATCHES.md) for the implementation boundary. | +| High-contrast detection | `SPI_GETHIGHCONTRAST` reads `HCF_HIGHCONTRASTON`; `WM_SETTINGCHANGE` for that action notifies GPUI observers. | The xdg-desktop-portal `org.freedesktop.appearance` `contrast` setting is read at startup and on setting changes; values `>= 2` mean high contrast. | The same portal contract is consumed by the Wayland event loop; the compositor/portal must provide the setting. | **source-audited, unverified manually**. The shared GPUI contract only notifies when the boolean changes. See the [high-contrast evidence](#high-contrast-evidence-wi-3056) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | | IME composition and committed text | GPUI separates composition/committed text from physical key events; native IME coverage is not recorded. | Same contract; toolkit/desktop IME setup is environment-dependent. | Same contract; toolkit/desktop IME setup is environment-dependent. | **unverified** natively. Follow [WI-3057](https://vogt.sprooty.com/work/WI-3057). | | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message, with `0xE000` marking an extended key. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key on the initial event and repeat timer. | **verified-by-test** for the shared patch/InputCapture press-repeat-release contract; **unverified natively** on all three backends. See [scancode evidence](#physical-scancode-evidence) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | | Clipboard (text and image) | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | @@ -64,6 +64,56 @@ manual scenarios above remain **unverified**. The follow-up items linked in the matrix are the owners of those desktop runs. This explicit negative record prevents a headless cargo/test run from being mistaken for manual evidence. +## High-contrast evidence (WI-3056) + +The source audit on 2026-11-02 confirms the following end-to-end contract: + +- **Windows DirectX:** `gpui_windows::system_settings::system_high_contrast` + calls `SystemParametersInfoW(SPI_GETHIGHCONTRAST)` and checks + `HCF_HIGHCONTRASTON`. The window message handler watches `WM_SETTINGCHANGE` + for `SPI_GETHIGHCONTRAST` and invokes the registered callback. GPUI reads + the setting again when the application asks for `high_contrast()` rather + than relying on a stale cached value. +- **Linux X11:** the xdg-desktop-portal listener reads + `org.freedesktop.appearance/contrast` initially and subscribes to its + setting-change signal. `contrast >= 2` is converted to `true`; the X11 + client updates its state and invokes the callback only when that boolean + differs from the previous value. +- **Linux Wayland:** the same portal event source is consumed by the Wayland + client. It applies the same `contrast >= 2` conversion and changed-value + guard before invoking the callback. +- **Fluent integration:** `FluentApp` applies `Theme::high_contrast()` at + startup when the system reports high contrast and subscribes to the GPUI + change notification. Turning the setting off restores the configured light + or dark system theme. + +The source boundary is the `high-contrast` patch listed in +[`vendor/zed/PATCHES.md`](../vendor/zed/PATCHES.md). The relevant files are +`gpui_windows/src/system_settings.rs`, `gpui_windows/src/platform.rs`, +`gpui_windows/src/events.rs`, `gpui_linux/src/linux/xdg_desktop_portal.rs`, +`gpui_linux/src/linux/x11/client.rs`, `gpui_linux/src/linux/wayland/client.rs`, +`gpui/src/platform.rs`, `gpui/src/app.rs`, and +`crates/fluent-app/src/app_builder.rs`. + +### Manual test record + +| Backend | Host/session evidence | Result | Status | +|---|---|---|---| +| Windows DirectX 11 | No Windows host or desktop session is available in this worker. | Not run; `SPI_GETHIGHCONTRAST` and `WM_SETTINGCHANGE` require a Windows desktop. | **source-audited, unverified manually** | +| Linux X11 | `DISPLAY` is unset in this worker. | Not run; no X11 display or portal signal source is available. | **source-audited, unverified manually** | +| Linux Wayland | `WAYLAND_DISPLAY` and `XDG_SESSION_TYPE` are unset in this worker. | Not run; no Wayland compositor or portal signal source is available. | **source-audited, unverified manually** | + +The native follow-up procedure is deliberately recorded here rather than +claiming that a headless test is equivalent to a desktop run: launch the +gallery with the same build, enable high contrast, verify the initial +`high_contrast()` state and Fluent high-contrast tokens, toggle the setting +off and on, and verify exactly one theme update per boolean transition. On +Linux, record the desktop portal's `contrast` value (including a value below +2 and a value of 2 or greater); on Windows, record the Windows contrast +scheme and the `WM_SETTINGCHANGE` transition. Attach the host/session details +and observations to WI-3056 before changing any matrix cell to +**verified-manually**. + ## Audit notes ### DPI audit and manual evidence (WI-3061) From 84bb2dc8cdd56deb204846871f6151d28368d0ed Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:49:11 +0000 Subject: [PATCH 11/13] graft land cb25da0a1efdd Graft-Changeset: cb25da0a1efdd80a2efce485d11f9eccc06632d67289662fe3a2a6c8c16733f52 Graft-Session: ses_01M6F02AZ3PQFZEEENP9VSE75B Graft-Work-Item: WI-3057 Graft-Batch: 20 --- docs/platform-parity.md | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/platform-parity.md b/docs/platform-parity.md index 3df61e0..a44fdec 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -24,7 +24,7 @@ row marked **divergent** is an intentional platform difference, not a failure. | Snap and caption buttons | Windows snap layouts and caption-button hit testing are OS behaviors; custom chrome must preserve the caption-button affordance and leave controls accessible. | No Windows snap-layout equivalent is promised; tiling/window-manager behavior is external to Fluent. | Tiling and decoration behavior are compositor-specific; no Windows snap-layout equivalent is promised. | **divergent, unverified**. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055) for a native interaction pass. | | System theme and accent | Fluent theme state supplies colors; `TitleBar` uses the Fluent accent colors. Automatic OS theme/accent synchronization is not claimed by the wrapper. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | Same Fluent theme contract; desktop-environment synchronization is not claimed. | **unverified** for native synchronization. Follow [WI-3055](https://vogt.sprooty.com/work/WI-3055). | | High-contrast detection | `SPI_GETHIGHCONTRAST` reads `HCF_HIGHCONTRASTON`; `WM_SETTINGCHANGE` for that action notifies GPUI observers. | The xdg-desktop-portal `org.freedesktop.appearance` `contrast` setting is read at startup and on setting changes; values `>= 2` mean high contrast. | The same portal contract is consumed by the Wayland event loop; the compositor/portal must provide the setting. | **source-audited, unverified manually**. The shared GPUI contract only notifies when the boolean changes. See the [high-contrast evidence](#high-contrast-evidence-wi-3056) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | -| IME composition and committed text | GPUI separates composition/committed text from physical key events; native IME coverage is not recorded. | Same contract; toolkit/desktop IME setup is environment-dependent. | Same contract; toolkit/desktop IME setup is environment-dependent. | **unverified** natively. Follow [WI-3057](https://vogt.sprooty.com/work/WI-3057). | +| IME composition and committed text | `WM_IME_COMPOSITION` sends committed `GCS_RESULTSTR` text through `replace_text_in_range`, and the active `GCS_COMPSTR` through `replace_and_mark_text_in_range`; the IME context is enabled only while the focused input accepts text. | XIM preedit callbacks map to marked text and XIM commit callbacks map to committed text; XKB compose sequences use the same marked/committed path, while the physical key event remains separate. | `zwp_text_input_v3` preedit maps to marked text and commit strings map to committed text; the text-input state is enabled only for an accepting input and committed before dispatch. | **source-audited; unverified manually**. The shared `TextInput` handler replaces marked text, clears the mark on commit, and preserves UTF-16 ranges. See [IME evidence](#ime-evidence). | | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message, with `0xE000` marking an extended key. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key on the initial event and repeat timer. | **verified-by-test** for the shared patch/InputCapture press-repeat-release contract; **unverified natively** on all three backends. See [scancode evidence](#physical-scancode-evidence) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | | Clipboard (text and image) | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | | File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | @@ -165,6 +165,47 @@ are attached to the work item. - No macOS, web, or wasm behavior is included because those targets are outside this repository's support contract. +## IME evidence + +The GPUI input contract and the Fluent `TextInput` adapter were source-audited +on 2026-11-02. All three supported native paths deliver composition separately +from physical key events: + +- **Windows DirectX:** `WM_IME_COMPOSITION` reads `GCS_COMPSTR` as the active + composition and `GCS_RESULTSTR` as committed text. The active string is sent + to `replace_and_mark_text_in_range`; the result string is sent to + `replace_text_in_range`. The IME context is associated only while the focused + window has an input handler that accepts text, and an inactive context is + completed before it is detached. +- **Linux X11:** XIM preedit callbacks send marked text and XIM commit callbacks + send committed text. The XKB compose path also emits marked text while a + sequence is in progress and commits the composed character separately. X11 + key events remain distinct from these text callbacks. +- **Linux Wayland:** `zwp_text_input_v3` sends preedit text and commit strings + independently. GPUI maps these to marked and committed text respectively, + enables the protocol only for an accepting input handler, updates the IME + cursor rectangle, and commits the text-input state. + +`TextInput` replaces the current marked range during composition, underlines the +marked range, clears it when committed text arrives, and converts the GPUI +UTF-16 ranges at the input boundary. This is the parity contract: composition +may be rendered differently by each native backend, but the consumer receives +the same marked/committed distinction and does not need to process IME text as +physical key presses. The existing test-platform coverage exercises ordinary +editing and UTF-16 offset handling; it does not emulate native IME callbacks. + +No native IME run can be claimed from this worker: `DISPLAY`, +`WAYLAND_DISPLAY`, and `XDG_SESSION_TYPE` are unset, and the worker is not a +Windows host. Therefore Windows DirectX, Linux X11, and Linux Wayland remain +**unverified manually**. A native follow-up should use the same `TextInput` +scenario on each backend (Japanese hiragana-to-kanji conversion or an +equivalent composition, cancellation, reconversion, and direct committed text), +record the desktop/IME, whether preedit was visibly marked, the final committed +string, and whether any physical key event was emitted for the committed text. +For Linux also record whether XIM or Wayland text-input-v3 was available; for +Windows record the IME name and whether focus changes commit or cancel the +composition. + ## Physical scancode evidence WI-3058 audited the `key-scan-code` patch end to end and added a regression test From 21facaba3cc2aad6f8650db1188a6d1965abc617 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:56:35 +0000 Subject: [PATCH 12/13] Verify native picker parity contracts Graft-Changeset: c6171233c19d93d9366378168b7a564eb797bd332a116ef5fc55ed32b2e5ef1d4 Graft-Session: ses_01M6F0C9VJ4775AR7GQM1HFX3H Graft-Work-Item: WI-3060 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- crates/fluent-app/src/pickers.rs | 53 ++++++++++++++++++++++++++++++++ docs/platform-parity.md | 27 +++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/fluent-app/src/pickers.rs b/crates/fluent-app/src/pickers.rs index b9800e4..9cc4056 100644 --- a/crates/fluent-app/src/pickers.rs +++ b/crates/fluent-app/src/pickers.rs @@ -308,4 +308,57 @@ mod tests { assert_eq!(task.await.unwrap(), None); } + + #[gpui::test] + async fn cancelled_file_picker_returns_none(cx: &mut TestAppContext) { + let task = cx.update(|cx| cx.spawn(async move |cx| open_file(cx, &[]).await)); + cx.run_until_parked(); + cx.simulate_path_prompt_response(|options| { + assert!(options.files); + assert!(!options.directories); + assert!(!options.multiple); + None + }); + + assert_eq!(task.await.unwrap(), None); + } + + #[gpui::test] + async fn save_picker_preserves_cancellation_and_applies_preferred_extension( + cx: &mut TestAppContext, + ) { + let cancelled = cx.update(|cx| { + cx.spawn(async move |cx| { + save_file( + cx, + Path::new("/tmp"), + Some("report"), + &[ExtensionFilter::new("Text", ["txt"])], + ) + .await + }) + }); + cx.run_until_parked(); + assert!(cx.did_prompt_for_new_path()); + cx.simulate_new_path_selection(|path| { + assert_eq!(path, Path::new("/tmp")); + None + }); + assert_eq!(cancelled.await.unwrap(), None); + + let selected = cx.update(|cx| { + cx.spawn(async move |cx| { + save_file( + cx, + Path::new("/tmp"), + Some("report"), + &[ExtensionFilter::new("Text", ["txt"])], + ) + .await + }) + }); + cx.run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("report"))); + assert_eq!(selected.await.unwrap(), Some(PathBuf::from("report.txt"))); + } } diff --git a/docs/platform-parity.md b/docs/platform-parity.md index a44fdec..02004c1 100644 --- a/docs/platform-parity.md +++ b/docs/platform-parity.md @@ -27,7 +27,7 @@ row marked **divergent** is an intentional platform difference, not a failure. | IME composition and committed text | `WM_IME_COMPOSITION` sends committed `GCS_RESULTSTR` text through `replace_text_in_range`, and the active `GCS_COMPSTR` through `replace_and_mark_text_in_range`; the IME context is enabled only while the focused input accepts text. | XIM preedit callbacks map to marked text and XIM commit callbacks map to committed text; XKB compose sequences use the same marked/committed path, while the physical key event remains separate. | `zwp_text_input_v3` preedit maps to marked text and commit strings map to committed text; the text-input state is enabled only for an accepting input and committed before dispatch. | **source-audited; unverified manually**. The shared `TextInput` handler replaces marked text, clears the mark on commit, and preserves UTF-16 ranges. See [IME evidence](#ime-evidence). | | Physical scancodes and key repeat | Set-1 make code is derived from the Windows message, with `0xE000` marking an extended key. | Scancode is derived from X11 event detail (`detail - 8`). | Wayland supplies the evdev key on the initial event and repeat timer. | **verified-by-test** for the shared patch/InputCapture press-repeat-release contract; **unverified natively** on all three backends. See [scancode evidence](#physical-scancode-evidence) and [`PATCHES.md`](../vendor/zed/PATCHES.md). | | Clipboard (text and image) | GPUI's DirectX backend reads and writes Unicode text and registered image formats (PNG, GIF, JPEG, SVG). | The X11 selection backend supports UTF-8 text and the GPUI image formats; ownership and persistence depend on the selection owner/clipboard manager. | The data-device backend supports text and image MIME types, with reads transferred through a pipe and cached after a successful offer read. | **source-verified; native run unavailable**. See the [clipboard audit](#clipboard-audit) and follow [WI-3059](https://vogt.sprooty.com/work/WI-3059) for a Windows/X11/Wayland host run. | -| File and folder pickers | `fluent-app::pickers` delegates to native GPUI prompts; test-platform filtering and cancellation are tested, native dialogs are not. | Native dialog availability depends on the desktop portal/toolkit environment. | Native dialog availability normally depends on the desktop portal/toolkit environment. | **unverified** natively. Follow [WI-3060](https://vogt.sprooty.com/work/WI-3060); see [picker contracts](consumer-contracts.md#3-file-and-folder-pickers). | +| File and folder pickers | `fluent-app::pickers` delegates to the Windows `IFileOpenDialog`/`IFileSaveDialog`; cancellation maps from `ERROR_CANCELLED` to `Ok(None)`, while other dialog and shell-item failures remain errors. The save dialog currently advertises `*.*`; Fluent applies extension filters and preferred save extensions after the response. | `fluent-app::pickers` delegates to the xdg-desktop-portal OpenFile/SaveFile requests. A missing portal is reported as an error; a portal response cancellation maps to `Ok(None)`, and returned URIs are converted to local paths. | Same portal contract as X11, using the Wayland connection identifier where available. | **source-audited; verified-by-test** for shared filtering, cancellation, save-extension, and result-shape contracts; **unverified manually** on all three native backends. See [picker evidence](#picker-evidence-wi-3060). | | DPI scaling and device pixels | `WM_DPICHANGED` updates the window scale factor; device-pixel resize sizes are converted to logical pixels before the resize callback. Text and layout continue to use logical pixels, while renderer bounds use device pixels. | The display scale is selected from `GPUI_X11_SCALE_FACTOR`, RandR, `Xft.dpi`, or the default `1.0` (in that order); X11 resize callbacks receive logical size and scale. | The compositor-provided output scale is applied to the surface; configure/resize handling is throttled during interactive resizing and callbacks receive logical size and scale. | **source-audited, unverified manually**. The shared contract is `logical = physical / scale` and `physical = round(logical * scale)`; see the manual evidence and test plan below. Follow [WI-3061](https://vogt.sprooty.com/work/WI-3061) for native runs. | | X11 vs Wayland window/input differences | Not applicable. | X11 exposes server-side selections and X11 event details; window-manager policy is external. | Wayland delegates more policy to the compositor and uses evdev/data-device protocols; server-side assumptions must not be made. | **divergent** by design and **unverified** as a complete matrix. Follow [WI-3062](https://vogt.sprooty.com/work/WI-3062). | @@ -165,6 +165,31 @@ are attached to the work item. - No macOS, web, or wasm behavior is included because those targets are outside this repository's support contract. +## Picker evidence (WI-3060) + +The shared picker contract was verified on the GPUI test platform on +2026-11-02. The tests cover single-file and folder prompt shape, cancellation +as `Ok(None)`, multi-file filtering with case-insensitive extensions, and save +selection extension completion. The source audit also confirms that Windows +uses the native Shell file dialogs, while Linux uses the xdg-desktop-portal +OpenFile/SaveFile requests for both X11 and Wayland. Portal absence and other +backend failures remain `Err`; a user cancellation is not an error. + +No native desktop run was available to this worker: `DISPLAY` and +`WAYLAND_DISPLAY` are unset and this is not a Windows host. Consequently the +following manual evidence remains **unverified** rather than being inferred +from the test platform: + +| Backend | Manual status | Required run | +|---|---|---| +| Windows DirectX 11 | unverified | Open one file and a folder, cancel both, choose multiple files with a filter, and save with and without an extension. Record the Shell dialog result, returned paths, and any non-cancellation error. | +| Linux X11 | unverified | Repeat the same file/folder, cancellation, filter, and save-extension cases with the xdg-desktop-portal backend. Record portal availability and returned local paths. | +| Linux Wayland | unverified | Repeat on a Wayland compositor, recording portal availability, cancellation, URI-to-path conversion, and save-extension behavior. | + +The manual runs should also exercise an invalid/unavailable portal or +equivalent dialog failure and confirm that it is surfaced to the consumer as +an error rather than mistaken for cancellation. + ## IME evidence The GPUI input contract and the Fluent `TextInput` adapter were source-audited From 35773f36357ae80c0cd4bd04c387ca04f191b063 Mon Sep 17 00:00:00 2001 From: thedancingdeveloper <306930456+thedancingdeveloper@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:59:02 +0000 Subject: [PATCH 13/13] Fix gallery stateful layout demos Graft-Changeset: cb1825ad7265d1ed52664c4f37e31c2a207bbc4fb30c6375e21665439ead07152 Graft-Session: ses_01M6F0P806EX6T0KRJ6JSZZNFK Graft-Work-Item: WI-3081 Co-authored-by: gpt-5.6-sol Graft-Batch: 20 --- examples/gallery/src/main.rs | 38 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index deffbd3..ae183f3 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -315,7 +315,29 @@ impl Render for DownloadProgressDemo { } } -struct ThemeMatrixDemo; +struct ThemeMatrixDemo { + breadcrumb: Entity, + pagination: Entity, +} + +impl ThemeMatrixDemo { + fn new(cx: &mut Context) -> Self { + let breadcrumb = cx.new(|cx| { + Breadcrumb::new(cx) + .items([ + BreadcrumbItem::new("home", "Home"), + BreadcrumbItem::new("library", "Library"), + BreadcrumbItem::new("current", "Current"), + ]) + .current(2) + }); + let pagination = cx.new(|_| Pagination::new(4).page(1)); + Self { + breadcrumb, + pagination, + } + } +} /// Shows Arabic and Hebrew sample content under the active layout direction. /// The rows reorder (logical start/end) but the text itself is never mirrored. @@ -366,16 +388,8 @@ impl Render for ThemeMatrixDemo { .bg(colors.surface) .text_color(colors.on_neutral) .child("Theme token matrix") - .child( - Breadcrumb::new(cx) - .items([ - BreadcrumbItem::new("home", "Home"), - BreadcrumbItem::new("library", "Library"), - BreadcrumbItem::new("current", "Current"), - ]) - .current(2), - ) - .child(Pagination::new(4).page(1)) + .child(self.breadcrumb.clone()) + .child(self.pagination.clone()) .child("Neutral, accent, selected, disabled, status, border, and focus states") .child( div() @@ -1035,7 +1049,7 @@ fn main() { threads: 4.0, opacity: 80.0, }); - let matrix_demo = cx.new(|_| ThemeMatrixDemo); + let matrix_demo = cx.new(ThemeMatrixDemo::new); let drawer_demo = cx.new(DrawerDemo::new); let rtl_demo = cx.new(|cx| { cx.observe_global::(|_, cx| cx.notify())