T25a Generative: prompt and settings

PromptEnhancer offers a fuller prompt with the words it adds washed,
split at punctuation; Use this takes it into the field, Keep mine
lets it go. StylePresetPicker and AspectRatioPicker are rows of Tab
stops, the chosen one in accent; a ratio chip draws its shape.
SeedInput keeps digits up to u32::MAX and rolls one with a die.
NegativePromptInput and ImageGenerationPanel point to forms::TagInput
and forms::Form. theme.generative() holds the chapter's sizes. The
gallery's preset pictures are the atrium photo in five styles.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 12:13:17 -04:00
1 parent 173e89ed03
commit 66345b7e24
22 files changed
+1169 -10

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
mod prompt;
mod ratio;
mod seed;
mod style;
#[cfg(all(test, feature = "test-support"))]
mod tests;
pub use prompt::PromptEnhancer;
pub use ratio::AspectRatioPicker;
pub use seed::SeedInput;
pub use style::{StylePreset, StylePresetPicker};
+238
View File
@@ -0,0 +1,238 @@
use std::{ops::Range, rc::Rc};
use gpui::{
App, ElementId, Entity, Focusable, FontWeight, HighlightStyle, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, StyledText, Window, div,
};
use similar::{ChangeTag, TextDiff};
use crate::{
buttons::{Button, ButtonVariant},
forms::{Input, OnFlag, TextInput},
motion::{Entrance, Transition},
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
};
type OnText = Rc<dyn Fn(&str, &mut Window, &mut App)>;
/// Byte ranges of `new` that `old` lacks, word by word: neighbors joined, spaces and punctuation trimmed from each end.
pub(crate) fn added_words(old: &str, new: &str) -> Vec<Range<usize>> {
let diff = TextDiff::from_unicode_words(old, new);
let mut ranges: Vec<Range<usize>> = Vec::new();
let mut at = 0;
for change in diff.iter_all_changes() {
let len = change.value().len();
match change.tag() {
ChangeTag::Delete => continue,
ChangeTag::Equal => {}
ChangeTag::Insert => match ranges.last_mut() {
Some(last) if last.end == at => last.end = at + len,
_ => ranges.push(at..at + len),
},
}
at += len;
}
ranges
.into_iter()
.filter_map(|range| {
let word = &new[range.clone()];
let glue = |ch: char| ch.is_whitespace() || ch.is_ascii_punctuation();
let start = range.start + word.len() - word.trim_start_matches(glue).len();
let end = range.end - (word.len() - word.trim_end_matches(glue).len());
(start < end).then_some(start..end)
})
.collect()
}
/// A prompt and a way to rewrite it: Enhance asks the owner for a fuller prompt, offered with the words it adds washed; Use this takes it, Keep mine lets it go.
#[derive(IntoElement)]
pub struct PromptEnhancer {
id: ElementId,
field: Entity<TextInput>,
enhancing: bool,
suggestion: Option<SharedString>,
on_enhance: Option<OnText>,
on_resolve: Option<OnFlag>,
}
impl PromptEnhancer {
/// `field` is the owner's prompt.
pub fn new(id: impl Into<ElementId>, field: &Entity<TextInput>) -> Self {
Self {
id: id.into(),
field: field.clone(),
enhancing: false,
suggestion: None,
on_enhance: None,
on_resolve: None,
}
}
/// While the owner writes a suggestion.
pub fn enhancing(mut self, enhancing: bool) -> Self {
self.enhancing = enhancing;
self
}
/// A fuller prompt to offer.
pub fn suggestion(mut self, text: impl Into<SharedString>) -> Self {
self.suggestion = Some(text.into());
self
}
/// Gets the prompt to rewrite, trimmed.
pub fn on_enhance(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
self.on_enhance = Some(Rc::new(handler));
self
}
/// Gets whether the suggestion was taken; a taken one is already in the field.
pub fn on_resolve(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
self.on_resolve = Some(Rc::new(handler));
self
}
}
impl RenderOnce for PromptEnhancer {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let last = window.use_keyed_state((self.id.clone(), "offer"), cx, |_, _| {
SharedString::default()
});
if let Some(text) = self
.suggestion
.as_ref()
.filter(|text| *last.read(cx) != **text)
{
last.update(cx, |last, _| *last = text.clone());
}
let (offered, shown) = (last.read(cx).clone(), self.suggestion.is_some());
let prompt = self.field.read(cx).text().trim().to_string();
let words = added_words(&prompt, &offered);
let theme = cx.theme();
let colors = theme.colors.clone();
let enhance = self.on_enhance.map(|enhance| {
Button::new((self.id.clone(), "enhance"), "Enhance")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.icon(IconName::Sparkles)
.loading(self.enhancing)
.disabled(prompt.is_empty() || self.enhancing)
.on_click(move |_, window, cx| {
log::info!("prompt enhancer: asked with {} characters", prompt.len());
enhance(&prompt, window, cx)
})
});
let answer = |taken: bool| {
let (field, text, resolve) =
(self.field.clone(), offered.clone(), self.on_resolve.clone());
move |_: &gpui::ClickEvent, window: &mut Window, cx: &mut App| {
if taken {
field.update(cx, |input, cx| input.set_text(text.to_string(), cx));
}
log::info!(
"prompt enhancer: suggestion {}",
if taken { "taken" } else { "kept out" }
);
window.focus(&field.focus_handle(cx));
if let Some(resolve) = &resolve {
resolve(taken, window, cx);
}
}
};
let wash = HighlightStyle {
background_color: Some(colors.success_subtle),
..HighlightStyle::default()
};
let card = div()
.mt_1()
.flex()
.flex_col()
.gap_3()
.p_3()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(colors.border)
.bg(colors.surface)
.child(
div()
.flex()
.items_center()
.gap_1p5()
.text_size(theme.text_size(TextSize::Xs))
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg_muted)
.child(
Icon::new(IconName::Sparkles)
.size(IconSize::Xs)
.color(colors.fg_muted),
)
.child("Suggested"),
)
.child(
div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(colors.fg)
.child(
StyledText::new(offered.clone())
.with_highlights(words.into_iter().map(|range| (range, wash))),
),
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(
Button::new((self.id.clone(), "take"), "Use this")
.variant(ButtonVariant::Primary)
.size(ControlSize::Sm)
.disabled(!shown)
.on_click(answer(true)),
)
.child(
Button::new((self.id.clone(), "keep"), "Keep mine")
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.disabled(!shown)
.on_click(answer(false)),
),
);
div()
.w_full()
.flex()
.flex_col()
.gap_2()
.child(Input::new(&self.field))
.children(enhance.map(|button| div().flex().justify_end().child(button)))
.child(
Transition::new((self.id.clone(), "offered"), shown)
.entrance(Entrance::Rise)
.child(card),
)
}
}
#[cfg(test)]
mod tests {
use super::added_words;
#[test]
fn added_words_are_the_new_runs_without_their_spaces() {
let new = "a fluffy cat asleep on a sunlit mat";
let ranges = added_words("a cat on a mat", new);
let words: Vec<&str> = ranges.iter().map(|range| &new[range.clone()]).collect();
assert_eq!(words, ["fluffy", "asleep", "sunlit"]);
let new = "a white atrium, pale limestone";
let ranges = added_words("a white atrium", new);
let words: Vec<&str> = ranges.iter().map(|range| &new[range.clone()]).collect();
assert_eq!(
words,
["pale limestone"],
"a comma added after a kept word stays plain"
);
assert!(added_words("same words", "same words").is_empty());
let whole = 2..7;
assert_eq!(added_words("", " a cat "), [whole]);
}
}
+164
View File
@@ -0,0 +1,164 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window, div, prelude::*,
};
use crate::{
primitives::tab_stop,
theme::{ActiveTheme, IconSize, Radius, TextSize},
typography::tabular,
};
type OnRatio = Rc<dyn Fn((u32, u32), &mut Window, &mut App)>;
/// The share of a square a `width:height` shape spans across and down; its long side spans it all.
pub(crate) fn span(width: u32, height: u32) -> (f32, f32) {
assert!(width > 0 && height > 0, "a ratio of {width}:{height}");
let (width, height) = (width as f32, height as f32);
let long = width.max(height);
(width / long, height / long)
}
/// A picture's shape: a chip per ratio, each with an outline drawn to its proportions. The chosen chip takes accent; each is a Tab stop.
#[derive(IntoElement)]
pub struct AspectRatioPicker {
id: ElementId,
ratios: Vec<(u32, u32)>,
selected: (u32, u32),
on_select: Option<OnRatio>,
}
impl AspectRatioPicker {
/// `selected` is one of `ratios`, each `(width, height)`.
pub fn new(
id: impl Into<ElementId>,
ratios: impl IntoIterator<Item = (u32, u32)>,
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
);
Self {
id: id.into(),
ratios,
selected,
on_select: None,
}
}
pub fn on_select(
mut self,
handler: impl Fn((u32, u32), &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
}
impl RenderOnce for AspectRatioPicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let chips: Vec<_> = self
.ratios
.iter()
.map(|&(width, height)| {
let id: ElementId = (self.id.clone(), format!("ratio-{width}-{height}")).into();
let focus = tab_stop((id.clone(), "focus").into(), true, window, cx);
(id, focus.is_focused(window), focus, (width, height))
})
.collect();
let theme = cx.theme();
let colors = theme.colors.clone();
let glyph = theme.icon_size(IconSize::Md);
div()
.w_full()
.flex()
.flex_wrap()
.gap_2()
.children(chips.into_iter().map(|(id, focused, focus, ratio)| {
let chosen = ratio == self.selected;
let ink = if chosen {
colors.accent
} else {
colors.fg_muted
};
let border = if focused {
colors.focus
} else if chosen {
colors.accent
} else {
colors.border
};
let (across, down) = span(ratio.0, ratio.1);
let pick = self.on_select.clone();
div()
.id(id)
.track_focus(&focus)
.flex()
.items_center()
.gap_1p5()
.px_2p5()
.py_1()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(border)
.text_size(theme.text_size(TextSize::Sm))
.font_weight(FontWeight::MEDIUM)
.text_color(ink)
.cursor_pointer()
.when(!chosen && !focused, |chip| {
chip.hover(|style| style.border_color(colors.border_strong))
})
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(move |_, window, cx| {
if chosen {
return;
}
log::info!("aspect ratio: {}:{}", ratio.0, ratio.1);
if let Some(pick) = &pick {
pick(ratio, window, cx);
}
})
.child(
div()
.flex_none()
.size(glyph)
.flex()
.items_center()
.justify_center()
.child(
div()
.w(glyph * across)
.h(glyph * down)
.rounded(theme.radius(Radius::Sm))
.border_1()
.border_color(ink),
),
)
.child(tabular(div()).child(format!("{}:{}", ratio.0, ratio.1)))
}))
}
}
#[cfg(test)]
mod tests {
use super::span;
#[test]
fn a_ratio_spans_its_long_side_whole() {
assert_eq!(span(16, 9), (1.0, 0.5625));
assert_eq!(span(9, 16), (0.5625, 1.0));
assert_eq!(span(1, 1), (1.0, 1.0));
}
#[test]
#[should_panic(expected = "a ratio of 0:1")]
fn an_empty_side_fails_loud() {
span(0, 1);
}
}
+92
View File
@@ -0,0 +1,92 @@
use std::{
hash::{BuildHasher, Hasher, RandomState},
ops::Range,
rc::Rc,
};
use gpui::{App, ElementId, Entity, IntoElement, RenderOnce, Window};
use crate::{
buttons::IconButton,
forms::{InputAddon, InputGroup, TextInput},
primitives::IconName,
};
/// Fits an edit into a seed: digits only, and nothing past `u32::MAX`; a refused edit keeps the text.
pub(crate) fn digits(text: &str, range: Range<usize>, typed: &str) -> (String, usize) {
let kept: String = typed.chars().filter(char::is_ascii_digit).collect();
let next = format!("{}{kept}{}", &text[..range.start], &text[range.end..]);
if next.is_empty() || next.parse::<u32>().is_ok() {
(next, range.start + kept.len())
} else {
(text.to_string(), range.start)
}
}
/// A seed from the system's source of randomness.
fn roll() -> u32 {
RandomState::new().build_hasher().finish() as u32
}
/// A generation's seed in the owner's field: digits up to `u32::MAX`, empty for a new one each run, and a die that rolls one.
#[derive(IntoElement)]
pub struct SeedInput {
id: ElementId,
field: Entity<TextInput>,
}
impl SeedInput {
pub fn new(id: impl Into<ElementId>, field: &Entity<TextInput>) -> Self {
Self {
id: id.into(),
field: field.clone(),
}
}
/// The seed in `field`; none while it is empty.
pub fn read(field: &Entity<TextInput>, cx: &App) -> Option<u32> {
let text = field.read(cx).text();
(!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);
self.field.update(cx, |input, cx| {
input.set_fit(Rc::new(digits));
let text = input.text().to_string();
input.set_text(text, cx);
input.set_placeholder("Random", cx);
});
}
let field = self.field.clone();
InputGroup::new(&self.field).after(InputAddon::new(
IconButton::new((self.id.clone(), "roll"), IconName::Dices)
.tooltip("Roll a seed")
.on_click(move |_, _, cx| {
let seed = roll();
log::info!("seed input: rolled {seed}");
field.update(cx, |input, cx| input.set_text(seed.to_string(), cx));
}),
))
}
}
#[cfg(test)]
mod tests {
use super::digits;
#[test]
fn a_seed_keeps_digits_up_to_the_largest() {
assert_eq!(digits("12", 2..2, "3a4"), ("1234".to_string(), 4));
assert_eq!(digits("", 0..0, "12,345"), ("12345".to_string(), 5));
assert_eq!(
digits("4294967295", 10..10, "1"),
("4294967295".to_string(), 10)
);
assert_eq!(digits("1234", 1..3, ""), ("14".to_string(), 1));
}
}
+148
View File
@@ -0,0 +1,148 @@
use std::rc::Rc;
use gpui::{
App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window, div, transparent_black,
};
use crate::{
documents::source,
forms::OnValue,
primitives::{Icon, IconName, Image, tab_stop},
theme::{ActiveTheme, IconSize, Radius, TextSize},
typography::Ellipsis,
};
/// A style to generate in: its key, its name, and a picture of it; one with no picture draws a plain tile.
#[derive(Clone, Debug, PartialEq)]
pub struct StylePreset {
pub key: SharedString,
pub name: SharedString,
pub picture: Option<SharedString>,
}
/// Styles to generate in, a tile each with its picture and name. The chosen tile takes accent; each is a Tab stop.
#[derive(IntoElement)]
pub struct StylePresetPicker {
id: ElementId,
presets: Vec<StylePreset>,
selected: SharedString,
on_select: Option<OnValue>,
}
impl StylePresetPicker {
/// `selected` names one of `presets` by key.
pub fn new(
id: impl Into<ElementId>,
presets: impl IntoIterator<Item = StylePreset>,
selected: impl Into<SharedString>,
) -> 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"
);
Self {
id: id.into(),
presets,
selected,
on_select: None,
}
}
pub fn on_select(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
}
impl RenderOnce for StylePresetPicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let tiles: Vec<_> = self
.presets
.into_iter()
.map(|preset| {
let id: ElementId = (self.id.clone(), format!("preset-{}", preset.key)).into();
let focus = tab_stop((id.clone(), "focus").into(), true, window, cx);
(id, focus.is_focused(window), focus, preset)
})
.collect();
let theme = cx.theme();
let colors = theme.colors.clone();
let side = theme.generative().preset;
div()
.w_full()
.flex()
.flex_wrap()
.gap_3()
.children(tiles.into_iter().map(|(id, focused, focus, preset)| {
let chosen = preset.key == self.selected;
let ring = if focused {
colors.focus
} else if chosen {
colors.accent
} else {
transparent_black()
};
let (key, pick) = (preset.key.clone(), self.on_select.clone());
let inner = theme.radius(Radius::Md);
let picture = match &preset.picture {
Some(path) => Image::new((id.clone(), "picture"), source(path))
.size_full()
.rounded(inner)
.into_any_element(),
None => div()
.size_full()
.rounded(inner)
.flex()
.items_center()
.justify_center()
.bg(colors.sunken)
.child(
Icon::new(IconName::Ban)
.size(IconSize::Md)
.color(colors.fg_subtle),
)
.into_any_element(),
};
div()
.id(id)
.track_focus(&focus)
.w(side)
.flex()
.flex_col()
.gap_1p5()
.cursor_pointer()
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(move |_, window, cx| {
if chosen {
return;
}
log::info!("style preset: {key}");
if let Some(pick) = &pick {
pick(&key, window, cx);
}
})
.child(
div()
.size(side)
.p_0p5()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(ring)
.child(picture),
)
.child(
div()
.px_0p5()
.text_size(theme.text_size(TextSize::Xs))
.text_color(if chosen { colors.fg } else { colors.fg_muted })
.child(Ellipsis::new(preset.name)),
)
}))
}
}
+216
View File
@@ -0,0 +1,216 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
AppContext as _, Context, Entity, IntoElement, KeyBinding, KeyUpEvent, Keystroke,
ParentElement, Render, SharedString, Styled, TestAppContext, VisualTestContext, Window, div,
px,
};
use crate::{
forms::{self, TextInput},
generative::{AspectRatioPicker, PromptEnhancer, SeedInput, StylePreset, StylePresetPicker},
primitives::FocusNext,
theme::Theme,
};
fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
/// Presses and releases `key`, a frame apart.
fn press(key: &str, cx: &mut VisualTestContext) {
cx.simulate_keystrokes(key);
settle(cx);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse(key).expect("a key"),
});
settle(cx);
}
fn tab(times: usize, cx: &mut VisualTestContext) {
for _ in 0..times {
cx.update(|window, _| window.focus_next());
}
}
fn setup(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
});
}
/// A prompt with its enhancer, a suggestion the test sets, and what it heard.
struct Enhancing {
field: Entity<TextInput>,
suggestion: Option<SharedString>,
heard: Vec<String>,
}
impl Render for Enhancing {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (asked, resolved) = (cx.entity(), cx.entity());
let enhancer = PromptEnhancer::new("enhancer", &self.field)
.on_enhance(move |prompt, _, cx| {
asked.update(cx, |view, _| view.heard.push(format!("asked {prompt}")))
})
.on_resolve(move |taken, _, cx| {
resolved.update(cx, |view, _| {
view.heard.push(format!("taken {taken}"));
view.suggestion = None;
})
});
let enhancer = match &self.suggestion {
Some(text) => enhancer.suggestion(text.clone()),
None => enhancer,
};
div().w(px(420.0)).child(enhancer)
}
}
fn enhancing(cx: &mut TestAppContext) -> (Entity<Enhancing>, &mut VisualTestContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|window, cx| Enhancing {
field: cx.new(|cx| TextInput::new(window, cx)),
suggestion: None,
heard: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
(view, cx)
}
#[gpui::test]
fn enhance_asks_with_the_trimmed_prompt_and_use_this_takes_the_offer(cx: &mut TestAppContext) {
let (view, cx) = enhancing(cx);
tab(1, cx);
cx.simulate_input(" a cat ");
tab(1, cx);
press("enter", cx);
view.update(cx, |view, cx| {
view.suggestion = Some("a fluffy cat asleep".into());
cx.notify();
});
settle(cx);
tab(1, cx);
press("enter", cx);
view.read_with(cx, |view, cx| {
assert_eq!(view.heard, ["asked a cat", "taken true"]);
assert_eq!(view.field.read(cx).text(), "a fluffy cat asleep");
});
}
#[gpui::test]
fn keep_mine_leaves_the_prompt_as_written(cx: &mut TestAppContext) {
let (view, cx) = enhancing(cx);
tab(1, cx);
cx.simulate_input("a cat");
view.update(cx, |view, cx| {
view.suggestion = Some("a fluffy cat".into());
cx.notify();
});
settle(cx);
tab(3, cx);
press("enter", cx);
view.read_with(cx, |view, cx| {
assert_eq!(view.heard, ["taken false"]);
assert_eq!(view.field.read(cx).text(), "a cat");
});
}
/// Three presets, two ratios, a seed, and every pick heard.
struct Picks {
seed: Entity<TextInput>,
style: SharedString,
heard: Rc<RefCell<Vec<String>>>,
}
impl Render for Picks {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (styled, shaped) = (self.heard.clone(), self.heard.clone());
let preset = |key: &str| StylePreset {
key: key.to_string().into(),
name: key.to_string().into(),
picture: None,
};
div()
.w(px(420.0))
.child(
StylePresetPicker::new(
"styles",
[preset("a"), preset("b"), preset("c")],
self.style.clone(),
)
.on_select(move |key, _, _| styled.borrow_mut().push(format!("style {key}"))),
)
.child(
AspectRatioPicker::new("ratios", [(1, 1), (4, 3)], (1, 1)).on_select(
move |ratio, _, _| {
shaped
.borrow_mut()
.push(format!("ratio {}:{}", ratio.0, ratio.1))
},
),
)
.child(SeedInput::new("seed", &self.seed))
}
}
fn picks(
cx: &mut TestAppContext,
) -> (
Entity<Picks>,
Rc<RefCell<Vec<String>>>,
&mut VisualTestContext,
) {
setup(cx);
let heard = Rc::new(RefCell::new(Vec::new()));
let store = heard.clone();
let (view, cx) = cx.add_window_view(|window, cx| Picks {
seed: cx.new(|cx| TextInput::new(window, cx)),
style: "a".into(),
heard: store,
});
cx.update(|window, _| window.activate_window());
settle(cx);
(view, heard, cx)
}
#[gpui::test]
fn tabs_reach_each_tile_and_chip_and_enter_picks_it(cx: &mut TestAppContext) {
let (_, heard, cx) = picks(cx);
tab(1, cx);
press("enter", cx);
tab(1, cx);
press("enter", cx);
tab(3, cx);
press("enter", cx);
assert_eq!(
*heard.borrow(),
["style b", "ratio 4:3"],
"the chosen tile stays quiet"
);
}
#[gpui::test]
fn a_seed_keeps_digits_and_the_die_rolls_one(cx: &mut TestAppContext) {
let (view, _, cx) = picks(cx);
tab(6, cx);
cx.simulate_input("12a3");
view.read_with(cx, |view, cx| {
assert_eq!(SeedInput::read(&view.seed, cx), Some(123))
});
tab(1, cx);
press("enter", cx);
view.read_with(cx, |view, cx| {
let text = view.seed.read(cx).text().to_string();
assert_ne!(text, "123", "the die rolls a new seed");
assert_eq!(
SeedInput::read(&view.seed, cx).map(|seed| seed.to_string()),
Some(text)
);
});
}