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.
No files matched your search
@@ -83,7 +83,12 @@ Per-component lines live in `tasks/`. Tags there:
|
||||
- [x] T24d Agent · Previews
|
||||
- [x] T24e Agent · Environment
|
||||
- [x] T24f Agent · Control and cost
|
||||
- [ ] T25 Generative
|
||||
- [ ] T25a Generative · Prompt and settings
|
||||
- [ ] T25b Generative · Queue and results
|
||||
- [ ] T25c Generative · Canvas
|
||||
- [ ] T25d Generative · Sound and motion
|
||||
- [ ] T25e Generative · Models
|
||||
- [ ] T25f Generative · Data and prompts
|
||||
- [ ] T26 Media
|
||||
- [ ] T27 Files
|
||||
- [ ] T28 Messaging
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-ban"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M4.929 4.929 19.07 19.071" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,20 @@
|
||||
<!-- @license lucide-static v1.48.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-dices"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="12" height="12" x="2" y="10" rx="2" ry="2" />
|
||||
<path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" />
|
||||
<path d="M6 18h.01" />
|
||||
<path d="M10 14h.01" />
|
||||
<path d="M15 6h.01" />
|
||||
<path d="M18 9h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,26 @@
|
||||
mod panel;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 25,
|
||||
slug: "generative",
|
||||
title: "Generative",
|
||||
summary: "Making pictures, sound and video with a model: the prompt and its settings, the queue and what comes back, the canvas, and the models behind them.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::DownAt("gen-enhancer", 356.0, 124.0),
|
||||
Step::UpAt("gen-enhancer", 356.0, 124.0),
|
||||
Step::Wait(1400),
|
||||
Step::Shot("suggested"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div().child(panel::panel(window, cx)).into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
forms::{Form, FormField, TagInput, TextInput},
|
||||
generative::{AspectRatioPicker, PromptEnhancer, SeedInput, StylePreset, StylePresetPicker},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, section, set},
|
||||
};
|
||||
|
||||
type Run = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
|
||||
/// What an enhancer might give back; the demo adds light, material and lens.
|
||||
fn fuller(prompt: &str) -> String {
|
||||
format!(
|
||||
"{prompt}, pale limestone, soft morning light through tall windows, long shadows, 35mm, quiet and airy"
|
||||
)
|
||||
}
|
||||
|
||||
fn presets() -> Vec<StylePreset> {
|
||||
let preset = |key: &str, name: &str, picture: Option<&str>| StylePreset {
|
||||
key: key.to_string().into(),
|
||||
name: name.to_string().into(),
|
||||
picture: picture.map(|path| path.to_string().into()),
|
||||
};
|
||||
vec![
|
||||
preset("none", "None", None),
|
||||
preset(
|
||||
"photo",
|
||||
"Photographic",
|
||||
Some(asset!("styles/photographic.jpg")),
|
||||
),
|
||||
preset("mono", "Monochrome", Some(asset!("styles/monochrome.jpg"))),
|
||||
preset("line", "Line art", Some(asset!("styles/line-art.jpg"))),
|
||||
preset("wash", "Watercolor", Some(asset!("styles/watercolor.jpg"))),
|
||||
preset("film", "Cinematic", Some(asset!("styles/cinematic.jpg"))),
|
||||
]
|
||||
}
|
||||
|
||||
/// The demo's settings: a suggestion on its way or offered, the left-out words, style, shape, and the last run.
|
||||
#[derive(Clone)]
|
||||
struct Settings {
|
||||
enhancing: bool,
|
||||
suggestion: Option<SharedString>,
|
||||
left_out: Vec<SharedString>,
|
||||
style: SharedString,
|
||||
ratio: (u32, u32),
|
||||
ran: Option<SharedString>,
|
||||
}
|
||||
|
||||
fn edit(settings: &Entity<Settings>, cx: &mut App, change: impl FnOnce(&mut Settings)) {
|
||||
let mut next = settings.read(cx).clone();
|
||||
change(&mut next);
|
||||
set(settings, next, cx)
|
||||
}
|
||||
|
||||
pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let prompt = window.use_keyed_state("gen-prompt-field", cx, |window, cx| {
|
||||
let mut input = TextInput::new(window, cx)
|
||||
.multi_line(3, 6)
|
||||
.placeholder("Describe a picture");
|
||||
input.set_text("a spiral staircase in a white atrium", cx);
|
||||
input
|
||||
});
|
||||
let seed = window.use_keyed_state("gen-seed-field", cx, |window, cx| {
|
||||
let mut input = TextInput::new(window, cx);
|
||||
input.set_text("2710", cx);
|
||||
input
|
||||
});
|
||||
let settings = keep(
|
||||
"gen-settings",
|
||||
|| Settings {
|
||||
enhancing: false,
|
||||
suggestion: None,
|
||||
left_out: vec!["blurry".into(), "watermark".into(), "people".into()],
|
||||
style: "photo".into(),
|
||||
ratio: (3, 2),
|
||||
ran: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let now = settings.read(cx).clone();
|
||||
let run: Run = {
|
||||
let (settings, seed, prompt) = (settings.clone(), seed.clone(), prompt.clone());
|
||||
Rc::new(move |_, cx| {
|
||||
let seed = SeedInput::read(&seed, cx)
|
||||
.map_or("a new seed".to_string(), |seed| format!("seed {seed}"));
|
||||
let words = prompt.read(cx).text().split_whitespace().count();
|
||||
let line = format!("Queued: {words} words, {seed}");
|
||||
edit(&settings, cx, |settings| settings.ran = Some(line.into()));
|
||||
})
|
||||
};
|
||||
let (asked, resolved, tagged, styled, shaped) = (
|
||||
settings.clone(),
|
||||
settings.clone(),
|
||||
settings.clone(),
|
||||
settings.clone(),
|
||||
settings,
|
||||
);
|
||||
let enhancer = PromptEnhancer::new("gen-prompt", &prompt)
|
||||
.enhancing(now.enhancing)
|
||||
.on_enhance(move |text, _, cx| {
|
||||
edit(&asked, cx, |settings| settings.enhancing = true);
|
||||
let (asked, text) = (asked.clone(), fuller(text));
|
||||
cx.spawn(async move |cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(900))
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
edit(&asked, cx, |settings| {
|
||||
settings.enhancing = false;
|
||||
settings.suggestion = Some(text.into());
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.on_resolve(move |_, _, cx| edit(&resolved, cx, |settings| settings.suggestion = None));
|
||||
let enhancer = match now.suggestion {
|
||||
Some(text) => enhancer.suggestion(text),
|
||||
None => enhancer,
|
||||
};
|
||||
let submit = run.clone();
|
||||
section(
|
||||
"ImageGenerationPanel / PromptEnhancer / NegativePromptInput / StylePresetPicker / AspectRatioPicker / SeedInput",
|
||||
"A picture's settings in one form: the prompt, which Enhance offers to fill out, the words it adds washed; what to leave out; a style; a shape; and a seed, empty for a new one each run. Cmd-Enter generates.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div().w(px(400.)).child(
|
||||
Form::new("gen-form")
|
||||
.on_submit(move |window, cx| submit(window, cx))
|
||||
.child(probe(
|
||||
"gen-enhancer",
|
||||
div().w(px(400.)).child(FormField::new("gen-prompt-label", "Prompt").child(enhancer)),
|
||||
))
|
||||
.child(
|
||||
FormField::new("gen-left-out", "Leave out").child(
|
||||
TagInput::new("gen-left-out-tags", now.left_out)
|
||||
.placeholder("A word, then Enter")
|
||||
.on_change(move |tags, _, cx| edit(&tagged, cx, |settings| settings.left_out = tags)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
FormField::new("gen-style", "Style").child(
|
||||
StylePresetPicker::new("gen-styles", presets(), now.style)
|
||||
.on_select(move |key, _, cx| {
|
||||
let key = key.clone();
|
||||
edit(&styled, cx, |settings| settings.style = key)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
FormField::new("gen-shape", "Shape").child(
|
||||
AspectRatioPicker::new("gen-ratios", [(1, 1), (4, 5), (3, 2), (16, 9), (9, 16)], now.ratio)
|
||||
.on_select(move |ratio, _, cx| edit(&shaped, cx, |settings| settings.ratio = ratio)),
|
||||
),
|
||||
)
|
||||
.child(FormField::new("gen-seed", "Seed").child(SeedInput::new("gen-seed-input", &seed)))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("gen-generate", "Generate")
|
||||
.variant(ButtonVariant::Primary)
|
||||
.full_width()
|
||||
.shortcut("secondary-enter")
|
||||
.on_click(move |_, window, cx| run(window, cx)),
|
||||
)
|
||||
.children(now.ran.map(Caption::new)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod editor;
|
||||
mod feedback;
|
||||
mod finance;
|
||||
mod forms;
|
||||
mod generative;
|
||||
mod git;
|
||||
mod layout;
|
||||
mod lists;
|
||||
@@ -63,6 +64,7 @@ pub const ALL: &[Page] = &[
|
||||
collab::PAGE,
|
||||
chat::PAGE,
|
||||
agent::PAGE,
|
||||
generative::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
@@ -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};
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod editor;
|
||||
pub mod feedback;
|
||||
pub mod finance;
|
||||
pub mod forms;
|
||||
pub mod generative;
|
||||
pub mod git;
|
||||
pub mod layout;
|
||||
pub mod lists;
|
||||
|
||||
@@ -56,6 +56,7 @@ icons! {
|
||||
ArrowUpRight => "arrow-up-right",
|
||||
ArrowUp => "arrow-up",
|
||||
AtSign => "at-sign",
|
||||
Ban => "ban",
|
||||
Battery => "battery",
|
||||
Bell => "bell",
|
||||
Bold => "bold",
|
||||
@@ -115,6 +116,7 @@ icons! {
|
||||
Crop => "crop",
|
||||
Crosshair => "crosshair",
|
||||
Database => "database",
|
||||
Dices => "dices",
|
||||
DollarSign => "dollar-sign",
|
||||
Dot => "dot",
|
||||
Download => "download",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
use gpui::Rems;
|
||||
|
||||
use super::{Theme, tokens::px_to_rems};
|
||||
|
||||
/// Generative tools' measures.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct GenerativeSizes {
|
||||
/// A style preset's picture, square.
|
||||
pub preset: Rems,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn generative(&self) -> GenerativeSizes {
|
||||
GenerativeSizes {
|
||||
preset: px_to_rems(88.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
mod chart;
|
||||
mod generative;
|
||||
mod palette;
|
||||
mod tokens;
|
||||
|
||||
@@ -8,6 +9,7 @@ use gpui::{App, Global, SharedString, WindowAppearance};
|
||||
|
||||
use crate::motion;
|
||||
pub use chart::ChartSizes;
|
||||
pub use generative::GenerativeSizes;
|
||||
pub use palette::{Mix, Palette, Syntax};
|
||||
pub use tokens::{
|
||||
AvatarSize, ContainerSize, ControlSize, Density, Elevation, IconSize, Radius, TextSize,
|
||||
|
||||
@@ -208,33 +208,48 @@
|
||||
- [x] TraceViewer → `debug::TimelineProfiler` (a track per depth)
|
||||
- [x] EvalResultTable → `tables::DataTable` (Tag and Progress cells)
|
||||
|
||||
## T25 Generative — `src/generative`
|
||||
## T25a Generative · Prompt and settings — `src/generative`
|
||||
|
||||
- [x] ImageGenerationPanel → `forms::Form` of the fields below; Cmd-Enter generates
|
||||
- [x] PromptEnhancer
|
||||
- [x] NegativePromptInput → `forms::TagInput`
|
||||
- [x] StylePresetPicker
|
||||
- [x] SeedInput
|
||||
- [x] AspectRatioPicker
|
||||
|
||||
## T25b Generative · Queue and results
|
||||
|
||||
- [ ] ImageGenerationPanel
|
||||
- [ ] GenerationQueue
|
||||
- [ ] GenerationGrid
|
||||
- [ ] VariationPicker
|
||||
- [ ] PromptEnhancer
|
||||
- [ ] NegativePromptInput
|
||||
- [ ] StylePresetPicker
|
||||
- [ ] SeedInput
|
||||
- [ ] AspectRatioPicker
|
||||
- [ ] ImageCompare → `data_display::BeforeAfter`
|
||||
- [ ] ABCompareView
|
||||
|
||||
## T25c Generative · Canvas
|
||||
|
||||
- [ ] InpaintCanvas
|
||||
- [ ] MaskBrush
|
||||
- [ ] ImageCompare → `data_display::BeforeAfter`
|
||||
|
||||
## T25d Generative · Sound and motion
|
||||
|
||||
- [ ] AudioGenerationPlayer
|
||||
- [ ] VideoGenerationTimeline
|
||||
- [ ] TTSVoicePicker
|
||||
|
||||
## T25e Generative · Models
|
||||
|
||||
- [ ] ModelCard
|
||||
- [ ] ModelDownloadManager
|
||||
- [ ] ModelStatus
|
||||
- [ ] HardwareMonitor
|
||||
- [ ] FineTuneJobCard
|
||||
|
||||
## T25f Generative · Data and prompts
|
||||
|
||||
- [ ] DatasetViewer
|
||||
- [ ] EmbeddingVisualizer — 2D, 3D projection
|
||||
- [ ] PromptPlayground
|
||||
- [ ] PromptDiff / PromptVersionHistory
|
||||
- [ ] ABCompareView
|
||||
|
||||
## T26 Media — `src/media`
|
||||
|
||||
|
||||