add icon button
This commit is contained in:
@@ -51,7 +51,7 @@ slider = []
|
|||||||
progress = []
|
progress = []
|
||||||
avatar = []
|
avatar = []
|
||||||
badge = []
|
badge = []
|
||||||
input = []
|
input = ["button"]
|
||||||
popover = []
|
popover = []
|
||||||
scroll_area = []
|
scroll_area = []
|
||||||
segmented = []
|
segmented = []
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ impl RenderOnce for Button {
|
|||||||
|
|
||||||
let on_click = self.on_click;
|
let on_click = self.on_click;
|
||||||
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
|
||||||
|
let icon_only = self.label.is_none() && self.icon.is_some();
|
||||||
|
|
||||||
let (background, foreground, hover, pressed) = match self.variant {
|
let (background, foreground, hover, pressed) = match self.variant {
|
||||||
ButtonVariant::Primary => (
|
ButtonVariant::Primary => (
|
||||||
@@ -166,8 +167,10 @@ impl RenderOnce for Button {
|
|||||||
.id(self.id)
|
.id(self.id)
|
||||||
.role(Role::Button)
|
.role(Role::Button)
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.h(metrics.control_height)
|
.when(icon_only, |button| button.size(metrics.control_height))
|
||||||
.px(metrics.padding_x)
|
.when(!icon_only, |button| {
|
||||||
|
button.h(metrics.control_height).px(metrics.padding_x)
|
||||||
|
})
|
||||||
.line_height(relative(1.0))
|
.line_height(relative(1.0))
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.rounded(theme.radius.control)
|
.rounded(theme.radius.control)
|
||||||
|
|||||||
+21
-17
@@ -14,9 +14,10 @@ use gpui::{
|
|||||||
ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription,
|
ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription,
|
||||||
TextAlign, TextRun, UTF16Selection, Window, actions, canvas, div, fill, point, px,
|
TextAlign, TextRun, UTF16Selection, Window, actions, canvas, div, fill, point, px,
|
||||||
};
|
};
|
||||||
use gpui_native_icons::{IconRole, icon};
|
use gpui_native_icons::IconRole;
|
||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size};
|
||||||
|
|
||||||
|
use crate::button::{Button, ButtonVariant};
|
||||||
use crate::common::{Sizable, h_flex};
|
use crate::common::{Sizable, h_flex};
|
||||||
|
|
||||||
/// The key context input bindings match against.
|
/// The key context input bindings match against.
|
||||||
@@ -1786,31 +1787,34 @@ impl RenderOnce for Input {
|
|||||||
.id((self.id.clone(), "input-reveal"))
|
.id((self.id.clone(), "input-reveal"))
|
||||||
.debug_selector(|| "input-reveal".to_string())
|
.debug_selector(|| "input-reveal".to_string())
|
||||||
.absolute()
|
.absolute()
|
||||||
.right(metrics.dense_padding_x)
|
.right(metrics.dense_padding_x / 2.0)
|
||||||
.top_0p5()
|
.top_0p5()
|
||||||
.bottom_0p5()
|
.bottom_0p5()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.cursor_pointer()
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
.rounded(theme.radius.sm)
|
|
||||||
.hover(|style| style.bg(colors.muted))
|
|
||||||
.on_mouse_down(MouseButton::Left, {
|
|
||||||
let state = self.state.clone();
|
|
||||||
move |_, _window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
state.update(cx, |state, cx| {
|
|
||||||
state.set_revealed(!state.is_revealed(), cx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
icon(if revealed {
|
Button::new((self.id.clone(), "input-reveal-button"))
|
||||||
|
.icon(if revealed {
|
||||||
IconRole::EyeOff
|
IconRole::EyeOff
|
||||||
} else {
|
} else {
|
||||||
IconRole::Eye
|
IconRole::Eye
|
||||||
})
|
})
|
||||||
.size(metrics.icon_size)
|
.variant(ButtonVariant::Ghost)
|
||||||
.color(colors.muted_foreground),
|
.small()
|
||||||
|
.accessibility_label(if revealed {
|
||||||
|
"Hide password"
|
||||||
|
} else {
|
||||||
|
"Show password"
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let state = self.state.clone();
|
||||||
|
move |_, _, cx| {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.set_revealed(!state.is_revealed(), cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -206,7 +206,7 @@ Each row is one review unit: build it, show it in the gallery, review, then move
|
|||||||
|
|
||||||
| Unit | Crate | Component | Notes |
|
| Unit | Crate | Component | Notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | core | Button | Variants (primary/secondary/ghost/destructive) painted from token ramps; single activation path; disabled + a11y role. The API template for everything after. |
|
| 1 | core | Button | Variants (primary/secondary/ghost/destructive) painted from token ramps; single activation path; disabled + a11y role. The API template for everything after. An icon with no label renders the button square — the icon-only shape for toolbars and reveal controls |
|
||||||
| 2 | core | Checkbox | Controlled `checked` + `on_change(next)`; Check icon from the icons crate. |
|
| 2 | core | Checkbox | Controlled `checked` + `on_change(next)`; Check icon from the icons crate. |
|
||||||
| 3 | core | Switch | Controlled pill + thumb; thumb slides once motion exists. |
|
| 3 | core | Switch | Controlled pill + thumb; thumb slides once motion exists. |
|
||||||
| 4 | core | Radio + RadioGroup | Group owns the selected value; radios never self-uncheck. |
|
| 4 | core | Radio + RadioGroup | Group owns the selected value; radios never self-uncheck. |
|
||||||
@@ -265,7 +265,7 @@ Wave M1 — unblocks the port; each through the review gate:
|
|||||||
| 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 ✅ 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 |
|
| 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 | core | Password input ✅ Landed (2026-11-01) | identity unlock + `nsec` import passphrase | Lifts §6.2's "password masking on first need" — the need arrived. `InputState::set_password(bool)` masks the painted line one bullet per grapheme (paint-only: the buffer, undo, and caret all stay real; a mask map of grapheme byte offsets converts between the buffer and the painted display for hit-testing, the caret, selection, the IME underline, and `bounds_for_range`). Masked fields refuse copy and cut so the secret never reaches the clipboard; paste, editing, and reveal stay available. No element builder — the flag is state, like read-only. **Follow-up (2026-11-01):** a built-in reveal toggle — an eye button (`IconRole::Eye`/`EyeOff`, native glyphs per platform) pinned inside the field's right edge, shown on every password field. The state now tracks the field kind (`set_password`/`is_password`) and its visibility (`set_revealed`/`is_revealed`) separately, so toggling reveal never changes the field's kind; the paint reserves the button's corner (`right_padding` threaded through `paint`/`store_layout`, with a content mask so text never runs under the icon) |
|
| M6 | core | Password input ✅ Landed (2026-11-01) | identity unlock + `nsec` import passphrase | Lifts §6.2's "password masking on first need" — the need arrived. `InputState::set_password(bool)` masks the painted line one bullet per grapheme (paint-only: the buffer, undo, and caret all stay real; a mask map of grapheme byte offsets converts between the buffer and the painted display for hit-testing, the caret, selection, the IME underline, and `bounds_for_range`). Masked fields refuse copy and cut so the secret never reaches the clipboard; paste, editing, and reveal stay available. No element builder — the flag is state, like read-only. **Follow-up (2026-11-01):** a built-in reveal toggle — a ghost icon-only Button (`IconRole::Eye`/`EyeOff`, native glyphs per platform) pinned inside the field's right edge, shown on every password field. The state now tracks the field kind (`set_password`/`is_password`) and its visibility (`set_revealed`/`is_revealed`) separately, so toggling reveal never changes the field's kind; the paint reserves the button's corner (`right_padding` threaded through `paint`/`store_layout`, with a content mask so text never runs under the icon). The reveal press stops propagation so it never places the caret. The `input` feature now depends on `button` |
|
||||||
| 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) |
|
||||||
|
|
||||||
|
|||||||
@@ -323,6 +323,13 @@ impl Render for Gallery {
|
|||||||
.label("Open")
|
.label("Open")
|
||||||
.icon(IconRole::Folder)
|
.icon(IconRole::Folder)
|
||||||
.tooltip(Tooltip::text("Opens the selected file")),
|
.tooltip(Tooltip::text("Opens the selected file")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("gallery-button-icon-only")
|
||||||
|
.icon(IconRole::Folder)
|
||||||
|
.variant(ButtonVariant::Ghost)
|
||||||
|
.accessibility_label("Open the selected file")
|
||||||
|
.tooltip(Tooltip::text("Opens the selected file")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
Reference in New Issue
Block a user