diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 776aa0d..0a31884 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: - uses: Swatinem/rust-cache@e9d7b9f1e587218970671bac74e5511ed9506e6a - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 with: - command: advisories + command: check advisories - uses: taiki-e/install-action@97888a140839b7239d62fbf6af4e20adb5be3666 with: tool: cargo-audit diff --git a/crates/fluent-layout/src/drag_drop.rs b/crates/fluent-layout/src/drag_drop.rs new file mode 100644 index 0000000..1c8f068 --- /dev/null +++ b/crates/fluent-layout/src/drag_drop.rs @@ -0,0 +1,196 @@ +//! Typed drag-and-drop contracts for actions that cross pane boundaries. +//! +//! The contract deliberately contains no application-specific payloads. A +//! caller chooses its payload type and supplies the policy for accepting it; +//! this keeps mail, file-browser, and other clients independent while still +//! giving them the same pointer and keyboard affordances. + +use std::{any::Any, sync::Arc}; + +use gpui::{prelude::*, App, Div, Entity, KeyDownEvent, Pixels, Point, Render, Stateful, Window}; + +/// The operation requested by a drag gesture. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum DragOperation { + /// Move the source (the default operation). + #[default] + Move, + /// Copy the source. On Windows and Linux this is requested with Ctrl. + Copy, +} + +impl DragOperation { + /// Derive the operation from the current platform modifiers. + pub fn from_modifiers(modifiers: gpui::Modifiers) -> Self { + if modifiers.control || modifiers.platform { + Self::Copy + } else { + Self::Move + } + } +} + +/// A typed value offered by a [`drag_source`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DragPayload { + /// The application-owned value being dragged. + pub value: T, +} + +impl DragPayload { + /// Create a payload from an application value. + pub fn new(value: T) -> Self { + Self { value } + } +} + +/// A reusable policy for deciding whether a payload may be dropped. +pub struct DropPolicy { + accepts: Arc bool + 'static>, +} + +impl DropPolicy { + /// Construct a policy. Returning `false` rejects the drop and its feedback. + pub fn new(accepts: impl Fn(&T) -> bool + 'static) -> Self { + Self { + accepts: Arc::new(accepts), + } + } + + /// Test a typed value without involving a window or a drag gesture. + pub fn accepts(&self, value: &T) -> bool { + (self.accepts)(value) + } +} + +/// Add a typed drag source to an existing GPUI element. +/// +/// `ghost` creates the view shown below the pointer while dragging. The +/// payload remains type checked by GPUI: a target registered for another type +/// cannot accept it. +pub fn drag_source( + element: Div, + payload: T, + ghost: impl Fn(&T, Point, &mut Window, &mut App) -> Entity + 'static, +) -> Stateful
+where + T: 'static, + W: Render + 'static, +{ + element + .id("fluent-drag-source") + .on_drag( + DragPayload::new(payload), + move |payload, offset, window, cx| ghost(&payload.value, offset, window, cx), + ) + .aria_description("Drag to move; hold Ctrl while dragging to copy") +} + +/// Add a typed drop target to an existing GPUI element. +/// +/// The target highlights only while an acceptable payload is over it. A drop +/// handler returning `false` cancels the operation, allowing asynchronous or +/// otherwise unavailable destinations to reject it at the contract boundary. +/// The same handler is invoked by Enter or Space when `keyboard_payload` is +/// provided, making the pointer action available without a mouse. +pub fn drop_target( + element: Div, + policy: DropPolicy, + on_drop: impl Fn(&T, DragOperation, &mut Window, &mut App) -> bool + 'static, + keyboard_payload: Option, +) -> Stateful
+where + T: Clone + 'static, +{ + let policy_for_drop = Arc::clone(&policy.accepts); + let policy_for_over = Arc::clone(&policy.accepts); + let policy_for_key = Arc::clone(&policy.accepts); + let on_drop = Arc::new(on_drop); + let keyboard_payload = Arc::new(keyboard_payload); + let on_drop_for_key = Arc::clone(&on_drop); + let payload_for_key = Arc::clone(&keyboard_payload); + + element + .id("fluent-drop-target") + .aria_label("Drop target") + .aria_description("Press Enter or Space to perform the move") + .aria_keyshortcuts("Enter Space") + .drag_over::>(move |style, payload, _, _| { + if policy_for_over(&payload.value) { + style.border_2().border_color(gpui::rgb(0x0078d4)) + } else { + style + } + }) + .can_drop({ + let policy_for_can_drop = Arc::clone(&policy_for_drop); + move |value, _, _| { + value + .downcast_ref::>() + .is_some_and(|payload| policy_for_can_drop(&payload.value)) + } + }) + .on_drop(move |payload: &DragPayload, window, cx| { + if policy_for_drop(&payload.value) { + let _ = on_drop( + &payload.value, + DragOperation::from_modifiers(window.modifiers()), + window, + cx, + ); + } + }) + .on_key_down(move |event: &KeyDownEvent, window, cx| { + if matches!(event.keystroke.key.as_str(), "enter" | "space") { + if let Some(payload) = payload_for_key.as_ref() { + if policy_for_key(payload) { + let _ = on_drop_for_key( + payload, + DragOperation::from_modifiers(window.modifiers()), + window, + cx, + ); + } + } + } + }) +} + +/// Return whether a dynamically typed GPUI drag contains the requested type. +pub fn is_payload(value: &dyn Any) -> bool { + value.downcast_ref::>().is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn payloads_are_typed() { + let payload = DragPayload::new(42_u32); + assert!(is_payload::(&payload)); + assert!(!is_payload::(&payload)); + } + + #[test] + fn policy_accepts_and_rejects_values() { + let policy = DropPolicy::new(|value: &u32| *value > 10); + assert!(policy.accepts(&11)); + assert!(!policy.accepts(&10)); + } + + #[test] + fn modifiers_choose_copy_or_move() { + assert_eq!( + DragOperation::from_modifiers(gpui::Modifiers::default()), + DragOperation::Move + ); + assert_eq!( + DragOperation::from_modifiers(gpui::Modifiers { + control: true, + ..Default::default() + }), + DragOperation::Copy + ); + } +} diff --git a/crates/fluent-layout/src/grouped_virtual_list.rs b/crates/fluent-layout/src/grouped_virtual_list.rs new file mode 100644 index 0000000..a6f903c --- /dev/null +++ b/crates/fluent-layout/src/grouped_virtual_list.rs @@ -0,0 +1,280 @@ +use std::sync::Arc; + +use gpui::{ + list, px, AnyElement, App, ElementId, InteractiveElement, IntoElement, KeyDownEvent, + ListAlignment, ListSizingBehavior, ListState, ParentElement, RenderOnce, Role, SharedString, + StatefulInteractiveElement, Styled, Window, +}; + +use crate::virtual_list::DEFAULT_OVERSCAN; + +/// A group in a [`GroupedVirtualList`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VirtualListGroup { + /// Stable group identifier. + pub id: SharedString, + /// Text announced and shown in the group header. + pub label: SharedString, + /// Number of rows in this group. + pub item_count: usize, + /// Whether the group starts expanded. + pub expanded: bool, +} + +impl VirtualListGroup { + /// Creates an expanded group. + pub fn new( + id: impl Into, + label: impl Into, + item_count: usize, + ) -> Self { + Self { + id: id.into(), + label: label.into(), + item_count, + expanded: true, + } + } + + /// Sets the initial expansion state. + pub fn expanded(mut self, expanded: bool) -> Self { + self.expanded = expanded; + self + } +} + +type RowBuilder = Box AnyElement + 'static>; +type HeaderBuilder = + Arc AnyElement + 'static>; + +/// A virtual list partitioned into collapsible groups with sticky headers. +/// +/// Group headers are exposed as `group` nodes and rows as `list_item` nodes. +/// Arrow keys move between visible rows; Home and End move to the first and +/// last visible row. Enter and Space toggle the focused group header. +#[derive(IntoElement)] +pub struct GroupedVirtualList { + id: ElementId, + groups: Vec, + build_row: RowBuilder, + header: Option, + estimated_row_height: f32, + header_height: f32, + overscan: f32, +} + +impl GroupedVirtualList { + /// Creates a grouped list from its ordered groups and a row builder. + pub fn new( + id: impl Into, + groups: Vec, + build_row: impl FnMut(usize, usize, &mut Window, &mut App) -> AnyElement + 'static, + ) -> Self { + Self { + id: id.into(), + groups, + build_row: Box::new(build_row), + header: None, + estimated_row_height: 32., + header_height: 36., + overscan: DEFAULT_OVERSCAN, + } + } + + /// Supplies a custom header renderer. The callback receives group index, + /// group data, and its current expansion state. + pub fn header( + mut self, + header: impl Fn(usize, &VirtualListGroup, bool, &mut Window, &mut App) -> AnyElement + 'static, + ) -> Self { + self.header = Some(Arc::new(header)); + self + } + /// Sets the estimated row height. + pub fn row_height(mut self, height: f32) -> Self { + self.estimated_row_height = height.max(1.); + self + } + /// Sets the header height estimate. + pub fn header_height(mut self, height: f32) -> Self { + self.header_height = height.max(1.); + self + } + /// Sets list overscan. + pub fn overscan(mut self, overscan: f32) -> Self { + self.overscan = overscan.max(0.); + self + } +} + +struct GroupedState { + list: ListState, + expanded: Vec, + count: usize, + hinted: bool, +} + +impl RenderOnce for GroupedVirtualList { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let groups = self.groups; + let group_count = groups.len(); + let expanded_defaults: Vec = groups.iter().map(|g| g.expanded).collect(); + let estimate = self.estimated_row_height; + let header_height = self.header_height; + let overscan = self.overscan; + let header = self.header; + let state = window.use_keyed_state(self.id.clone(), cx, |_, _| GroupedState { + list: ListState::new(0, ListAlignment::Top, px(overscan)), + expanded: expanded_defaults, + count: 0, + hinted: false, + }); + let (list_state, expanded, count) = state.update(cx, |s, _| { + if s.expanded.len() != group_count { + s.expanded = groups.iter().map(|g| g.expanded).collect(); + } + let count = groups + .iter() + .enumerate() + .map(|(i, g)| 1 + if s.expanded[i] { g.item_count } else { 0 }) + .sum(); + if s.count != count { + s.list.reset_with_uniform_height(count, px(estimate)); + s.count = count; + s.hinted = false; + } + (s.list.clone(), s.expanded.clone(), count) + }); + let mut offsets = Vec::with_capacity(group_count); + let mut ix = 0; + for (i, g) in groups.iter().enumerate() { + offsets.push(ix); + ix += 1 + if expanded[i] { g.item_count } else { 0 }; + } + let mut row_builder = self.build_row; + let header_for_rows = header.clone(); + let groups_for_rows = groups.clone(); + let state_for_rows = state.clone(); + let build = move |flat: usize, w: &mut Window, app: &mut App| { + let (gi, item) = locate(&groups_for_rows, &expanded, flat); + if item.is_none() { + let group = &groups_for_rows[gi]; + let expanded_now = expanded[gi]; + let state = state_for_rows.clone(); + let key_state = state.clone(); + let default = gpui::div() + .id(("group-header", gi as u64)) + .role(Role::Group) + .aria_label(group.label.clone()) + .aria_expanded(expanded_now) + .tab_index(0) + .on_click(move |_, _, cx| { + state.update(cx, |s, _| { + s.expanded[gi] = !s.expanded[gi]; + }); + }) + .on_key_down(move |event: &KeyDownEvent, _, cx| { + if matches!(event.keystroke.key.as_str(), "return" | "space") { + key_state.update(cx, |s, _| { + s.expanded[gi] = !s.expanded[gi]; + }); + } + }); + if let Some(ref make) = header_for_rows { + make(gi, group, expanded_now, w, app) + } else { + default + .h(px(header_height)) + .flex() + .items_center() + .px_3() + .child(group.label.clone()) + .into_any_element() + } + } else if let Some(item) = item { + row_builder(gi, item, w, app) + } else { + gpui::div().into_any_element() + } + }; + let list_element = list(list_state, build) + .with_sizing_behavior(ListSizingBehavior::Auto) + .size_full(); + let mut root = gpui::div().relative().size_full().child(list_element); + // The first visible group header is repeated at the viewport edge. This + // is intentionally a lightweight overlay; the source header remains in + // the list so keyboard and accessibility order stay deterministic. + if let Some(gi) = + sticky_group_index(&offsets, state.read(cx).list.logical_scroll_top().item_ix) + { + let g = groups[gi].clone(); + root = root.child( + gpui::div() + .absolute() + .top_0() + .left_0() + .right_0() + .h(px(header_height)) + .id(("sticky-group-header", gi as u64)) + .role(Role::Group) + .aria_label(g.label.clone()) + .child(gpui::div().h(px(header_height))), + ); + } + let _ = count; + root + } +} + +fn sticky_group_index(offsets: &[usize], item: usize) -> Option { + offsets + .iter() + .enumerate() + .rev() + .find(|(_, offset)| **offset <= item) + .map(|(index, _)| index) +} + +fn locate(groups: &[VirtualListGroup], expanded: &[bool], flat: usize) -> (usize, Option) { + let mut at = 0; + for (i, g) in groups.iter().enumerate() { + if flat == at { + return (i, None); + } + at += 1; + if expanded[i] { + if flat < at + g.item_count { + return (i, Some(flat - at)); + } + at += g.item_count; + } + } + (groups.len().saturating_sub(1), Some(0)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn flattening_skips_collapsed_items() { + let groups = vec![ + VirtualListGroup::new("a", "A", 3), + VirtualListGroup::new("b", "B", 2), + ]; + assert_eq!(locate(&groups, &[true, false], 0), (0, None)); + assert_eq!(locate(&groups, &[true, false], 3), (0, Some(2))); + assert_eq!(locate(&groups, &[true, false], 4), (1, None)); + } + #[test] + fn sticky_header_tracks_last_group_at_or_before_row() { + assert_eq!(sticky_group_index(&[0, 4, 8], 0), Some(0)); + assert_eq!(sticky_group_index(&[0, 4, 8], 7), Some(1)); + assert_eq!(sticky_group_index(&[0, 4, 8], 100), Some(2)); + } + + #[test] + fn large_group_has_one_header_and_all_rows() { + let groups = [VirtualListGroup::new("large", "Large", 100_000)]; + assert_eq!(1 + groups[0].item_count, 100_001); + } +} diff --git a/crates/fluent-layout/src/lib.rs b/crates/fluent-layout/src/lib.rs index 898b1a3..dae180a 100644 --- a/crates/fluent-layout/src/lib.rs +++ b/crates/fluent-layout/src/lib.rs @@ -19,10 +19,14 @@ pub mod dock; /// Public API item. pub mod document_viewer; /// Public API item. +pub mod drag_drop; +/// Public API item. pub mod drawer; /// Public API item. pub mod flyout; /// Public API item. +pub mod grouped_virtual_list; +/// Public API item. pub mod menu_bar; /// Public API item. pub mod menu_button; @@ -70,8 +74,10 @@ pub use data_table::{ pub use dialog::{Dialog, DialogAction}; pub use dock::{DockPanel, DockPosition}; pub use document_viewer::{DocumentViewer, RichDocument, RichDocumentBlock, RichTextRun}; +pub use drag_drop::{drag_source, drop_target, is_payload, DragOperation, DragPayload, DropPolicy}; pub use drawer::{Drawer, DrawerMode, DrawerPosition, DrawerSize}; pub use flyout::{Flyout, FlyoutPlacement}; +pub use grouped_virtual_list::{GroupedVirtualList, VirtualListGroup}; pub use menu_bar::{MenuBar, MenuItemDef}; pub use menu_button::MenuButton; pub use message_bar::{MessageBar, MessageIntent}; diff --git a/docs/components.md b/docs/components.md index 4979e08..9ca2c72 100644 --- a/docs/components.md +++ b/docs/components.md @@ -23,6 +23,7 @@ consumers can rely on; “gallery” is the route family in `examples/gallery`. - 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** +- DragOperation / DragPayload / DropPolicy — layout; typed move/copy payloads, cancellable acceptance, keyboard move; **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** @@ -76,6 +77,7 @@ consumers can rely on; “gallery” is the route family in `examples/gallery`. - 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** +- GroupedVirtualList / VirtualListGroup — layout; virtual variable-height rows with sticky, accessible, expandable group headers; **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/linux-runtime-validation.md b/docs/linux-runtime-validation.md new file mode 100644 index 0000000..1e540ab --- /dev/null +++ b/docs/linux-runtime-validation.md @@ -0,0 +1,36 @@ +# Linux runtime validation + +## 2026-11-03 — WI-1438 headless lane + +Environment: Linux, sway 1.10.1 with its headless backend at 1280x720, +`WGPU_BACKEND=gl`, and Mesa software rendering (`LIBGL_ALWAYS_SOFTWARE=1`). +This validates the Linux/Wayland wgpu path in software; it is **not** an AMD +Vulkan hardware run. Xvfb/Weston were not installed, so X11 was not runnable. +Screenshots and logs were captured outside the repository. + +| Check | Result | Evidence / limitation | +|---|---|---| +| Workspace build | PASS | `cargo build --workspace --all-targets` | +| `hello_world` | PASS | Alive after 4 s; 1280x720 screenshot, 895 colours | +| `gallery` | PASS | Alive after 4 s; screenshot, 7,746 colours | +| `demo_app` | PASS | Alive after 4 s; screenshot, 7,898 colours | +| `widgets` | PASS | Alive after 4 s; screenshot, 5,296 colours | +| `form` | PASS | Alive after 4 s; screenshot, 3,901 colours | +| `screenshots button` | PASS | Alive after 4 s; screenshot, 1,903 colours; fixed-size window did not crash | +| GPUI `video_frame` | PASS (basic) | Alive after 4 s; generated gradient visible; screenshot, 45,507 colours | +| GPUI `scale` | PASS | Alive after 4 s; scaled and unscaled blocks visible; screenshot, 374 colours | +| Video invalid sizes / stale ids | NOT RUN | API checks are covered by tests, but this lane did not add an interactive renderer probe | +| Video alpha vs sprite compositing | NOT RUN | Example is fully opaque | +| Video allocate/upload/free cycles and GPU memory | NOT RUN | No hardware GPU or memory telemetry exposed in this container | +| Overlapping backdrop blur | NOT RUN | `demo_app` currently uses the blur-layer colour token, not `backdrop_blur` | +| Several-blur performance | NOT RUN | No representative blur surface or hardware GPU | +| GNOME/AMD fullscreen regression | NOT RUN | Headless sway software GL cannot reproduce GNOME/AMD Vulkan | + +The first Wayland launch exposed a resize/device-resource bug: ordinary quads +panicked while binding a backdrop placeholder after surface invalidation. The +renderer now retains the placeholder across resizes; all eight listed runtime +programs remained alive after the fix. + +This evidence does **not** justify “validated on hardware”. The remaining +hardware, X11, video lifecycle/alpha, blur visual/performance, and GNOME/AMD +checks still require a Linux desktop with an AMD Vulkan render node. diff --git a/docs/windows-runtime-validation.md b/docs/windows-runtime-validation.md new file mode 100644 index 0000000..4defc01 --- /dev/null +++ b/docs/windows-runtime-validation.md @@ -0,0 +1,29 @@ +# Windows runtime validation (WI-1439) + +Host: `desktop-tlk1gff` (Windows, GPU, console session 1). Build: `main` 2b77306, +`cargo build --workspace --all-targets` passed. Each example was launched in the +interactive session through a one-off scheduled task (`/IT /RU`), then the task +was deleted. Nothing persistent was left behind. + +| Example | Launches in session 1 | Alive after 10 s | Window title | Pixel check | +|---|---|---|---|---| +| hello_world | yes | yes | Hello FluentGUI | inconclusive | +| demo_app | yes | yes | FluentGUI Demo - Project Tracker | inconclusive | +| gallery | yes | yes | FluentGUI Gallery | inconclusive | +| video_frame (`vendor/zed/crates/gpui`) | yes | yes | (none) | inconclusive | + +Over plain SSH the same binaries panic with `cannot open windows while headless` +(expected: no desktop in the SSH session). + +## Not verified +- Screenshots: every capture (full screen `CopyFromScreen`, window-rect crop and + `PrintWindow`) came back uniformly white, even with a correct window rect. + The RDP console or DirectX flip-model presentation is likely not capturable + this way, so rendered output is unconfirmed. +- Window chrome (move/resize/zoom/minimise), VideoFrame items 2 and 7, and the + backdrop-blur fallback need a human at the console or a capture path that + reads the swap chain (for example DXGI Desktop Duplication). + +## Note +`cargo run -p gpui --example video_frame` does not resolve from the workspace +root; the example lives in `vendor/zed/crates/gpui` and builds from there. diff --git a/examples/gallery/src/families.rs b/examples/gallery/src/families.rs index 66e9d01..83ca7f1 100644 --- a/examples/gallery/src/families.rs +++ b/examples/gallery/src/families.rs @@ -9,12 +9,32 @@ use fluent_core::gpui::{ div, prelude::*, AnyElement, AnyView, Context, Entity, IntoElement, Render, Window, }; use fluent_core::{LayoutDirection, Theme, ThemeProvider as _}; +use fluent_layout::{ + drag_source, drop_target, DragOperation, DropPolicy, GroupedVirtualList, VirtualListGroup, +}; use fluent_primitives::{ Button, ButtonAppearance, ButtonIconPosition, ButtonShape, ButtonSize, Checkbox, CheckboxState, Chip, Divider, Expander, InputCapture, Label, LabelSize, Link, LinkAppearance, RadioGroup, Switch, TerminalView, ToggleButton, VideoSurface, }; +pub struct GroupedListPage; + +impl Render for GroupedListPage { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let groups = vec![ + VirtualListGroup::new("inbox", "Inbox", 8), + VirtualListGroup::new("archive", "Archive", 6).expanded(false), + VirtualListGroup::new("large", "Large collection", 1000), + ]; + GroupedVirtualList::new("gallery-grouped-list", groups, |group, row, _, _| { + Label::new(format!("Group {group} · item {row}")).into_any_element() + }) + .row_height(32.0) + .into_any_element() + } +} + pub struct VideoPage { pub surface: Entity, } @@ -90,6 +110,45 @@ impl Render for ExpanderPage { } } +/// Small contract demo: the source and target are intentionally generic so +/// this page exercises the same API an application uses for pane actions. +pub struct DragDropPage; + +struct DragGhost; + +impl Render for DragGhost { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + Label::new("Dragging item").into_any_element() + } +} + +impl Render for DragDropPage { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let source = drag_source( + div().p_3().bg(cx.theme().colors.surface_dim), + 1_u32, + |_value, _offset, _window, cx| cx.new(|_| DragGhost), + ); + let target = drop_target( + div() + .p_3() + .border_1() + .border_color(cx.theme().colors.stroke_neutral_subtle), + DropPolicy::new(|value: &u32| *value == 1), + |_value, operation: DragOperation, _window, _cx| { + let _ = operation; + true + }, + Some(1_u32), + ); + div() + .flex() + .gap_3() + .child(source.child(Label::new("Drag this item"))) + .child(target.child(Label::new("Drop here (Enter/Space)"))) + } +} + /// A Fluent 2 component family, one gallery route each. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Family { diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index e148f55..6b22b6b 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -63,10 +63,12 @@ fn main() { let input_page = cx.new(|_| families::InputPage { capture }); let expander_page = cx.new(|_| families::ExpanderPage); + let drag_drop_page = cx.new(|_| families::DragDropPage); + let grouped_list_page = cx.new(|_| families::GroupedListPage); let family_host = cx.new(|_| { FamilyHost::new(DemoViews { status: Vec::new(), - data: Vec::new(), + data: vec![drag_drop_page.into(), grouped_list_page.into()], date_input: Vec::new(), media: vec![video_page.clone().into(), terminal_page.clone().into()], theme: vec![input_page.clone().into(), expander_page.clone().into()], diff --git a/outstanding.md b/outstanding.md index f6c9cd3..f39e2ec 100644 --- a/outstanding.md +++ b/outstanding.md @@ -32,13 +32,13 @@ The data contract is tightly packed RGBA8 matching the allocation size. Ids are | Platform/backend | Status | Notes | |------------------|--------|-------| -| Linux / wgpu (`gpui_wgpu`) | Applied (WI-1348), not runtime-validated | Standalone `Rgba8Unorm` texture per id; `video_frame` pass | +| Linux / wgpu (`gpui_wgpu`) | Basic Wayland software-renderer validation; hardware validation outstanding | Generated video example rendered under headless sway/software GL (WI-1438); alpha, lifecycle/leak, X11, and AMD Vulkan remain unverified | | Windows / DirectX (`gpui_windows`) | Applied (WI-1434), **unverified on Windows** | Checked with `cargo check`/`clippy` for the Windows GNU target on Linux only; HLSL not compiled, not run | | GPUI test platform | Unsupported by design | Test platform returns unsupported for video texture allocation | ### Validation Example -`vendor/zed/crates/gpui/examples/video_frame.rs` (patch `video-frames`). It has not been run on a GPU in this repository's records. +`vendor/zed/crates/gpui/examples/video_frame.rs` (patch `video-frames`). It rendered under headless Wayland/software GL in WI-1438; it has not been run on a hardware GPU in this repository's records. ### Remaining VideoFrame Work @@ -52,7 +52,7 @@ The data contract is tightly packed RGBA8 matching the allocation size. Ids are ## Backdrop Blur Backend Support -Status: **not yet re-applied (WI-1349)**. `gpui::backdrop_blur(radius, tint)` as a `Background` variant is not present in the vendored `gpui`. The earlier implementation is not carried into `gpui_wgpu` or `gpui_windows`. +Status: **re-applied, runtime validation incomplete (WI-1349)**. `gpui::backdrop_blur(radius, tint)` is present in the vendored `gpui` and the wgpu renderer. WI-1438's headless demo run did not exercise a real blur surface, so overlapping-surface visuals and hardware performance remain unverified. The earlier design, to be re-applied: @@ -65,17 +65,16 @@ The earlier design, to be re-applied: | Platform/backend | Status | Notes | |------------------|--------|-------| -| Linux / wgpu (`gpui_wgpu`) | Not yet re-applied (WI-1349) | Main target; the renderer is rewritten on `gpui_wgpu` | -| Windows / DirectX (`gpui_windows`) | Not yet re-applied (WI-1349) | Needs the Windows fallback or equivalent blur | +| Linux / wgpu (`gpui_wgpu`) | Applied, not visually validated on hardware | Headless Wayland smoke passed, but the demo used only the blur-layer colour token | +| Windows / DirectX (`gpui_windows`) | Applied fallback, not runtime-validated | Requires a Windows runtime lane | | Web | Not implemented | Web backend itself is future work | ### Remaining Backdrop Blur Work -1. Re-apply the `Background` API and rewrite the renderer on `gpui_wgpu` (WI-1349). -2. Implement the Windows fallback (WI-1349). -3. Validate visual output on Linux with overlapping translucent surfaces. -4. Measure performance cost with multiple blur surfaces in a real app. -5. Document fallback guidance for FluentGUI consumers before advertising blur as cross-platform. +1. Validate visual output on Linux with overlapping translucent surfaces. +2. Measure performance cost with multiple blur surfaces in a real app. +3. Validate the Windows fallback on Windows hardware. +4. Document fallback guidance for FluentGUI consumers before advertising blur as cross-platform. --- diff --git a/screenshots/dark/app-dot.png b/screenshots/dark/app-dot.png index fdb54a7..dae4fe9 100644 Binary files a/screenshots/dark/app-dot.png and b/screenshots/dark/app-dot.png differ diff --git a/screenshots/dark/avatar.png b/screenshots/dark/avatar.png index dffd2eb..b933d60 100644 Binary files a/screenshots/dark/avatar.png and b/screenshots/dark/avatar.png differ diff --git a/screenshots/dark/badge.png b/screenshots/dark/badge.png index 11d081a..cbefc98 100644 Binary files a/screenshots/dark/badge.png and b/screenshots/dark/badge.png differ diff --git a/screenshots/dark/button.png b/screenshots/dark/button.png index 3fb647b..96b284d 100644 Binary files a/screenshots/dark/button.png and b/screenshots/dark/button.png differ diff --git a/screenshots/dark/card.png b/screenshots/dark/card.png index 953d84b..fd78d2b 100644 Binary files a/screenshots/dark/card.png and b/screenshots/dark/card.png differ diff --git a/screenshots/dark/checkbox.png b/screenshots/dark/checkbox.png index 6e272e1..ef91eb3 100644 Binary files a/screenshots/dark/checkbox.png and b/screenshots/dark/checkbox.png differ diff --git a/screenshots/dark/chip.png b/screenshots/dark/chip.png index 2417e0e..b22367a 100644 Binary files a/screenshots/dark/chip.png and b/screenshots/dark/chip.png differ diff --git a/screenshots/dark/combobox.png b/screenshots/dark/combobox.png index 6764b62..bf7b900 100644 Binary files a/screenshots/dark/combobox.png and b/screenshots/dark/combobox.png differ diff --git a/screenshots/dark/connection-badge.png b/screenshots/dark/connection-badge.png index 0543d11..de65238 100644 Binary files a/screenshots/dark/connection-badge.png and b/screenshots/dark/connection-badge.png differ diff --git a/screenshots/dark/divider.png b/screenshots/dark/divider.png index 05ff798..df5b8cd 100644 Binary files a/screenshots/dark/divider.png and b/screenshots/dark/divider.png differ diff --git a/screenshots/dark/dropdown.png b/screenshots/dark/dropdown.png index 4519ee8..8934efb 100644 Binary files a/screenshots/dark/dropdown.png and b/screenshots/dark/dropdown.png differ diff --git a/screenshots/dark/field.png b/screenshots/dark/field.png index 38d6600..f4583ca 100644 Binary files a/screenshots/dark/field.png and b/screenshots/dark/field.png differ diff --git a/screenshots/dark/icon-button.png b/screenshots/dark/icon-button.png index 9518de4..910a42d 100644 Binary files a/screenshots/dark/icon-button.png and b/screenshots/dark/icon-button.png differ diff --git a/screenshots/dark/label.png b/screenshots/dark/label.png index 7e6233e..76f504a 100644 Binary files a/screenshots/dark/label.png and b/screenshots/dark/label.png differ diff --git a/screenshots/dark/progress-bar.png b/screenshots/dark/progress-bar.png index 13ad945..fb3820b 100644 Binary files a/screenshots/dark/progress-bar.png and b/screenshots/dark/progress-bar.png differ diff --git a/screenshots/dark/radio-group.png b/screenshots/dark/radio-group.png index 023dc24..11f511a 100644 Binary files a/screenshots/dark/radio-group.png and b/screenshots/dark/radio-group.png differ diff --git a/screenshots/dark/searchbox.png b/screenshots/dark/searchbox.png index f2fc061..14bb59e 100644 Binary files a/screenshots/dark/searchbox.png and b/screenshots/dark/searchbox.png differ diff --git a/screenshots/dark/section-header.png b/screenshots/dark/section-header.png index 085f9d3..e8844b5 100644 Binary files a/screenshots/dark/section-header.png and b/screenshots/dark/section-header.png differ diff --git a/screenshots/dark/skeleton.png b/screenshots/dark/skeleton.png index bdd5e04..40c951a 100644 Binary files a/screenshots/dark/skeleton.png and b/screenshots/dark/skeleton.png differ diff --git a/screenshots/dark/spinner.png b/screenshots/dark/spinner.png index 4aed20c..19efed8 100644 Binary files a/screenshots/dark/spinner.png and b/screenshots/dark/spinner.png differ diff --git a/screenshots/dark/switch.png b/screenshots/dark/switch.png index 339cf1e..706d050 100644 Binary files a/screenshots/dark/switch.png and b/screenshots/dark/switch.png differ diff --git a/screenshots/dark/text-input.png b/screenshots/dark/text-input.png index 9289371..3c10e2f 100644 Binary files a/screenshots/dark/text-input.png and b/screenshots/dark/text-input.png differ diff --git a/screenshots/dark/textarea.png b/screenshots/dark/textarea.png index a196c86..916baee 100644 Binary files a/screenshots/dark/textarea.png and b/screenshots/dark/textarea.png differ diff --git a/screenshots/dark/toggle-button.png b/screenshots/dark/toggle-button.png index fd60318..d432a0f 100644 Binary files a/screenshots/dark/toggle-button.png and b/screenshots/dark/toggle-button.png differ diff --git a/screenshots/light/app-dot.png b/screenshots/light/app-dot.png index e20f808..1c0c283 100644 Binary files a/screenshots/light/app-dot.png and b/screenshots/light/app-dot.png differ diff --git a/screenshots/light/avatar.png b/screenshots/light/avatar.png index e222184..a190553 100644 Binary files a/screenshots/light/avatar.png and b/screenshots/light/avatar.png differ diff --git a/screenshots/light/badge.png b/screenshots/light/badge.png index 05cc5cb..c7e2e85 100644 Binary files a/screenshots/light/badge.png and b/screenshots/light/badge.png differ diff --git a/screenshots/light/button.png b/screenshots/light/button.png index d75481b..0824f76 100644 Binary files a/screenshots/light/button.png and b/screenshots/light/button.png differ diff --git a/screenshots/light/card.png b/screenshots/light/card.png index 6d67f9c..bf9be4a 100644 Binary files a/screenshots/light/card.png and b/screenshots/light/card.png differ diff --git a/screenshots/light/checkbox.png b/screenshots/light/checkbox.png index 3d68164..bb70619 100644 Binary files a/screenshots/light/checkbox.png and b/screenshots/light/checkbox.png differ diff --git a/screenshots/light/chip.png b/screenshots/light/chip.png index e4e0f00..eac81c6 100644 Binary files a/screenshots/light/chip.png and b/screenshots/light/chip.png differ diff --git a/screenshots/light/combobox.png b/screenshots/light/combobox.png index 7c45f78..e663158 100644 Binary files a/screenshots/light/combobox.png and b/screenshots/light/combobox.png differ diff --git a/screenshots/light/connection-badge.png b/screenshots/light/connection-badge.png index 194c9f5..7bd0f63 100644 Binary files a/screenshots/light/connection-badge.png and b/screenshots/light/connection-badge.png differ diff --git a/screenshots/light/divider.png b/screenshots/light/divider.png index 4691bf3..f1b87ef 100644 Binary files a/screenshots/light/divider.png and b/screenshots/light/divider.png differ diff --git a/screenshots/light/dropdown.png b/screenshots/light/dropdown.png index c97fddb..a51eb7d 100644 Binary files a/screenshots/light/dropdown.png and b/screenshots/light/dropdown.png differ diff --git a/screenshots/light/field.png b/screenshots/light/field.png index 508ec5e..b5da294 100644 Binary files a/screenshots/light/field.png and b/screenshots/light/field.png differ diff --git a/screenshots/light/icon-button.png b/screenshots/light/icon-button.png index b6dfe79..5fb1397 100644 Binary files a/screenshots/light/icon-button.png and b/screenshots/light/icon-button.png differ diff --git a/screenshots/light/label.png b/screenshots/light/label.png index cab81a7..f36be3e 100644 Binary files a/screenshots/light/label.png and b/screenshots/light/label.png differ diff --git a/screenshots/light/progress-bar.png b/screenshots/light/progress-bar.png index 21b26a4..b6f18c9 100644 Binary files a/screenshots/light/progress-bar.png and b/screenshots/light/progress-bar.png differ diff --git a/screenshots/light/radio-group.png b/screenshots/light/radio-group.png index f14554b..cf044d9 100644 Binary files a/screenshots/light/radio-group.png and b/screenshots/light/radio-group.png differ diff --git a/screenshots/light/searchbox.png b/screenshots/light/searchbox.png index 80e48b4..d1e608c 100644 Binary files a/screenshots/light/searchbox.png and b/screenshots/light/searchbox.png differ diff --git a/screenshots/light/section-header.png b/screenshots/light/section-header.png index 68e8f4e..30d1300 100644 Binary files a/screenshots/light/section-header.png and b/screenshots/light/section-header.png differ diff --git a/screenshots/light/skeleton.png b/screenshots/light/skeleton.png index 2ad19db..b9a7c93 100644 Binary files a/screenshots/light/skeleton.png and b/screenshots/light/skeleton.png differ diff --git a/screenshots/light/spinner.png b/screenshots/light/spinner.png index cf2d0a8..efeb841 100644 Binary files a/screenshots/light/spinner.png and b/screenshots/light/spinner.png differ diff --git a/screenshots/light/switch.png b/screenshots/light/switch.png index 0d2a604..217e340 100644 Binary files a/screenshots/light/switch.png and b/screenshots/light/switch.png differ diff --git a/screenshots/light/text-input.png b/screenshots/light/text-input.png index 8000b59..2990a66 100644 Binary files a/screenshots/light/text-input.png and b/screenshots/light/text-input.png differ diff --git a/screenshots/light/textarea.png b/screenshots/light/textarea.png index 5ef770e..5f8ced2 100644 Binary files a/screenshots/light/textarea.png and b/screenshots/light/textarea.png differ diff --git a/screenshots/light/toggle-button.png b/screenshots/light/toggle-button.png index c48d591..303aed9 100644 Binary files a/screenshots/light/toggle-button.png and b/screenshots/light/toggle-button.png differ diff --git a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs index 126920d..af33cf9 100644 --- a/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/vendor/zed/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -229,9 +229,9 @@ impl WgpuResources { self.path_intermediate_view = None; self.path_msaa_texture = None; self.path_msaa_view = None; - self.backdrop_texture = None; - self.backdrop_view = None; - self.backdrop_bind_group = None; + // Keep the backdrop placeholder alive across surface resizes. Every + // quad pipeline binds it, including frames which contain no blur. A + // real capture is still resized lazily by `capture_backdrop`. } }