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

+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)
);
});
}