add empty state
This commit is contained in:
@@ -36,6 +36,7 @@ full = [
|
|||||||
"menu",
|
"menu",
|
||||||
"select",
|
"select",
|
||||||
"dialog",
|
"dialog",
|
||||||
|
"empty_state",
|
||||||
"alert",
|
"alert",
|
||||||
"toast",
|
"toast",
|
||||||
]
|
]
|
||||||
@@ -58,5 +59,6 @@ tooltip = []
|
|||||||
menu = ["scroll_area"]
|
menu = ["scroll_area"]
|
||||||
select = ["menu"]
|
select = ["menu"]
|
||||||
dialog = []
|
dialog = []
|
||||||
|
empty_state = []
|
||||||
alert = []
|
alert = []
|
||||||
toast = []
|
toast = []
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
App, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _,
|
||||||
|
Window, div, relative,
|
||||||
|
};
|
||||||
|
use gpui_native_icons::{IconRole, icon};
|
||||||
|
use gpui_native_theme::{ActiveTheme as _, Size};
|
||||||
|
|
||||||
|
use crate::common::{StyledExt, v_flex};
|
||||||
|
|
||||||
|
/// The centered "nothing here yet" panel for lists, tables, and searches.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct EmptyState {
|
||||||
|
icon: Option<IconRole>,
|
||||||
|
headline: SharedString,
|
||||||
|
hint: Option<SharedString>,
|
||||||
|
action: Option<gpui::AnyElement>,
|
||||||
|
style: StyleRefinement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmptyState {
|
||||||
|
/// Creates an empty state whose headline is the primary statement.
|
||||||
|
pub fn new<L>(headline: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
icon: None,
|
||||||
|
headline: headline.into(),
|
||||||
|
hint: None,
|
||||||
|
action: None,
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a large muted icon above the headline.
|
||||||
|
pub fn icon(mut self, role: IconRole) -> Self {
|
||||||
|
self.icon = Some(role);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a secondary line under the headline.
|
||||||
|
pub fn hint<L>(mut self, hint: L) -> Self
|
||||||
|
where
|
||||||
|
L: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.hint = Some(hint.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets an action element — usually a button — under the hint.
|
||||||
|
pub fn action<E>(mut self, action: E) -> Self
|
||||||
|
where
|
||||||
|
E: IntoElement,
|
||||||
|
{
|
||||||
|
self.action = Some(action.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Styled for EmptyState {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for EmptyState {
|
||||||
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let metrics = theme.sizes.resolve(Size::Medium);
|
||||||
|
let muted_foreground = theme.colors.muted_foreground;
|
||||||
|
let foreground = theme.colors.foreground;
|
||||||
|
let illustration = metrics.icon_size * 2.0;
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_center()
|
||||||
|
.p_4()
|
||||||
|
.when_some(self.icon, |state, role| {
|
||||||
|
state.child(
|
||||||
|
div()
|
||||||
|
.mb_2()
|
||||||
|
.child(icon(role).size(illustration).color(muted_foreground)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(metrics.text_size)
|
||||||
|
.font_weight(gpui::FontWeight::MEDIUM)
|
||||||
|
.text_color(foreground)
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.child(self.headline),
|
||||||
|
)
|
||||||
|
.when_some(self.hint, |state, hint| {
|
||||||
|
state.child(
|
||||||
|
div()
|
||||||
|
.text_size(metrics.caption_size)
|
||||||
|
.text_color(muted_foreground)
|
||||||
|
.child(hint),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.action, |state, action| {
|
||||||
|
state.child(div().mt_3().child(action))
|
||||||
|
})
|
||||||
|
.refine_style(&self.style)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ pub mod input;
|
|||||||
#[cfg(feature = "dialog")]
|
#[cfg(feature = "dialog")]
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
|
|
||||||
|
#[cfg(feature = "empty_state")]
|
||||||
|
pub mod empty_state;
|
||||||
|
|
||||||
#[cfg(feature = "menu")]
|
#[cfg(feature = "menu")]
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -264,7 +264,7 @@ Wave M1 — unblocks the port; each through the review gate:
|
|||||||
| M2 | core | Select ✅ Landed (2026-11-01) | default-branch chooser, event-fetch-strategy setting, explore filter | bezel: field-shaped trigger over a caller-assembled menu; gpuikit: placeholder + clear. Shipped as a controlled value (`selected(Option<T>)` + `on_change(Option<T>)`, tuples in) with the popup owned per instance via element-namespace-keyed state; composes `Menu` — field-shaped trigger (`input`/`border`/`ring` tokens, `Role::ComboBox`), options as checked items. Menu gained `initial_selection` so a popup opens keyboard-highlighted on the current value, like native popups, and its panel dropped the `max_h(relative(0.75))` fraction — resolved against the wrong box inside nested flex/scroll shells, it clipped the last items (found in the gallery); row height is governed by the ScrollArea's 320 px cap and `anchored` handles window fit. Feature `select = ["menu"]`. **Follow-ups (2026-11-01):** Menu's check indicator moved to the row's right end (Adwaita dropdown look; the optional icon keeps the leading slot), and Select options gained optional leading icons — `SelectOption::new(value, label).icon(role)`, with `(value, label)` tuples still accepted through `From` |
|
| M2 | core | Select ✅ Landed (2026-11-01) | default-branch chooser, event-fetch-strategy setting, explore filter | bezel: field-shaped trigger over a caller-assembled menu; gpuikit: placeholder + clear. Shipped as a controlled value (`selected(Option<T>)` + `on_change(Option<T>)`, tuples in) with the popup owned per instance via element-namespace-keyed state; composes `Menu` — field-shaped trigger (`input`/`border`/`ring` tokens, `Role::ComboBox`), options as checked items. Menu gained `initial_selection` so a popup opens keyboard-highlighted on the current value, like native popups, and its panel dropped the `max_h(relative(0.75))` fraction — resolved against the wrong box inside nested flex/scroll shells, it clipped the last items (found in the gallery); row height is governed by the ScrollArea's 320 px cap and `anchored` handles window fit. Feature `select = ["menu"]`. **Follow-ups (2026-11-01):** Menu's check indicator moved to the row's right end (Adwaita dropdown look; the optional icon keeps the leading slot), and Select options gained optional leading icons — `SelectOption::new(value, label).icon(role)`, with `(value, label)` tuples still accepted through `From` |
|
||||||
| M3 | core | Segmented control ✅ Landed (2026-11-01) | All/Popular/Recent explore filters | bezel's toggle-group pill; single-select only, multiple waits. Shipped as a controlled single-select (`Segmented::new(id, selected)` + `options((value, label))` + `on_change(T)` — exactly one segment always active); muted pill track, the selected segment on a raised `surface` plate with border; left/right/home/end move and activate when the strip has focus (no wrap), `RadioGroup`/`RadioButton` roles with `aria_toggled`; renders hugging via a wrapper so it works in block and flex parents alike. Plain `segmented` feature, no deps. **Follow-ups (2026-11-01):** the selected plate dropped its border for a small raised shadow — a third `Elevations.control` class, profiled per platform like popover/dialog (D24); `Size::Small` became the default and medium/large resolve through the size table — taller tracks (`control_height × 1.15 / 1.35`), a scaled track inset (`0.125 / 0.25 / 0.375 rem`) and segment padding (`0.5× / 0.75× / 0.75×` of `padding_x`), with the large label staying at the medium size; options take optional leading icons (`SegmentedOption::new(value, label).icon(role)`, tuples still accepted through `From`); and the selected plate is now a measured, absolutely-positioned layer that springs between segments (per-segment measure canvases feed keyed geometry; the plate springs on `left` and `width` separately, Switch-style — mounts at its target so the first frame never animates, retargets mid-flight, and snaps under reduce-motion). **Follow-up 2 (2026-11-01):** the track inset also scales per size (`0.125 / 0.25 / 0.375 rem`), and a settled-plate regression test guards the plate's existence and slot (`debug_bounds("segmented-plate")`) |
|
| M3 | core | Segmented control ✅ Landed (2026-11-01) | All/Popular/Recent explore filters | bezel's toggle-group pill; single-select only, multiple waits. Shipped as a controlled single-select (`Segmented::new(id, selected)` + `options((value, label))` + `on_change(T)` — exactly one segment always active); muted pill track, the selected segment on a raised `surface` plate with border; left/right/home/end move and activate when the strip has focus (no wrap), `RadioGroup`/`RadioButton` roles with `aria_toggled`; renders hugging via a wrapper so it works in block and flex parents alike. Plain `segmented` feature, no deps. **Follow-ups (2026-11-01):** the selected plate dropped its border for a small raised shadow — a third `Elevations.control` class, profiled per platform like popover/dialog (D24); `Size::Small` became the default and medium/large resolve through the size table — taller tracks (`control_height × 1.15 / 1.35`), a scaled track inset (`0.125 / 0.25 / 0.375 rem`) and segment padding (`0.5× / 0.75× / 0.75×` of `padding_x`), with the large label staying at the medium size; options take optional leading icons (`SegmentedOption::new(value, label).icon(role)`, tuples still accepted through `From`); and the selected plate is now a measured, absolutely-positioned layer that springs between segments (per-segment measure canvases feed keyed geometry; the plate springs on `left` and `width` separately, Switch-style — mounts at its target so the first frame never animates, retargets mid-flight, and snaps under reduce-motion). **Follow-up 2 (2026-11-01):** the track inset also scales per size (`0.125 / 0.25 / 0.375 rem`), and a settled-plate regression test guards the plate's existence and slot (`debug_bounds("segmented-plate")`) |
|
||||||
| M4 | core | Context menu ✅ Landed (2026-11-01) | repo rows, file-tree rows, tab bar | gpuikit attaches the menu to an existing element; bezel keys one-open-at-a-time. Shipped as a self-contained `ContextMenu::new(id).target(el).items(...)` — open state and pointer position are namespaced keyed state, right-click opens at the pointer, left clicks pass through to the target untouched, and any click outside, escape, or activation closes. Composes `Menu`, which gained `trigger_button(MouseButton)` and `open_at(Point)` for pointer-anchored opening (outside-click dismissal now closes on any button). Plain `context_menu` feature, depends on `menu` |
|
| M4 | core | Context menu ✅ Landed (2026-11-01) | repo rows, file-tree rows, tab bar | gpuikit attaches the menu to an existing element; bezel keys one-open-at-a-time. Shipped as a self-contained `ContextMenu::new(id).target(el).items(...)` — open state and pointer position are namespaced keyed state, right-click opens at the pointer, left clicks pass through to the target untouched, and any click outside, escape, or activation closes. Composes `Menu`, which gained `trigger_button(MouseButton)` and `open_at(Point)` for pointer-anchored opening (outside-click dismissal now closes on any button). Plain `context_menu` feature, depends on `menu` |
|
||||||
| M5 | core | Empty state | inbox, issues, search misses | bezel: icon + headline + hint + optional action. Trivial paint over tokens |
|
| M5 | core | Empty state ✅ Landed (2026-11-01) | inbox, issues, search misses | bezel: icon + headline + one line of hint. Shipped as `EmptyState::new(headline)` with optional `.icon(role)`, `.hint(text)`, and `.action(element)` — no id (nothing interactive), no size classes (content, always medium metrics); the illustration is the component's own `icon_size × 2` decoration, muted like the hint, `line_height 1.3` on the headline, everything centered. Plain `empty_state` feature, no deps |
|
||||||
| M6 | input | Password input | identity unlock + `nsec` import passphrase | lifts §6.2's "password masking on first need" — the need arrived |
|
| M6 | input | Password input | identity unlock + `nsec` import passphrase | lifts §6.2's "password masking on first need" — the need arrived |
|
||||||
| M7 | input | Multiline textarea | discussion composer, patch + commit messages | bezel: fixed rows or min/max auto-grow; gpuikit: rows on InputState. Reuses Input's buffer; D12's editor crate stays separate |
|
| M7 | input | Multiline textarea | discussion composer, patch + commit messages | bezel: fixed rows or min/max auto-grow; gpuikit: rows on InputState. Reuses Input's buffer; D12's editor crate stays separate |
|
||||||
| M8 | core | Avatar image | profile pictures in rows and panel tabs | lifts the deferred image slot: gpui `img` + initials fallback (signed adds its pixel-art fallback app-side) |
|
| M8 | core | Avatar image | profile pictures in rows and panel tabs | lifts the deferred image slot: gpui `img` + initials fallback (signed adds its pixel-art fallback app-side) |
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use gpui_native_core::checkbox::Checkbox;
|
|||||||
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
||||||
use gpui_native_core::context_menu::ContextMenu;
|
use gpui_native_core::context_menu::ContextMenu;
|
||||||
use gpui_native_core::dialog::Dialog;
|
use gpui_native_core::dialog::Dialog;
|
||||||
|
use gpui_native_core::empty_state::EmptyState;
|
||||||
use gpui_native_core::input::{Input, InputState};
|
use gpui_native_core::input::{Input, InputState};
|
||||||
use gpui_native_core::link::Link;
|
use gpui_native_core::link::Link;
|
||||||
use gpui_native_core::menu::{Menu, MenuItem};
|
use gpui_native_core::menu::{Menu, MenuItem};
|
||||||
@@ -527,6 +528,43 @@ impl Render for Gallery {
|
|||||||
.child(self.context_action.clone()),
|
.child(self.context_action.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(section_label(
|
||||||
|
"empty state · native refs — AdwStatusPage, Kirigami PlaceHolderMessage, macOS placeholder views",
|
||||||
|
colors,
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(px(180.))
|
||||||
|
.rounded(theme.radius.overlay)
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.flex()
|
||||||
|
.child(
|
||||||
|
EmptyState::new("No repositories found")
|
||||||
|
.icon(IconRole::Search)
|
||||||
|
.hint("Try a different name, or publish one first.")
|
||||||
|
.action(
|
||||||
|
Button::new("empty-state-clear")
|
||||||
|
.label("Clear filters")
|
||||||
|
.variant(ButtonVariant::Secondary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h(px(120.))
|
||||||
|
.rounded(theme.radius.overlay)
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.flex()
|
||||||
|
.child(EmptyState::new("Nothing here yet")),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
Reference in New Issue
Block a user