Panics · P25 Generative: gone choices mark none, overruns peg; seed fields hold seeds through fits, undo and input methods

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 02:12:07 -04:00
1 parent fb7ee42e3d
commit 68a98198b0
17 files changed
+451 -123

No files matched your search

+10 -6
View File
@@ -80,12 +80,16 @@ impl RenderOnce for ABCompareView {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let count = self.sides.len();
assert!((2..=4).contains(&count), "a comparison of {count} sides");
if let Some(Verdict::Side(ix)) = self.verdict {
assert!(ix < count, "verdict for side {ix} of {count}");
}
let verdict = match self.verdict {
Some(Verdict::Side(ix)) if ix >= count => {
log::error!("comparison: verdict for side {ix} of {count}; none given");
None
}
verdict => verdict,
};
let theme = cx.theme();
let colors = theme.colors.clone();
let decided = self.verdict.is_some();
let decided = verdict.is_some();
let judge = |verdict: Verdict, label: SharedString, key: String| {
self.on_verdict.clone().map(|judge| {
Button::new((self.id.clone(), key), label)
@@ -103,7 +107,7 @@ impl RenderOnce for ABCompareView {
.into_iter()
.enumerate()
.map(|(ix, (name, answer))| {
let won = self.verdict == Some(Verdict::Side(ix));
let won = verdict == Some(Verdict::Side(ix));
let title = heading(ix, &name, self.blind, decided);
titles.push(title.clone());
div()
@@ -147,7 +151,7 @@ impl RenderOnce for ABCompareView {
)
})
.collect();
let verdicts = match self.verdict {
let verdicts = match verdict {
Some(Verdict::Tie) => vec![div().child("Called a tie").into_any_element()],
Some(Verdict::Side(_)) => Vec::new(),
None => titles
+15 -7
View File
@@ -63,14 +63,22 @@ impl EmbeddingVisualizer {
for (ix, name) in groups.iter().enumerate() {
assert!(!groups[..ix].contains(name), "group {name} named twice");
}
let points: Vec<Embedded> = points
.into_iter()
.filter(|point| {
let held = point.group < groups.len();
if !held {
log::error!(
"embedding: point {} in group {} of {}; left out",
point.key,
point.group,
groups.len()
);
}
held
})
.collect();
for point in &points {
assert!(
point.group < groups.len(),
"point {} in group {} of {}",
point.key,
point.group,
groups.len()
);
assert!(
point.at.iter().all(|axis| (-1.0..=1.0).contains(axis)),
"point {} sits outside -1 to 1",
+5 -2
View File
@@ -225,13 +225,16 @@ fn shaped(
}
}
/// `at` as shares of `bounds`.
/// `at` as shares of `bounds`, kept on the picture as the pen keeps its points in the pad; a canvas that shrank mid-stroke ends the stroke at its edge.
fn share(bounds: Bounds<Pixels>, at: Point<Pixels>) -> (f32, f32) {
let (width, height) = (
f32::from(bounds.size.width).max(1.0),
f32::from(bounds.size.height).max(1.0),
);
(f32::from(at.x) / width, f32::from(at.y) / height)
(
(f32::from(at.x) / width).clamp(0.0, 1.0),
(f32::from(at.y) / height).clamp(0.0, 1.0),
)
}
impl RenderOnce for InpaintCanvas {
+5 -2
View File
@@ -203,7 +203,10 @@ impl ModelStatus {
/// GPU memory it holds and the card's whole, in bytes.
pub fn memory(mut self, used: u64, total: u64) -> Self {
assert!(total > 0 && used <= total, "GPU memory {used} of {total}");
assert!(total > 0, "GPU memory {used} of none");
if used > total {
log::error!("model status: GPU memory {used} past {total}; the meter fills");
}
self.memory = Some((used, total));
self
}
@@ -247,7 +250,7 @@ impl RenderOnce for ModelStatus {
Meter::new(
(self.id.clone(), "memory"),
"GPU memory",
used as f32 / total as f32,
used.min(total) as f32 / total as f32,
)
.detail(format!(
"{} of {}",
+7 -6
View File
@@ -38,12 +38,13 @@ impl AspectRatioPicker {
selected: (u32, u32),
) -> Self {
let ratios: Vec<(u32, u32)> = ratios.into_iter().collect();
assert!(
ratios.contains(&selected),
"ratio {}:{} is not among the ratios",
selected.0,
selected.1
);
if !ratios.contains(&selected) {
log::error!(
"aspect ratio: {}:{} is not among the ratios; none selected",
selected.0,
selected.1
);
}
Self {
id: id.into(),
ratios,
+17 -8
View File
@@ -4,7 +4,7 @@ use std::{
rc::Rc,
};
use gpui::{App, ElementId, Entity, IntoElement, RenderOnce, Window};
use gpui::{App, ElementId, Entity, EntityId, IntoElement, RenderOnce, Window};
use crate::{
buttons::IconButton,
@@ -43,22 +43,31 @@ impl SeedInput {
}
}
/// The seed in `field`; none while it is empty.
/// The seed in `field`, read after a `SeedInput` drew it; none while it is empty.
pub fn read(field: &Entity<TextInput>, cx: &App) -> Option<u32> {
let text = field.read(cx).text();
let text = field.read(cx).committed();
(!text.is_empty()).then(|| text.parse().expect("a seed field holds digits"))
}
}
impl RenderOnce for SeedInput {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let fitted = window.use_keyed_state((self.id.clone(), "fitted"), cx, |_, _| false);
if !*fitted.read(cx) {
fitted.update(cx, |fitted, _| *fitted = true);
let fitted =
window.use_keyed_state((self.id.clone(), "fitted"), cx, |_, _| None::<EntityId>);
let field_id = self.field.entity_id();
if *fitted.read(cx) != Some(field_id) {
fitted.update(cx, |fitted, _| *fitted = Some(field_id));
self.field.update(cx, |input, cx| {
input.set_fit(Rc::new(digits));
let text = input.text().to_string();
input.set_text(text, cx);
let kept = match text.is_empty() || text.parse::<u32>().is_ok() {
true => text,
false => {
log::error!("seed input: {text:?} is no seed; cleared");
String::new()
}
};
input.set_fit(Rc::new(digits));
input.set_text(kept, cx);
input.set_placeholder("Random", cx);
});
}
+3 -4
View File
@@ -39,10 +39,9 @@ impl StylePresetPicker {
) -> Self {
let (presets, selected): (Vec<StylePreset>, SharedString) =
(presets.into_iter().collect(), selected.into());
assert!(
presets.iter().any(|preset| preset.key == selected),
"style {selected} is not among the presets"
);
if !presets.iter().any(|preset| preset.key == selected) {
log::error!("style presets: {selected} is not among the presets; none selected");
}
Self {
id: id.into(),
presets,
+1
View File
@@ -8,6 +8,7 @@ mod models;
mod results;
mod settings;
mod sound;
mod stale;
fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
+1 -7
View File
@@ -5,7 +5,7 @@ use gpui::{Context, IntoElement, ParentElement, Render, Styled, TestAppContext,
use super::{press, settle, setup, tab};
use crate::generative::{
Download, DownloadState, FineTuneJobCard, HardwareMonitor, ModelCard, ModelDownloadManager,
ModelState, ModelStatus, Reading, TunePhase,
Reading, TunePhase,
};
type Heard = Rc<RefCell<Vec<String>>>;
@@ -120,12 +120,6 @@ fn cards_offer_what_their_state_allows(cx: &mut TestAppContext) {
);
}
#[test]
#[should_panic(expected = "GPU memory 5 of 4")]
fn memory_past_the_card_fails_loud() {
let _ = ModelStatus::new("model", "Orchid", ModelState::Ready).memory(5, 4);
}
#[test]
#[should_panic(expected = "GPU reads outside 0 to 1")]
fn a_reading_past_full_fails_loud() {
+270
View File
@@ -0,0 +1,270 @@
use std::{cell::RefCell, rc::Rc, time::Duration};
use gpui::{
AppContext as _, Context, Entity, EntityInputHandler as _, Focusable as _, IntoElement,
Modifiers, MouseButton, ParentElement, Render, Styled, TestAppContext, Window, div, point, px,
};
use super::{press, settle, setup, tab};
use crate::forms::TextInput;
use crate::generative::{
ABCompareView, AspectRatioPicker, Embedded, EmbeddingVisualizer, FineTuneJobCard,
InpaintCanvas, MaskStroke, ModelState, ModelStatus, Outcome, PromptVersionHistory, SeedInput,
Shot, StylePreset, StylePresetPicker, TTSVoicePicker, TunePhase, VariationPicker, Verdict,
VideoGenerationTimeline, Voice,
};
/// Each picker asked for what its list no longer holds, and counts past their whole.
struct Stale;
impl Render for Stale {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let preset = StylePreset {
key: "ink".into(),
name: "Ink".into(),
picture: None,
};
let voice = Voice {
key: "iris".into(),
name: "Iris".into(),
about: "a voice".into(),
tags: Vec::new(),
};
let shot = Shot {
key: "a".into(),
prompt: "a pan".into(),
length: Duration::from_secs(4),
outcome: Outcome::Done("missing.jpg".into()),
};
let point = |group| Embedded {
key: format!("in-{group}").into(),
label: "a point".into(),
group,
at: [0.0, 0.0, 0.0],
};
div()
.w(px(640.0))
.child(
ABCompareView::new("compare")
.side("Orchid", "one answer")
.side("Juniper", "another")
.verdict(Verdict::Side(5)),
)
.child(AspectRatioPicker::new("ratios", [(1, 1), (4, 3)], (16, 9)))
.child(StylePresetPicker::new("styles", [preset], "gone"))
.child(
VideoGenerationTimeline::new("timeline", [shot], Duration::ZERO).selected("gone"),
)
.child(TTSVoicePicker::new("voices", [voice]).selected("gone"))
.child(VariationPicker::new("variations", ["a.jpg"], 1.5, 4))
.child(PromptVersionHistory::new("history", [], 0))
.child(FineTuneJobCard::new(
"tune",
"Orchid tuned",
"Orchid",
"notes.jsonl",
TunePhase::Running(7, 3, 0.5),
))
.child(ModelStatus::new("model", "Orchid", ModelState::Ready).memory(5, 4))
.child(div().h(px(320.0)).child(EmbeddingVisualizer::new(
"space",
[point(0), point(9)],
["Docs"],
)))
}
}
#[gpui::test]
fn gone_choices_mark_none_and_overruns_peg(cx: &mut TestAppContext) {
setup(cx);
let (_, cx) = cx.add_window_view(|_, _| Stale);
settle(cx);
}
/// A seed field the owner may swap for another.
struct Seeded(Entity<TextInput>);
impl Render for Seeded {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().w(px(420.0)).child(SeedInput::new("seed", &self.0))
}
}
#[gpui::test]
fn a_seed_field_fits_whatever_it_held_and_each_new_field(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|window, cx| {
let field = cx.new(|cx| TextInput::new(window, cx));
field.update(cx, |input, cx| {
input.set_text("4294967296", cx);
input.insert("7", cx);
});
Seeded(field)
});
cx.update(|window, _| window.activate_window());
settle(cx);
view.read_with(cx, |view, cx| {
assert_eq!(SeedInput::read(&view.0, cx), None)
});
cx.update(|window, cx| {
let field = view.read(cx).0.clone();
window.focus(&field.focus_handle(cx), cx);
});
cx.simulate_keystrokes("secondary-z");
settle(cx);
view.read_with(cx, |view, cx| {
assert_eq!(
SeedInput::read(&view.0, cx),
None,
"undo reaches no unfit text"
)
});
let field = cx.update(|window, cx| cx.new(|cx| TextInput::new(window, cx)));
view.update(cx, |view, cx| {
view.0 = field.clone();
cx.notify();
});
settle(cx);
cx.update(|window, cx| window.focus(&field.focus_handle(cx), cx));
cx.simulate_input("12a3");
settle(cx);
view.read_with(cx, |view, cx| {
assert_eq!(SeedInput::read(&view.0, cx), Some(123))
});
cx.update(|window, cx| {
field.update(cx, |input, cx| {
input.replace_and_mark_text_in_range(None, "ni", None, window, cx)
})
});
settle(cx);
assert_eq!(
field.read_with(cx, |input, _| input.text().to_string()),
"123ni"
);
view.read_with(cx, |view, cx| {
assert_eq!(
SeedInput::read(&view.0, cx),
Some(123),
"a composition is not yet the seed"
)
});
cx.simulate_keystrokes("secondary-z secondary-shift-z");
settle(cx);
view.read_with(cx, |view, cx| {
assert_eq!(
SeedInput::read(&view.0, cx),
Some(123),
"redo brings back what was committed"
)
});
cx.update(|window, cx| {
field.update(cx, |input, cx| {
input.replace_and_mark_text_in_range(None, "ni", None, window, cx);
input.set_text("4294967296", cx);
})
});
settle(cx);
view.read_with(cx, |view, cx| {
assert_eq!(
SeedInput::read(&view.0, cx),
Some(123),
"a refused text keeps what was committed"
)
});
}
type Heard = Rc<RefCell<Vec<String>>>;
/// Variations whose chosen one may leave, and the actions asked.
struct Varied {
chosen: usize,
heard: Heard,
}
impl Render for Varied {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (vary, upscale) = (self.heard.clone(), self.heard.clone());
div().w(px(420.0)).child(
VariationPicker::new("variations", ["a.jpg"], 1.5, self.chosen)
.on_vary(move |strong, _, _| vary.borrow_mut().push(format!("vary {strong}")))
.on_upscale(move |_, _| upscale.borrow_mut().push("upscale".into())),
)
}
}
#[gpui::test]
fn no_picture_chosen_offers_no_way_on(cx: &mut TestAppContext) {
setup(cx);
let heard = Heard::default();
let kept = heard.clone();
let (view, cx) = cx.add_window_view(|_, _| Varied {
chosen: 0,
heard: kept,
});
cx.update(|window, _| window.activate_window());
settle(cx);
tab(2, cx);
press("enter", cx);
assert_eq!(*heard.borrow(), ["vary false"]);
heard.borrow_mut().clear();
view.update(cx, |view, cx| {
view.chosen = 4;
cx.notify();
});
settle(cx);
cx.update(|window, cx| window.blur(cx));
for _ in 0..4 {
tab(1, cx);
press("enter", cx);
}
assert_eq!(*heard.borrow(), Vec::<String>::new());
}
/// A canvas the owner narrows, keeping every stroke it hands over.
struct Painting {
width: f32,
strokes: Vec<MaskStroke>,
}
impl Render for Painting {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
div().w(px(self.width)).child(
InpaintCanvas::new("canvas", "missing.jpg", 2.0, self.strokes.clone()).on_stroke(
move |stroke, _, cx| {
owner.update(cx, |view, cx| {
view.strokes.push(stroke);
cx.notify();
})
},
),
)
}
}
#[gpui::test]
fn a_canvas_that_shrinks_under_the_pen_ends_the_stroke_on_it(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Painting {
width: 400.0,
strokes: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
let none = Modifiers::none();
cx.simulate_mouse_down(point(px(300.0), px(100.0)), MouseButton::Left, none);
cx.simulate_mouse_move(point(px(320.0), px(100.0)), MouseButton::Left, none);
cx.simulate_mouse_move(point(px(340.0), px(100.0)), MouseButton::Left, none);
settle(cx);
view.update(cx, |view, cx| {
view.width = 200.0;
cx.notify();
});
settle(cx);
cx.simulate_mouse_up(point(px(160.0), px(50.0)), MouseButton::Left, none);
settle(cx);
let strokes = view.read_with(cx, |view, _| view.strokes.clone());
assert_eq!(strokes.len(), 1);
let on = |share: f32| (0.0..=1.0).contains(&share);
assert!(strokes[0].points.iter().all(|&(x, y)| on(x) && on(y)));
}
+7 -2
View File
@@ -71,11 +71,16 @@ impl FineTuneJobCard {
data: impl Into<SharedString>,
phase: TunePhase,
) -> Self {
if let TunePhase::Running(epoch, epochs, share) = phase {
let mut phase = phase;
if let TunePhase::Running(epoch, epochs, share) = &mut phase {
assert!(
(1..=epochs).contains(&epoch) && (0.0..=1.0).contains(&share),
*epochs > 0 && (0.0..=1.0).contains(share),
"epoch {epoch} of {epochs} at {share}"
);
if !(1..=*epochs).contains(epoch) {
log::error!("fine-tuning: epoch {epoch} of {epochs}; pegged");
*epoch = (*epoch).clamp(1, *epochs);
}
}
Self {
id: id.into(),
+59 -47
View File
@@ -21,7 +21,7 @@ pub struct VariationPicker {
id: ElementId,
pictures: Vec<SharedString>,
ratio: f32,
chosen: usize,
chosen: Option<usize>,
on_choose: Option<Pick>,
on_vary: Option<OnFlag>,
on_upscale: Option<Run>,
@@ -36,16 +36,18 @@ impl VariationPicker {
chosen: usize,
) -> Self {
let pictures: Vec<SharedString> = pictures.into_iter().map(Into::into).collect();
assert!(
chosen < pictures.len(),
"variation {chosen} of {}",
pictures.len()
);
let held = chosen < pictures.len();
if !held {
log::error!(
"variations: variation {chosen} of {}; none chosen",
pictures.len()
);
}
Self {
id: id.into(),
pictures,
ratio: checked_ratio(ratio),
chosen,
chosen: held.then_some(chosen),
on_choose: None,
on_vary: None,
on_upscale: None,
@@ -89,45 +91,50 @@ impl RenderOnce for VariationPicker {
let (large, small) = (theme.radius(Radius::Lg), theme.radius(Radius::Md));
let thumb = theme.avatar_size(AvatarSize::Xl);
let length = motion::duration(motion::BASE, cx);
let picture = Image::new(
(self.id.clone(), format!("large-{}", self.chosen)),
source(&self.pictures[self.chosen]),
)
.size_full()
.rounded(large);
let shown = match turn {
0 => picture.into_any_element(),
_ => div()
.size_full()
.child(picture)
.with_animation(
(self.id.clone(), format!("fade-{turn}")),
Animation::new(length).with_easing(motion::ease_out_cubic),
|shown, t| shown.opacity(0.4 + 0.6 * t),
)
.into_any_element(),
};
let shown = self.chosen.map(|chosen| {
let picture = Image::new(
(self.id.clone(), format!("large-{chosen}")),
source(&self.pictures[chosen]),
)
.size_full()
.rounded(large);
match turn {
0 => picture.into_any_element(),
_ => div()
.size_full()
.child(picture)
.with_animation(
(self.id.clone(), format!("fade-{turn}")),
Animation::new(length).with_easing(motion::ease_out_cubic),
|shown, t| shown.opacity(0.4 + 0.6 * t),
)
.into_any_element(),
}
});
let vary = |strong: bool, label: &'static str| {
self.on_vary.clone().map(|vary| {
Button::new((self.id.clone(), label), label)
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
.icon(IconName::Shuffle)
.on_click(move |_, window, cx| {
log::info!("variation picker: vary, strong {strong}");
vary(strong, window, cx)
})
})
self.on_vary
.clone()
.filter(|_| self.chosen.is_some())
.map(|vary| {
Button::new((self.id.clone(), label), label)
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
.icon(IconName::Shuffle)
.on_click(move |_, window, cx| {
log::info!("variation picker: vary, strong {strong}");
vary(strong, window, cx)
})
})
};
div()
.flex()
.flex_col()
.gap_3()
.child(framed(self.ratio, cx).rounded(large).child(shown))
.child(framed(self.ratio, cx).rounded(large).children(shown))
.child(div().flex().flex_wrap().gap_2().children(
self.pictures.iter().zip(thumbs).enumerate().map(
|(ix, (picture, (focus, focused)))| {
let chosen = ix == self.chosen;
let chosen = self.chosen == Some(ix);
let choose = self.on_choose.clone();
div()
.id((self.id.clone(), format!("thumb-{ix}")))
@@ -176,16 +183,21 @@ impl RenderOnce for VariationPicker {
.gap_2()
.children(vary(false, "Vary subtly"))
.children(vary(true, "Vary strongly"))
.children(self.on_upscale.clone().map(|upscale| {
Button::new((self.id.clone(), "upscale"), "Upscale")
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
.icon(IconName::Maximize2)
.on_click(move |_, window, cx| {
log::info!("variation picker: upscale");
upscale(window, cx)
})
})),
.children(
self.on_upscale
.clone()
.filter(|_| self.chosen.is_some())
.map(|upscale| {
Button::new((self.id.clone(), "upscale"), "Upscale")
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
.icon(IconName::Maximize2)
.on_click(move |_, window, cx| {
log::info!("variation picker: upscale");
upscale(window, cx)
})
}),
),
)
}
}
+23 -16
View File
@@ -118,7 +118,7 @@ pub struct PromptVersion {
pub struct PromptVersionHistory {
id: ElementId,
versions: Vec<PromptVersion>,
chosen: usize,
chosen: Option<usize>,
on_choose: Option<Pick>,
on_restore: Option<Pick>,
}
@@ -131,15 +131,17 @@ impl PromptVersionHistory {
chosen: usize,
) -> Self {
let versions: Vec<PromptVersion> = versions.into_iter().collect();
assert!(
chosen < versions.len(),
"version {chosen} of {}",
versions.len()
);
let held = chosen < versions.len();
if !held {
log::error!(
"prompt versions: version {chosen} of {}; none chosen",
versions.len()
);
}
Self {
id: id.into(),
versions,
chosen,
chosen: held.then_some(chosen),
on_choose: None,
on_restore: None,
}
@@ -175,13 +177,18 @@ impl RenderOnce for PromptVersionHistory {
let theme = cx.theme();
let colors = theme.colors.clone();
let chosen = self.chosen;
let (heading, before) = compared(&self.versions, chosen);
let shown = &self.versions[chosen];
let restore = self
.on_restore
.clone()
.filter(|_| chosen > 0)
.map(|restore| {
let diff = chosen.map(|chosen| {
let (heading, before) = compared(&self.versions, chosen);
(heading, before, self.versions[chosen].text.clone())
});
let heading: SharedString = match &diff {
Some((heading, ..)) => heading.clone(),
None => "No version is chosen".into(),
};
let restore = chosen
.filter(|chosen| *chosen > 0)
.zip(self.on_restore.clone())
.map(|(chosen, restore)| {
Button::new((self.id.clone(), "restore"), "Restore")
.variant(ButtonVariant::Secondary)
.size(ControlSize::Sm)
@@ -192,7 +199,7 @@ impl RenderOnce for PromptVersionHistory {
});
let rows = self.versions.iter().zip(focuses).enumerate().map(
|(ix, (version, (focus, focused)))| {
let on = ix == chosen;
let on = chosen == Some(ix);
let choose = self.on_choose.clone();
div()
.id((self.id.clone(), format!("version-{ix}")))
@@ -291,7 +298,7 @@ impl RenderOnce for PromptVersionHistory {
)
.children(restore),
)
.child(PromptDiff::new(before, shown.text.clone())),
.children(diff.map(|(_, before, text)| PromptDiff::new(before, text))),
)
}
}
+5 -3
View File
@@ -93,9 +93,11 @@ impl VideoGenerationTimeline {
impl RenderOnce for VideoGenerationTimeline {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let named = (self.selected.as_ref())
.is_none_or(|key| self.shots.iter().any(|shot| &shot.key == key));
assert!(named, "the chosen shot is not listed");
if let Some(key) = &self.selected
&& !self.shots.iter().any(|shot| &shot.key == key)
{
log::error!("video timeline: shot {key} is not listed; none selected");
}
let pickable = self.on_select.is_some();
let scroll = window
.use_keyed_state((self.id.clone(), "scroll"), cx, |_, _| ScrollHandle::new())
+5 -8
View File
@@ -80,14 +80,11 @@ impl TTSVoicePicker {
impl RenderOnce for TTSVoicePicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let known = |key: &Option<SharedString>| {
key.as_ref()
.is_none_or(|key| self.voices.iter().any(|voice| &voice.key == key))
};
assert!(
known(&self.selected) && known(&self.playing),
"a voice named is not listed"
);
for key in [&self.selected, &self.playing].into_iter().flatten() {
if !self.voices.iter().any(|voice| &voice.key == key) {
log::error!("voice picker: voice {key} is not listed; none marked");
}
}
let pickable = self.on_select.is_some();
let focuses: Vec<_> = self
.voices