add multiline support to input
This commit is contained in:
+761
-262
File diff suppressed because it is too large
Load Diff
+174
-1
@@ -4,7 +4,8 @@ use gpui::{
|
|||||||
TestAppContext, VisualTestContext, Window, div, point, px,
|
TestAppContext, VisualTestContext, Window, div, point, px,
|
||||||
};
|
};
|
||||||
use gpui_native_core::input::{
|
use gpui_native_core::input::{
|
||||||
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, bind_input_keys,
|
INPUT_CONTEXT, Input, InputBindings, InputState, InputStateEvent, Submit, SubmitOn,
|
||||||
|
bind_input_keys,
|
||||||
};
|
};
|
||||||
use gpui_native_theme::Theme;
|
use gpui_native_theme::Theme;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -599,6 +600,34 @@ fn password_window(initial: &str) -> (TestAppContext, Entity<InputState>, Visual
|
|||||||
(cx, state, visual)
|
(cx, state, visual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn multiline_window(initial: &str) -> (TestAppContext, Entity<InputState>, VisualTestContext) {
|
||||||
|
let mut cx = TestAppContext::single();
|
||||||
|
cx.update(|cx| {
|
||||||
|
Theme::init(cx);
|
||||||
|
bind_input_keys(cx, InputBindings::default());
|
||||||
|
});
|
||||||
|
let state = cx.update(InputState::new);
|
||||||
|
cx.update(|cx| {
|
||||||
|
state.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
|
});
|
||||||
|
let state_for_window = state.clone();
|
||||||
|
let window = cx.add_window(move |_, _| Harness {
|
||||||
|
state: state_for_window,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||||
|
if !initial.is_empty() {
|
||||||
|
// Seeding after the mode flips so newlines survive the sanitizer.
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
state.update(cx, |state, cx| state.set_value(initial, window, cx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
(cx, state, visual)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn password_fields_keep_the_real_buffer_while_masked() {
|
fn password_fields_keep_the_real_buffer_while_masked() {
|
||||||
let (cx, state, mut visual) = password_window("");
|
let (cx, state, mut visual) = password_window("");
|
||||||
@@ -733,3 +762,147 @@ fn the_reveal_toggle_flips_masking() {
|
|||||||
let revealed = cx.update(|cx| state.read(cx).is_revealed());
|
let revealed = cx.update(|cx| state.read(cx).is_revealed());
|
||||||
assert!(!revealed, "the toggle should mask the field again");
|
assert!(!revealed, "the toggle should mask the field again");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_caret_can_rest_on_a_line_break() {
|
||||||
|
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
|
||||||
|
// Five lefts land the caret on the line break at the end of row 1 —
|
||||||
|
// an offset between the two rows — and painting must not panic there.
|
||||||
|
visual.simulate_keystrokes("left left left left left");
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
visual.simulate_keystrokes("x");
|
||||||
|
assert_eq!(value(&cx, &state), "abx\ncdef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiline_fields_keep_newlines() {
|
||||||
|
let (cx, state, mut visual) = multiline_window("");
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
state.update(cx, |state, cx| state.set_value("a\nb", window, cx));
|
||||||
|
});
|
||||||
|
assert_eq!(value(&cx, &state), "a\nb");
|
||||||
|
|
||||||
|
// Paste keeps line breaks too.
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string("x\ny".to_string()));
|
||||||
|
});
|
||||||
|
visual.simulate_keystrokes("cmd-v");
|
||||||
|
assert_eq!(value(&cx, &state), "a\nbx\ny");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_inserts_a_newline_and_modifier_enter_submits() {
|
||||||
|
let (mut cx, state, mut visual) = multiline_window("");
|
||||||
|
let (sink, _subscription) = record_events(&mut cx, &state);
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
|
||||||
|
visual.simulate_keystrokes("a b");
|
||||||
|
visual.simulate_keystrokes("enter");
|
||||||
|
assert_eq!(value(&cx, &state), "ab\n");
|
||||||
|
assert_eq!(count(&sink, &InputStateEvent::Submitted), 0);
|
||||||
|
|
||||||
|
visual.simulate_keystrokes("cmd-enter");
|
||||||
|
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
|
||||||
|
assert_eq!(value(&cx, &state), "ab\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submit_on_enter_swaps_the_keys() {
|
||||||
|
let (mut cx, state, mut visual) = multiline_window("");
|
||||||
|
let (sink, _subscription) = record_events(&mut cx, &state);
|
||||||
|
visual.update(|_, cx| {
|
||||||
|
state.update(cx, |state, cx| state.set_submit_on(SubmitOn::Enter, cx));
|
||||||
|
});
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
|
||||||
|
// Enter submits; Shift + Enter breaks the line.
|
||||||
|
visual.simulate_keystrokes("enter");
|
||||||
|
assert_eq!(count(&sink, &InputStateEvent::Submitted), 1);
|
||||||
|
assert_eq!(value(&cx, &state), "");
|
||||||
|
visual.simulate_keystrokes("shift-enter");
|
||||||
|
assert_eq!(value(&cx, &state), "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn up_and_down_move_between_rows() {
|
||||||
|
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
|
||||||
|
// From the end of row 1, shift-up reaches the end of the shorter row 0.
|
||||||
|
visual.simulate_keystrokes("shift-up");
|
||||||
|
visual.simulate_keystrokes("backspace");
|
||||||
|
assert_eq!(value(&cx, &state), "ab");
|
||||||
|
|
||||||
|
// From the start of row 0, shift-down reaches the start of row 1.
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
state.update(cx, |state, cx| state.set_value("ab\ncdef", window, cx));
|
||||||
|
});
|
||||||
|
visual.simulate_keystrokes("home");
|
||||||
|
visual.simulate_keystrokes("shift-down");
|
||||||
|
visual.simulate_keystrokes("backspace");
|
||||||
|
assert_eq!(value(&cx, &state), "cdef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_line_fields_ignore_up_and_down() {
|
||||||
|
let (cx, state, mut visual) = input_window("ab", false);
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
visual.simulate_keystrokes("up down");
|
||||||
|
visual.simulate_keystrokes("x");
|
||||||
|
assert_eq!(value(&cx, &state), "abx");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn triple_click_selects_the_clicked_row() {
|
||||||
|
let (cx, state, mut visual) = multiline_window("ab\ncdef");
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
|
||||||
|
// The second row's center sits one line height below the first's.
|
||||||
|
let at = point(px(30.), px(44.));
|
||||||
|
visual.simulate_event(MouseDownEvent {
|
||||||
|
position: at,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
button: MouseButton::Left,
|
||||||
|
click_count: 3,
|
||||||
|
first_mouse: false,
|
||||||
|
});
|
||||||
|
visual.simulate_event(MouseUpEvent {
|
||||||
|
position: at,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
button: MouseButton::Left,
|
||||||
|
click_count: 3,
|
||||||
|
});
|
||||||
|
visual.simulate_keystrokes("cmd-c");
|
||||||
|
let clipboard = cx.update(|cx| {
|
||||||
|
cx.read_from_clipboard()
|
||||||
|
.and_then(|item| item.text())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
assert_eq!(clipboard, "cdef");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_scroll_keeps_the_caret_in_view() {
|
||||||
|
let (_cx, state, mut visual) = multiline_window("l1\nl2\nl3\nl4\nl5\nl6");
|
||||||
|
focus(&mut visual, &state);
|
||||||
|
visual.update(|window, cx| {
|
||||||
|
window.draw(cx).clear(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The field shows four of the six rows and the caret ends on the last,
|
||||||
|
// so the view scrolls down and row 1 sits above the field's top.
|
||||||
|
let first_row_top = visual
|
||||||
|
.update(|window, cx| {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.bounds_for_range(0..1, Default::default(), window, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|bounds| bounds.top())
|
||||||
|
.unwrap_or_default();
|
||||||
|
assert!(first_row_top < px(0.), "first row top: {first_row_top:?}");
|
||||||
|
}
|
||||||
|
|||||||
+4
-4
@@ -250,7 +250,7 @@ Remaining, in order:
|
|||||||
| 5 | Caret blink | ✅ Landed. A private `CaretBlink` entity per `InputState`: 500 ms tick on the GPUI executor, visible on focus and hidden after one interval. Any field change (`InputState::notify`) pauses it — caret solid, cycle restarts — so typing, moving and dragging never hide it; generation counters invalidate stale ticks. Read-only suppresses the caret entirely. `caret_visible(focused, cx)` is the single gate used by paint and tests |
|
| 5 | Caret blink | ✅ Landed. A private `CaretBlink` entity per `InputState`: 500 ms tick on the GPUI executor, visible on focus and hidden after one interval. Any field change (`InputState::notify`) pauses it — caret solid, cycle restarts — so typing, moving and dragging never hide it; generation counters invalidate stale ticks. Read-only suppresses the caret entirely. `caret_visible(focused, cx)` is the single gate used by paint and tests |
|
||||||
| 6 | Paint polish | ✅ Landed. IME composition underline (1 px, foreground token) under the marked range. I-beam hover cursor via a canvas-prepaint hitbox + `set_cursor_style` in paint. Two-sided horizontal scroll: `scroll_offset` lives in state; caret-affecting changes set a pending flag that the next paint consumes with a fresh layout (two-sided pull-back + clamp to content), while `scroll_by` (wheel, `ScrollEvent`, lines→pixels via a fixed unit) bypasses the flag so manual pans survive. Scroll-aware hit-testing, IME `bounds_for_range` and drag select all come free through the scroll-adjusted origin |
|
| 6 | Paint polish | ✅ Landed. IME composition underline (1 px, foreground token) under the marked range. I-beam hover cursor via a canvas-prepaint hitbox + `set_cursor_style` in paint. Two-sided horizontal scroll: `scroll_offset` lives in state; caret-affecting changes set a pending flag that the next paint consumes with a fresh layout (two-sided pull-back + clamp to content), while `scroll_by` (wheel, `ScrollEvent`, lines→pixels via a fixed unit) bypasses the flag so manual pans survive. Scroll-aware hit-testing, IME `bounds_for_range` and drag select all come free through the scroll-adjusted origin |
|
||||||
|
|
||||||
Deferred until a real need: bidi/RTL; prefix/suffix adornments; `SubmitOn` enter/cmd-enter modes (multiline concern); `Undo`/`Redo` events; selection/cursor color overrides (theme tokens are deliberate); max length, clear button (gpuikit has none of these either — decide on first need). Password masking and the form field wrapper lifted 2026-11-01 — §6.3 M6/M9.
|
Deferred until a real need: bidi/RTL; prefix/suffix adornments; `Undo`/`Redo` events; selection/cursor color overrides (theme tokens are deliberate); max length, clear button (gpuikit has none of these either — decide on first need). Password masking and `SubmitOn` lifted 2026-11-01 — §6.3 M6/M7; the form field wrapper lifts with M9.
|
||||||
|
|
||||||
### 6.3 Migration-driven additions — signed is the first downstream app
|
### 6.3 Migration-driven additions — signed is the first downstream app
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ Wave M1 — unblocks the port; each through the review gate:
|
|||||||
| 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 — 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` |
|
| 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 ✅ Landed (2026-11-01) | 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. Shipped as a state mode — `InputState::set_multiline(bool)` — sharing the single-line machinery: shaping moved to gpui `shape_text`, which wraps to the field's inner width, and the paint layout gained a visual-row table (byte range + y per wrapped row) that the caret, selection, IME underline, hit-testing, triple-click and `bounds_for_range` all consume; `scroll_offset` became 2-D (x single-line, y multiline) with scroll-to-caret per axis. Height = the ambient line-height ratio × the element's `rows(usize)` (default 4); content taller than the field scrolls, and the field stops wheel propagation only while overflowing (min/max auto-grow deferred). Enter inserts a newline and modifier+Enter submits — `SubmitOn::ModifiedEnter` is the default, `SubmitOn::Enter` plus the `InsertNewline` shift-enter binding gives chat-style enter-submit — lifting §6.2's deferral. Up/down (+ shift variants) move between visual rows with a goal column; triple-click selects the clicked row; the sanitizer is mode-aware (multiline keeps newlines, drops `\r`; single-line unchanged), and the mask map gained per-grapheme display lengths so masked fields survive line breaks. No theme changes — all geometry derives from existing tokens. 7 headless tests join `tests/input.rs` |
|
||||||
| 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) |
|
||||||
|
|
||||||
Wave M2 — completes the surface: Field wrapper (M9: label/description/error around any control — the settings dialog; lifts another §6.2 deferral), Tag (M10: removable GRASP-server and relay entries), Status dot (M11: sync-drift beads on rows), Combobox (M12: type-to-filter pickers; gpuikit's `on_create`/`keep_unmatched` blur modes are worth copying), Command palette (M13: filtered actions — gpuikit keeps the matcher caller-owned, adopt that), VirtualList in `data` (M14: a thin `uniform_list` binding for commit histories and the inbox).
|
Wave M2 — completes the surface: Field wrapper (M9: label/description/error around any control — the settings dialog; lifts another §6.2 deferral), Tag (M10: removable GRASP-server and relay entries), Status dot (M11: sync-drift beads on rows), Combobox (M12: type-to-filter pickers; gpuikit's `on_create`/`keep_unmatched` blur modes are worth copying), Command palette (M13: filtered actions — gpuikit keeps the matcher caller-owned, adopt that), VirtualList in `data` (M14: a thin `uniform_list` binding for commit histories and the inbox).
|
||||||
@@ -354,8 +354,8 @@ Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in
|
|||||||
4. ~~Phase D input~~ ✅ — single-line Input (unit 10) + the §6.2 completion pass (modifiers, undo/redo, read-only, actions + key context, caret blink, paint polish). NumberInput stays deferred; multiline returns as §6.3 M7.
|
4. ~~Phase D input~~ ✅ — single-line Input (unit 10) + the §6.2 completion pass (modifiers, undo/redo, read-only, actions + key context, caret blink, paint polish). NumberInput stays deferred; multiline returns as §6.3 M7.
|
||||||
5. ~~Phase E overlays~~ ✅ — Popover (unit 11), ScrollArea (unit 11.5), Menu (unit 12), Tooltip + Dialog/AlertDialog + Toast (unit 11+ row; Sheet dropped). Phase gate held: escape/dismiss semantics and focus behavior per overlay.
|
5. ~~Phase E overlays~~ ✅ — Popover (unit 11), ScrollArea (unit 11.5), Menu (unit 12), Tooltip + Dialog/AlertDialog + Toast (unit 11+ row; Sheet dropped). Phase gate held: escape/dismiss semantics and focus behavior per overlay.
|
||||||
6. ~~Unit 14 — motion, per component (§6 row 14, D21)~~ ✅ Landed in five passes (2026-10-30): (a) theme `reduce_motion` → `cx.set_reduce_motion` in `reapply`; (b) Switch thumb `with_spring`; (c) Toast 8 px slide-in; (d) Dialog scrim enters with the panel; (e) Progress 30 fps cap. Exits and hover washes stay deferred (§6 deferred list).
|
6. ~~Unit 14 — motion, per component (§6 row 14, D21)~~ ✅ Landed in five passes (2026-10-30): (a) theme `reduce_motion` → `cx.set_reduce_motion` in `reapply`; (b) Switch thumb `with_spring`; (c) Toast 8 px slide-in; (d) Dialog scrim enters with the panel; (e) Progress 30 fps cap. Exits and hover washes stay deferred (§6 deferred list).
|
||||||
7. ~~Phase F, data first~~ ✅ data complete (2026-10-30) — `crates/data` opened with List, then Tree, Tabs, Sidebar (the dead `sheet` feature dropped from core along the way; Table dropped from the roadmap). Also extracted `crates/utils` (2026-10-30): `h_flex`/`v_flex` (core's `common` re-exports them), `focus_handle` (the namespaced keyed handle, 8 call sites), `focus_ring` (7 sites), `adjacent_selectable` + `nav_key` (4 sites), and the selectable-row paint helpers `selectable_label_color`/`paint_selectable_row` (3 sites, reading `&Theme` directly — no intermediate color struct), plus the `StyledExt` trait (`refine_style` — adapted from gpui-kit `base`, D15 — replacing the per-component `style().refine` pattern; 5 sites) — `core` and `data` depend on it; the facade doesn't re-export it. **Linux theme sources landed (2026-10-30)**: Omarchy (colors.toml parse + state-dir detection + inotify on `current`), KDE (kdeglobals `[Colors:*]` + AccentColor + `~/.config` inotify), GNOME/generic (portal `ReadAll` + `SettingChanged` via zbus 5 blocking on its own thread, gsettings fallback with the libadwaita accent table) — `SourceKind::Omarchy/Kde/Gnome`, detection cascade Omarchy → KDE → portal → fallback, all parsers fixture-tested on every OS and the Linux build compile-checked via `cargo check --target x86_64-unknown-linux-gnu`. **Windows source + icon resolvers landed (2026-10-30)**: `SourceKind::Windows` (WinRT `UISettings` + `Personalize` fallback + `ColorValuesChanged`/`HighContrastChanged`, §5.3), the Segoe Fluent codepoint renderer, and the freedesktop theme walker (§4.4); the WinRT surface was verified against the real `windows` 0.62 bindings via a standalone cross-check (gpui's own manifest build needs llvm-rc from macOS). Remaining Phase F: **side-by-side gallery screenshots per platform** — Omarchy/KDE/GNOME need a Linux run, Windows needs its first hardware build. NumberInput stays deferred until needed; multiline is §6.3 M7.
|
7. ~~Phase F, data first~~ ✅ data complete (2026-10-30) — `crates/data` opened with List, then Tree, Tabs, Sidebar (the dead `sheet` feature dropped from core along the way; Table dropped from the roadmap). Also extracted `crates/utils` (2026-10-30): `h_flex`/`v_flex` (core's `common` re-exports them), `focus_handle` (the namespaced keyed handle, 8 call sites), `focus_ring` (7 sites), `adjacent_selectable` + `nav_key` (4 sites), and the selectable-row paint helpers `selectable_label_color`/`paint_selectable_row` (3 sites, reading `&Theme` directly — no intermediate color struct), plus the `StyledExt` trait (`refine_style` — adapted from gpui-kit `base`, D15 — replacing the per-component `style().refine` pattern; 5 sites) — `core` and `data` depend on it; the facade doesn't re-export it. **Linux theme sources landed (2026-10-30)**: Omarchy (colors.toml parse + state-dir detection + inotify on `current`), KDE (kdeglobals `[Colors:*]` + AccentColor + `~/.config` inotify), GNOME/generic (portal `ReadAll` + `SettingChanged` via zbus 5 blocking on its own thread, gsettings fallback with the libadwaita accent table) — `SourceKind::Omarchy/Kde/Gnome`, detection cascade Omarchy → KDE → portal → fallback, all parsers fixture-tested on every OS and the Linux build compile-checked via `cargo check --target x86_64-unknown-linux-gnu`. **Windows source + icon resolvers landed (2026-10-30)**: `SourceKind::Windows` (WinRT `UISettings` + `Personalize` fallback + `ColorValuesChanged`/`HighContrastChanged`, §5.3), the Segoe Fluent codepoint renderer, and the freedesktop theme walker (§4.4); the WinRT surface was verified against the real `windows` 0.62 bindings via a standalone cross-check (gpui's own manifest build needs llvm-rc from macOS). Remaining Phase F: **side-by-side gallery screenshots per platform** — Omarchy/KDE/GNOME need a Linux run, Windows needs its first hardware build. NumberInput stays deferred until needed; multiline landed as §6.3 M7 (2026-11-01).
|
||||||
8. Then — Phase G is the migration additions (§6.3): Badge, Select, Segmented control, Context menu, Empty state, password input, multiline Textarea, Avatar image first — each unblocks a concrete signed screen. Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. `crates/editor` starts when the port reaches file viewing (M15).
|
8. Then — Phase G is the migration additions (§6.3): Badge ✅, Select ✅, Segmented control ✅, Context menu ✅, Empty state ✅, password input ✅, multiline textarea ✅, then Avatar image (M8) — each unblocks a concrete signed screen. Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. `crates/editor` starts when the port reaches file viewing (M15).
|
||||||
9. Then — Phase H, the signed migration itself (§7): theme swap first (native-theme → `gpui-native-theme`), then views one at a time, keeping the gpui-component build side by side until parity.
|
9. Then — Phase H, the signed migration itself (§7): theme swap first (native-theme → `gpui-native-theme`), then views one at a time, keeping the gpui-component build side by side until parity.
|
||||||
|
|
||||||
Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events.
|
Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ struct Gallery {
|
|||||||
volume: f32,
|
volume: f32,
|
||||||
input_state: Entity<InputState>,
|
input_state: Entity<InputState>,
|
||||||
password_state: Entity<InputState>,
|
password_state: Entity<InputState>,
|
||||||
|
notes_state: Entity<InputState>,
|
||||||
popover_open: bool,
|
popover_open: bool,
|
||||||
menu_open: bool,
|
menu_open: bool,
|
||||||
strategy: Option<Strategy>,
|
strategy: Option<Strategy>,
|
||||||
@@ -362,6 +363,11 @@ impl Render for Gallery {
|
|||||||
Input::new("gallery-password", self.password_state.clone())
|
Input::new("gallery-password", self.password_state.clone())
|
||||||
.placeholder("Password"),
|
.placeholder("Password"),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
Input::new("gallery-notes", self.notes_state.clone())
|
||||||
|
.placeholder("Notes — Enter breaks the line, cmd-Enter submits")
|
||||||
|
.rows(4),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -1460,6 +1466,8 @@ fn main() {
|
|||||||
let input_state = InputState::new(cx);
|
let input_state = InputState::new(cx);
|
||||||
let password_state = InputState::new(cx);
|
let password_state = InputState::new(cx);
|
||||||
password_state.update(cx, |state, cx| state.set_password(true, cx));
|
password_state.update(cx, |state, cx| state.set_password(true, cx));
|
||||||
|
let notes_state = InputState::new(cx);
|
||||||
|
notes_state.update(cx, |state, cx| state.set_multiline(true, cx));
|
||||||
let bounds = Bounds::centered(None, size(px(720.0), px(560.0)), cx);
|
let bounds = Bounds::centered(None, size(px(720.0), px(560.0)), cx);
|
||||||
if let Err(err) = cx.open_window(
|
if let Err(err) = cx.open_window(
|
||||||
WindowOptions {
|
WindowOptions {
|
||||||
@@ -1497,6 +1505,7 @@ fn main() {
|
|||||||
volume: 40.0,
|
volume: 40.0,
|
||||||
input_state,
|
input_state,
|
||||||
password_state,
|
password_state,
|
||||||
|
notes_state,
|
||||||
popover_open: false,
|
popover_open: false,
|
||||||
menu_open: false,
|
menu_open: false,
|
||||||
strategy: Some(Strategy::Curated),
|
strategy: Some(Strategy::Curated),
|
||||||
|
|||||||
Reference in New Issue
Block a user