From 73a8bf2e3455e04572b8bd64c4b2fdc7d38e2632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 6 Oct 2026 16:32:12 -0400 Subject: [PATCH] =?UTF-8?q?Panics=20=C2=B7=20P05=20Buttons:=20a=20segmente?= =?UTF-8?q?d=20choice=20missing=20from=20its=20segments=20marks=20none;=20?= =?UTF-8?q?slide=20logs=20and=20shows=20no=20marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/buttons/segmented.rs | 7 ++----- src/buttons/tests.rs | 30 +++++++++++++++++++++++++++++- src/motion/slide.rs | 13 +++++++------ tasks/panics.md | 3 ++- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/buttons/segmented.rs b/src/buttons/segmented.rs index 90ccba7..3210230 100644 --- a/src/buttons/segmented.rs +++ b/src/buttons/segmented.rs @@ -69,10 +69,7 @@ impl RenderOnce for SegmentedControl { .iter() .map(|(value, ..)| value.clone()) .collect(); - let chosen = values - .iter() - .position(|value| *value == self.selected) - .unwrap_or_else(|| panic!("segmented control has no segment {}", self.selected)); + let chosen = values.iter().position(|value| *value == self.selected); let (state, marker) = slide(self.id.clone(), &values, &self.selected, window, cx); let scroll = window .use_keyed_state((self.id.clone(), "scroll"), cx, |_, _| ScrollHandle::new()) @@ -133,7 +130,7 @@ impl RenderOnce for SegmentedControl { .into_iter() .enumerate() .map(|(ix, (value, label, icon))| { - let on = ix == chosen; + let on = Some(ix) == chosen; let fg = if on { colors.fg } else { colors.fg_muted }; let (focus, reveal) = stops.remove(0); let (named, labeled) = (value.clone(), value.clone()); diff --git a/src/buttons/tests.rs b/src/buttons/tests.rs index eedd059..23305f6 100644 --- a/src/buttons/tests.rs +++ b/src/buttons/tests.rs @@ -3,7 +3,7 @@ use gpui::{ Render, Styled, TestAppContext, VisualTestContext, Window, div, point, px, }; -use super::{Button, ToggleButton, ToggleItem}; +use super::{Button, SegmentedControl, ToggleButton, ToggleItem}; use crate::{ primitives::{FocusNext, FocusScope}, theme::Theme, @@ -98,3 +98,31 @@ fn a_disabled_toggle_ignores_clicks(cx: &mut TestAppContext) { cx.simulate_click(point(px(4.0), px(4.0)), Modifiers::none()); assert_eq!(view.read_with(cx, |toggles, _| toggles.presses), 1); } + +/// Segments whose chosen value left with a change of list. +struct Stale; + +impl Render for Stale { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().w(px(300.0)).child( + SegmentedControl::new("views", "gone") + .segment("list", "List", None) + .segment("grid", "Grid", None), + ) + } +} + +#[gpui::test] +fn a_choice_missing_from_its_segments_marks_none(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| Stale); + for _ in 0..3 { + cx.run_until_parked(); + cx.update(|window, _| window.refresh()); + } + assert!(cx.debug_bounds("segment list").is_some()); + assert!( + cx.debug_bounds("segment-thumb").is_none(), + "no thumb marks a choice" + ); +} diff --git a/src/motion/slide.rs b/src/motion/slide.rs index bda292a..e3a2cd9 100644 --- a/src/motion/slide.rs +++ b/src/motion/slide.rs @@ -46,7 +46,7 @@ pub(crate) struct Marker { pub generation: u64, } -/// The slide over `values` with `selected` chosen; no marker until that item is measured. +/// The slide over `values` with `selected` chosen; no marker until it is measured, or if it is not there. pub(crate) fn slide( id: impl Into, values: &[SharedString], @@ -54,14 +54,15 @@ pub(crate) fn slide( window: &mut Window, cx: &mut App, ) -> (Entity, Option) { - let chosen = values - .iter() - .position(|value| value == selected) - .unwrap_or_else(|| panic!("no item {selected} to slide to")); - let state = window.use_keyed_state(id, cx, |_, _| Slide::default()); + let id = id.into(); + let state = window.use_keyed_state(id.clone(), cx, |_, _| Slide::default()); if state.read(cx).spans.len() != values.len() { state.update(cx, |slide, _| slide.spans = vec![None; values.len()]); } + let Some(chosen) = values.iter().position(|value| value == selected) else { + log::error!("slide {id:?}: no item {selected} among {values:?}; nothing marked"); + return (state, None); + }; if state.read(cx).last.as_ref() != Some(selected) { state.update(cx, |slide, _| { let previous = slide diff --git a/tasks/panics.md b/tasks/panics.md index c16cf64..a9dd153 100644 --- a/tasks/panics.md +++ b/tasks/panics.md @@ -19,7 +19,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca - [x] P02 Typography — `src/typography` - [x] P03 Layout — `src/layout` - [x] P04 Window & Shell — `src/shell` -- [ ] P05 Buttons & Actions — `src/buttons` +- [x] P05 Buttons & Actions — `src/buttons` - [ ] P06 Forms — `src/forms` - [ ] P07 Navigation — `src/navigation` - [ ] P08 Menus — `src/menus` @@ -69,3 +69,4 @@ One line per item: what changed, what stayed and why, review rounds. - P02: `Link::new` takes its handler; `system_zone` logs once and reads as jiff's unknown zone, which `datetime` marks UTC; an oversized highlight query logs and marks nothing; a cut line's paint fault logs. Kept: caller contracts (speed, digits, step, keystroke, heading level, pattern) and the exponent parse. Review: 3 rounds. - P03: `PaneGroup` leaves a gone or last pane and logs (`split` returns `Option`); a stale split path sets nothing; `SplitPane` splits evenly when sizes lag or fail, seeds by the owner's raw sizes, and drops a drag over another pane count; masonry sorts with `total_cmp`. Kept: dock ids (panels never leave), one-value contracts, pane-id overflow, workspace JSON. Review: 2 rounds. - P04: no change. Kept: splash progress and column count (caller contracts), the objc class under `Once`, `Tray::pick` (items fixed at creation). Review: 1 round. +- P05: a `SegmentedControl` whose choice left its segments marks none; the cause, `motion::slide`, logs and returns no marker (P11 inherits it). Kept: one-value contracts (group and quick-action counts, toggle label or icon, share payload). Review: 1 round.