T25c Generative: canvas
InpaintCanvas lays a mask over a picture: brush strokes in shares of the picture, rastered into a 320-column grid where a later stroke wins and an erasing one clears, painted as merged rectangles snapped to device pixels. A ring follows the pointer at the brush's size; [ and ] size the brush and X swaps it when focused. MaskBrush sets paint or erase and the size, with Undo and Clear waiting for a stroke. Pointer strokes move to forms::pen, which SignaturePad and the canvas share.
This commit is contained in:
1 parent
c349a025e5
commit
02f0a8deaf
11 files changed
+853
-57
No files matched your search
@@ -81,6 +81,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- A rounded box does not clip its children. Each layer inside takes the radius itself; `checker` takes one.
|
- A rounded box does not clip its children. Each layer inside takes the radius itself; `checker` takes one.
|
||||||
- A window keeps one `feedback::Toaster` and renders one `ToastViewport`; anything holding the entity pushes to it. Messages share `primitives::Severity`.
|
- A window keeps one `feedback::Toaster` and renders one `ToastViewport`; anything holding the entity pushes to it. Messages share `primitives::Severity`.
|
||||||
- Only gpui can build `ExternalPaths` with paths, so drop rules live in `forms::files::dropped`, where tests reach them.
|
- Only gpui can build `ExternalPaths` with paths, so drop rules live in `forms::files::dropped`, where tests reach them.
|
||||||
|
- A stroke drawn with the pointer goes through `forms::pen`: a press starts it, a drag extends it, and the lift hands it over, in or out of the pad (`SignaturePad`, `generative::InpaintCanvas`).
|
||||||
- gpui runs key bindings before key listeners. A container takes a child's bound key through the action with `capture_action`, as `Form` takes `Submit`.
|
- gpui runs key bindings before key listeners. A container takes a child's bound key through the action with `capture_action`, as `Form` takes `Submit`.
|
||||||
- `ScrollHandle::scroll_to_item` runs in the container's prepaint against last frame's bounds and overflow, so a call before a list's first frame does nothing. Call it from a child's prepaint once the handle has bounds, then request one frame (`navigation::editor_tabs`). A floating list reveals its cursor, and a strip its focused item, through `forms::options::reveal` and `revealer`. A focused Tab stop deeper in a scroll box sets the box's offset whole from its own painted bounds in prepaint, once per focus (`tables::header`). Never poll layout with `request_animation_frame`.
|
- `ScrollHandle::scroll_to_item` runs in the container's prepaint against last frame's bounds and overflow, so a call before a list's first frame does nothing. Call it from a child's prepaint once the handle has bounds, then request one frame (`navigation::editor_tabs`). A floating list reveals its cursor, and a strip its focused item, through `forms::options::reveal` and `revealer`. A focused Tab stop deeper in a scroll box sets the box's offset whole from its own painted bounds in prepaint, once per focus (`tables::header`). Never poll layout with `request_animation_frame`.
|
||||||
- gpui animations run on the wall clock; timers run on the executor's clock, which tests advance by hand. A test that needs an animation settled turns on the theme's reduced motion. Clocks read `background_executor().now()` and wake only their own view.
|
- gpui animations run on the wall clock; timers run on the executor's clock, which tests advance by hand. A test that needs an animation settled turns on the theme's reduced motion. Clocks read `background_executor().now()` and wake only their own view.
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use ely_gpui_component::generative::{InpaintCanvas, MaskBrush, MaskStroke};
|
||||||
|
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
probe::probe,
|
||||||
|
ui::{keep, section, set},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A mask over the olive tree, painted before the demo opens.
|
||||||
|
fn tree() -> Vec<MaskStroke> {
|
||||||
|
vec![MaskStroke {
|
||||||
|
points: vec![
|
||||||
|
(0.07, 0.18),
|
||||||
|
(0.12, 0.34),
|
||||||
|
(0.09, 0.52),
|
||||||
|
(0.13, 0.7),
|
||||||
|
(0.1, 0.86),
|
||||||
|
],
|
||||||
|
radius: 0.07,
|
||||||
|
erase: false,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canvas(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||||
|
let strokes = keep("gen-mask", tree, window, cx);
|
||||||
|
let brush = keep("gen-brush", || (0.05_f32, false), window, cx);
|
||||||
|
let (now, (radius, erase)) = (strokes.read(cx).clone(), *brush.read(cx));
|
||||||
|
let (painted, undone, cleared) = (strokes.clone(), strokes.clone(), strokes);
|
||||||
|
let (keyed, set_brush) = (brush.clone(), brush);
|
||||||
|
let count = now.len();
|
||||||
|
section(
|
||||||
|
"InpaintCanvas / MaskBrush",
|
||||||
|
"A picture to repaint in part: paint a mask where it should change, erase where it went too far; the mask washes over the picture. On the canvas, [ and ] size the brush and X swaps painting for erasing. Undo takes the last stroke, Clear takes them all.",
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(460.))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_3()
|
||||||
|
.child(probe(
|
||||||
|
"gen-canvas",
|
||||||
|
div().w(px(460.)).child(
|
||||||
|
InpaintCanvas::new("gen-canvas", asset!("atrium-olive.jpg"), 1.5, now)
|
||||||
|
.brush(radius, erase)
|
||||||
|
.on_stroke(move |stroke, _, cx| {
|
||||||
|
let mut next = painted.read(cx).clone();
|
||||||
|
next.push(stroke);
|
||||||
|
set(&painted, next, cx)
|
||||||
|
})
|
||||||
|
.on_brush(move |radius, erase, _, cx| set(&keyed, (radius, erase), cx)),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
MaskBrush::new("gen-mask-brush", radius, erase, count)
|
||||||
|
.on_brush(move |radius, erase, _, cx| set(&set_brush, (radius, erase), cx))
|
||||||
|
.on_undo(move |_, cx| {
|
||||||
|
let mut next = undone.read(cx).clone();
|
||||||
|
next.pop();
|
||||||
|
set(&undone, next, cx)
|
||||||
|
})
|
||||||
|
.on_clear(move |_, cx| set(&cleared, Vec::new(), cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod canvas;
|
||||||
mod panel;
|
mod panel;
|
||||||
mod results;
|
mod results;
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ const SCRIPT: &[Step] = &[
|
|||||||
Step::UpAt("gen-ab", 181.0, 147.0),
|
Step::UpAt("gen-ab", 181.0, 147.0),
|
||||||
Step::Wait(300),
|
Step::Wait(300),
|
||||||
Step::Shot("verdict"),
|
Step::Shot("verdict"),
|
||||||
|
Step::DownAt("gen-canvas", 300.0, 70.0),
|
||||||
|
Step::UpAt("gen-canvas", 390.0, 220.0),
|
||||||
|
Step::Wait(300),
|
||||||
|
Step::Shot("masked"),
|
||||||
];
|
];
|
||||||
|
|
||||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||||
@@ -36,5 +41,6 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
|||||||
.child(results::queue(window, cx))
|
.child(results::queue(window, cx))
|
||||||
.child(results::results(window, cx))
|
.child(results::results(window, cx))
|
||||||
.child(results::variations(window, cx))
|
.child(results::variations(window, cx))
|
||||||
|
.child(canvas::canvas(window, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,7 @@ pub use rating::Rating;
|
|||||||
pub use search::SearchInput;
|
pub use search::SearchInput;
|
||||||
pub use select::Select;
|
pub use select::Select;
|
||||||
pub(crate) use select::{Listing, field_button, field_text, listing};
|
pub(crate) use select::{Listing, field_button, field_text, listing};
|
||||||
|
pub(crate) use signature::{Drawing, pen};
|
||||||
pub use signature::{SignaturePad, Stroke};
|
pub use signature::{SignaturePad, Stroke};
|
||||||
pub use slider::{RangeSlider, Slider};
|
pub use slider::{RangeSlider, Slider};
|
||||||
pub use stepper::Stepper;
|
pub use stepper::Stepper;
|
||||||
|
|||||||
+73
-55
@@ -1,9 +1,10 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, Corners, DragMoveEvent, ElementId, EmptyView, EntityId, Hsla, InteractiveElement,
|
App, Bounds, Corners, Div, DragMoveEvent, ElementId, EmptyView, Entity, EntityId, Hsla,
|
||||||
IntoElement, MouseButton, ParentElement, Path, PathBuilder, Pixels, Point, RenderOnce,
|
InteractiveElement, IntoElement, MouseButton, ParentElement, Path, PathBuilder, Pixels, Point,
|
||||||
StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, prelude::*, size,
|
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window, canvas, div, fill, point,
|
||||||
|
prelude::*, size,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -16,17 +17,17 @@ use crate::{
|
|||||||
pub type Stroke = Vec<Point<Pixels>>;
|
pub type Stroke = Vec<Point<Pixels>>;
|
||||||
|
|
||||||
type OnStrokes = Rc<dyn Fn(&[Stroke], &mut Window, &mut App)>;
|
type OnStrokes = Rc<dyn Fn(&[Stroke], &mut Window, &mut App)>;
|
||||||
type Lift = Rc<dyn Fn(Point<Pixels>, &mut Window, &mut App)>;
|
type Lift = Rc<dyn Fn(Stroke, &mut Window, &mut App)>;
|
||||||
|
|
||||||
struct Pen {
|
struct Pen {
|
||||||
owner: EntityId,
|
owner: EntityId,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where the pad sits, and the stroke under the pen.
|
/// Where a pad sits, and the stroke under the pen.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Drawing {
|
pub(crate) struct Drawing {
|
||||||
bounds: Bounds<Pixels>,
|
pub bounds: Bounds<Pixels>,
|
||||||
stroke: Stroke,
|
pub stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drawing {
|
impl Drawing {
|
||||||
@@ -67,6 +68,63 @@ fn ink(points: &[Point<Pixels>], origin: Point<Pixels>, width: Pixels) -> Option
|
|||||||
Some(path.build().expect("a stroke is a simple open path"))
|
Some(path.build().expect("a stroke is a simple open path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turns a press and drag on `pad` into a stroke of points from its corner, kept inside it; `lift` gets the stroke as the pen leaves. The pad's owner keeps `drawing`'s bounds.
|
||||||
|
pub(crate) fn pen(
|
||||||
|
pad: Stateful<Div>,
|
||||||
|
drawing: &Entity<Drawing>,
|
||||||
|
lift: impl Fn(Stroke, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Stateful<Div> {
|
||||||
|
let owner = drawing.entity_id();
|
||||||
|
let lift: Lift = Rc::new(lift);
|
||||||
|
let finish = {
|
||||||
|
let drawing = drawing.clone();
|
||||||
|
move |at: Point<Pixels>, window: &mut Window, cx: &mut App| {
|
||||||
|
let stroke = drawing.update(cx, |drawing, cx| {
|
||||||
|
if !drawing.stroke.is_empty() {
|
||||||
|
drawing.reach(at);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
std::mem::take(&mut drawing.stroke)
|
||||||
|
});
|
||||||
|
if !stroke.is_empty() {
|
||||||
|
lift(stroke, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (press, start, moving) = (drawing.clone(), drawing.clone(), drawing.clone());
|
||||||
|
let up = Rc::new(finish);
|
||||||
|
let leave = up.clone();
|
||||||
|
pad.on_mouse_down(MouseButton::Left, move |event, _, cx| {
|
||||||
|
press.update(cx, |drawing, cx| {
|
||||||
|
drawing.stroke = vec![local(drawing.bounds, event.position)];
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_drag(Pen { owner }, move |_, _, window, cx| {
|
||||||
|
let at = window.mouse_position();
|
||||||
|
start.update(cx, |drawing, cx| {
|
||||||
|
drawing.reach(at);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
cx.new(|_| EmptyView)
|
||||||
|
})
|
||||||
|
.on_drag_move(move |event: &DragMoveEvent<Pen>, _, cx| {
|
||||||
|
if event.drag(cx).owner != owner {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moving.update(cx, |drawing, cx| {
|
||||||
|
drawing.reach(event.event.position);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_mouse_up(MouseButton::Left, move |event, window, cx| {
|
||||||
|
up(event.position, window, cx)
|
||||||
|
})
|
||||||
|
.on_mouse_up_out(MouseButton::Left, move |event, window, cx| {
|
||||||
|
leave(event.position, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
strokes: &[Stroke],
|
strokes: &[Stroke],
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
@@ -120,7 +178,6 @@ impl RenderOnce for SignaturePad {
|
|||||||
let strokes = use_seeded((self.id.clone(), "strokes"), self.strokes, window, cx);
|
let strokes = use_seeded((self.id.clone(), "strokes"), self.strokes, window, cx);
|
||||||
let drawing =
|
let drawing =
|
||||||
window.use_keyed_state((self.id.clone(), "drawing"), cx, |_, _| Drawing::default());
|
window.use_keyed_state((self.id.clone(), "drawing"), cx, |_, _| Drawing::default());
|
||||||
let owner = drawing.entity_id();
|
|
||||||
let set: OnStrokes = {
|
let set: OnStrokes = {
|
||||||
let (id, strokes, on_change) = (self.id.clone(), strokes.clone(), self.on_change);
|
let (id, strokes, on_change) = (self.id.clone(), strokes.clone(), self.on_change);
|
||||||
Rc::new(move |next, window, cx| {
|
Rc::new(move |next, window, cx| {
|
||||||
@@ -134,22 +191,13 @@ impl RenderOnce for SignaturePad {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let lift: Lift = {
|
let lift = {
|
||||||
let (strokes, drawing, set) = (strokes.clone(), drawing.clone(), set.clone());
|
let (strokes, set) = (strokes.clone(), set.clone());
|
||||||
Rc::new(move |at, window, cx| {
|
move |stroke: Stroke, window: &mut Window, cx: &mut App| {
|
||||||
let stroke = drawing.update(cx, |drawing, _| {
|
|
||||||
if !drawing.stroke.is_empty() {
|
|
||||||
drawing.reach(at);
|
|
||||||
}
|
|
||||||
std::mem::take(&mut drawing.stroke)
|
|
||||||
});
|
|
||||||
if stroke.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut next = strokes.read(cx).value.clone();
|
let mut next = strokes.read(cx).value.clone();
|
||||||
next.push(stroke);
|
next.push(stroke);
|
||||||
set(&next, window, cx);
|
set(&next, window, cx);
|
||||||
})
|
}
|
||||||
};
|
};
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let colors = &theme.colors;
|
let colors = &theme.colors;
|
||||||
@@ -158,9 +206,7 @@ impl RenderOnce for SignaturePad {
|
|||||||
let empty = strokes.read(cx).value.is_empty();
|
let empty = strokes.read(cx).value.is_empty();
|
||||||
let mut shown = strokes.read(cx).value.clone();
|
let mut shown = strokes.read(cx).value.clone();
|
||||||
shown.push(drawing.read(cx).stroke.clone());
|
shown.push(drawing.read(cx).stroke.clone());
|
||||||
let (measure, press, start, pen) =
|
let measure = drawing.clone();
|
||||||
(drawing.clone(), drawing.clone(), drawing.clone(), drawing);
|
|
||||||
let (up, out) = (lift.clone(), lift);
|
|
||||||
let color = colors.fg;
|
let color = colors.fg;
|
||||||
let pad = div()
|
let pad = div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
@@ -203,36 +249,8 @@ impl RenderOnce for SignaturePad {
|
|||||||
.top_0()
|
.top_0()
|
||||||
.left_0()
|
.left_0()
|
||||||
.size_full(),
|
.size_full(),
|
||||||
)
|
);
|
||||||
.on_mouse_down(MouseButton::Left, move |event, _, cx| {
|
let pad = pen(pad, &drawing, lift);
|
||||||
press.update(cx, |drawing, cx| {
|
|
||||||
drawing.stroke = vec![local(drawing.bounds, event.position)];
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_drag(Pen { owner }, move |_, _, window, cx| {
|
|
||||||
let at = window.mouse_position();
|
|
||||||
start.update(cx, |drawing, cx| {
|
|
||||||
drawing.reach(at);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
cx.new(|_| EmptyView)
|
|
||||||
})
|
|
||||||
.on_drag_move(move |event: &DragMoveEvent<Pen>, _, cx| {
|
|
||||||
if event.drag(cx).owner != owner {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pen.update(cx, |drawing, cx| {
|
|
||||||
drawing.reach(event.event.position);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_mouse_up(MouseButton::Left, move |event, window, cx| {
|
|
||||||
up(event.position, window, cx)
|
|
||||||
})
|
|
||||||
.on_mouse_up_out(MouseButton::Left, move |event, window, cx| {
|
|
||||||
out(event.position, window, cx)
|
|
||||||
});
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{App, ElementId, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
|
||||||
|
|
||||||
|
use super::mask::{OnBrush, REACH};
|
||||||
|
use crate::{
|
||||||
|
buttons::{IconButton, SegmentedControl},
|
||||||
|
forms::{Run, Slider},
|
||||||
|
primitives::IconName,
|
||||||
|
theme::{ActiveTheme, ControlSize, TextSize},
|
||||||
|
typography::{format::percent, tabular},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The mask brush: paint or erase, its size, and a way back: Undo takes the last stroke, Clear takes them all.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct MaskBrush {
|
||||||
|
id: ElementId,
|
||||||
|
radius: f32,
|
||||||
|
erase: bool,
|
||||||
|
strokes: usize,
|
||||||
|
on_brush: Option<OnBrush>,
|
||||||
|
on_undo: Option<Run>,
|
||||||
|
on_clear: Option<Run>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaskBrush {
|
||||||
|
/// `radius` is a share of the picture's width; `strokes` counts the mask's strokes, which Undo and Clear wait for.
|
||||||
|
pub fn new(id: impl Into<ElementId>, radius: f32, erase: bool, strokes: usize) -> Self {
|
||||||
|
assert!(
|
||||||
|
(REACH.0..=REACH.1).contains(&radius),
|
||||||
|
"a brush radius of {radius}"
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
radius,
|
||||||
|
erase,
|
||||||
|
strokes,
|
||||||
|
on_brush: None,
|
||||||
|
on_undo: None,
|
||||||
|
on_clear: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the brush's radius and whether it erases.
|
||||||
|
pub fn on_brush(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(f32, bool, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_brush = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_undo(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_undo = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_clear(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||||
|
self.on_clear = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for MaskBrush {
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let colors = theme.colors.clone();
|
||||||
|
let (radius, erase, empty) = (self.radius, self.erase, self.strokes == 0);
|
||||||
|
let (mode, size) = (self.on_brush.clone(), self.on_brush);
|
||||||
|
let back = |key: &'static str, icon: IconName, tip: &'static str, run: Option<Run>| {
|
||||||
|
run.map(|run| {
|
||||||
|
IconButton::new((self.id.clone(), key), icon)
|
||||||
|
.size(ControlSize::Sm)
|
||||||
|
.tooltip(tip)
|
||||||
|
.disabled(empty)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
log::info!("mask brush: {key}");
|
||||||
|
run(window, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
SegmentedControl::new(
|
||||||
|
(self.id.clone(), "mode"),
|
||||||
|
if erase { "erase" } else { "paint" },
|
||||||
|
)
|
||||||
|
.segment("paint", "Paint", Some(IconName::Brush))
|
||||||
|
.segment("erase", "Erase", Some(IconName::Eraser))
|
||||||
|
.size(ControlSize::Sm)
|
||||||
|
.on_change(move |value, window, cx| {
|
||||||
|
if let Some(mode) = &mode {
|
||||||
|
mode(radius, value == "erase", window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.gap_1()
|
||||||
|
.children(back(
|
||||||
|
"undo",
|
||||||
|
IconName::Undo2,
|
||||||
|
"Undo the last stroke",
|
||||||
|
self.on_undo,
|
||||||
|
))
|
||||||
|
.children(back(
|
||||||
|
"clear",
|
||||||
|
IconName::Trash2,
|
||||||
|
"Clear the mask",
|
||||||
|
self.on_clear,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.text_size(theme.text_size(TextSize::Sm))
|
||||||
|
.child(div().flex_none().text_color(colors.fg_muted).child("Size"))
|
||||||
|
.child(
|
||||||
|
div().flex_1().min_w_0().child(
|
||||||
|
Slider::new((self.id.clone(), "size"), f64::from(radius))
|
||||||
|
.range(f64::from(REACH.0), f64::from(REACH.1))
|
||||||
|
.step(0.005)
|
||||||
|
.on_change(move |value, window, cx| {
|
||||||
|
if let Some(size) = &size {
|
||||||
|
size(value as f32, erase, window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
tabular(div())
|
||||||
|
.flex_none()
|
||||||
|
.text_color(colors.fg_muted)
|
||||||
|
.child(percent(f64::from(radius), 1, false)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
App, Bounds, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, Pixels, Point,
|
||||||
|
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, canvas, div, fill, point,
|
||||||
|
size, transparent_black,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
documents::source,
|
||||||
|
forms::{Drawing, pen},
|
||||||
|
primitives::{FocusRing, Image, checked_ratio, framed, tab_stop},
|
||||||
|
theme::{ActiveTheme, Radius},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Cells across a mask; rows follow the picture's shape.
|
||||||
|
const COLUMNS: usize = 320;
|
||||||
|
|
||||||
|
/// The brush's smallest and largest radius, as shares of the picture's width.
|
||||||
|
pub(crate) const REACH: (f32, f32) = (0.01, 0.2);
|
||||||
|
|
||||||
|
/// A stroke of the mask brush: points as shares of the picture across and down, a radius as a share of its width, and whether it erases.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct MaskStroke {
|
||||||
|
pub points: Vec<(f32, f32)>,
|
||||||
|
pub radius: f32,
|
||||||
|
pub erase: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The squared distance from `p` to the segment from `a` to `b`.
|
||||||
|
fn reach(p: (f32, f32), a: (f32, f32), b: (f32, f32)) -> f32 {
|
||||||
|
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
|
||||||
|
let length = dx * dx + dy * dy;
|
||||||
|
let t = match length {
|
||||||
|
0.0 => 0.0,
|
||||||
|
_ => (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / length).clamp(0.0, 1.0),
|
||||||
|
};
|
||||||
|
let (x, y) = (a.0 + t * dx - p.0, a.1 + t * dy - p.1);
|
||||||
|
x * x + y * y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cells of a `columns` by `rows` grid over a picture `ratio` wide over tall that the strokes cover, row by row: a later stroke wins, and an erasing one clears.
|
||||||
|
pub(crate) fn raster(strokes: &[MaskStroke], columns: usize, rows: usize, ratio: f32) -> Vec<bool> {
|
||||||
|
let mut cells = vec![false; columns * rows];
|
||||||
|
let column = |x: f32| ((x * columns as f32).floor().max(0.0) as usize).min(columns);
|
||||||
|
let row = |y: f32| ((y * ratio * rows as f32).floor().max(0.0) as usize).min(rows);
|
||||||
|
for stroke in strokes {
|
||||||
|
let across: Vec<(f32, f32)> = stroke.points.iter().map(|&(x, y)| (x, y / ratio)).collect();
|
||||||
|
let pairs: Vec<_> = match across.as_slice() {
|
||||||
|
[] => continue,
|
||||||
|
[only] => vec![(*only, *only)],
|
||||||
|
_ => across.windows(2).map(|pair| (pair[0], pair[1])).collect(),
|
||||||
|
};
|
||||||
|
let (r, near) = (stroke.radius, stroke.radius * stroke.radius);
|
||||||
|
for (a, b) in pairs {
|
||||||
|
let columns_in = column(a.0.min(b.0) - r)..(column(a.0.max(b.0) + r) + 1).min(columns);
|
||||||
|
let rows_in = row(a.1.min(b.1) - r)..(row(a.1.max(b.1) + r) + 1).min(rows);
|
||||||
|
for j in rows_in {
|
||||||
|
for i in columns_in.clone() {
|
||||||
|
let center = (
|
||||||
|
(i as f32 + 0.5) / columns as f32,
|
||||||
|
(j as f32 + 0.5) / rows as f32 / ratio,
|
||||||
|
);
|
||||||
|
if reach(center, a, b) <= near {
|
||||||
|
cells[j * columns + i] = !stroke.erase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Covered cells as rectangles: runs in a row, merged down while the rows below run the same. Each is first row, row past the last, first column, column past the last.
|
||||||
|
pub(crate) fn spans(cells: &[bool], columns: usize) -> Vec<(usize, usize, usize, usize)> {
|
||||||
|
let mut open: Vec<(usize, usize, usize, usize)> = Vec::new();
|
||||||
|
let mut done = Vec::new();
|
||||||
|
for (j, line) in cells.chunks(columns).enumerate() {
|
||||||
|
let mut runs = Vec::new();
|
||||||
|
let mut start = None;
|
||||||
|
for (i, &on) in line.iter().chain(std::iter::once(&false)).enumerate() {
|
||||||
|
match (on, start) {
|
||||||
|
(true, None) => start = Some(i),
|
||||||
|
(false, Some(from)) => {
|
||||||
|
runs.push((from, i));
|
||||||
|
start = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut next = Vec::new();
|
||||||
|
for rect in open.drain(..) {
|
||||||
|
match runs.iter().position(|&run| run == (rect.2, rect.3)) {
|
||||||
|
Some(at) => {
|
||||||
|
runs.remove(at);
|
||||||
|
next.push((rect.0, j + 1, rect.2, rect.3));
|
||||||
|
}
|
||||||
|
None => done.push(rect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.extend(runs.into_iter().map(|(from, to)| (j, j + 1, from, to)));
|
||||||
|
open = next;
|
||||||
|
}
|
||||||
|
done.extend(open);
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnStroke = Rc<dyn Fn(MaskStroke, &mut Window, &mut App)>;
|
||||||
|
pub(crate) type OnBrush = Rc<dyn Fn(f32, bool, &mut Window, &mut App)>;
|
||||||
|
|
||||||
|
/// A picture to repaint in part: the brush masks where it passes and an erasing brush clears, the mask a wash over the picture. A ring follows the pointer at the brush's size. Focused, [ and ] size the brush and X swaps painting for erasing.
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct InpaintCanvas {
|
||||||
|
id: ElementId,
|
||||||
|
picture: SharedString,
|
||||||
|
ratio: f32,
|
||||||
|
strokes: Vec<MaskStroke>,
|
||||||
|
radius: f32,
|
||||||
|
erase: bool,
|
||||||
|
on_stroke: Option<OnStroke>,
|
||||||
|
on_brush: Option<OnBrush>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InpaintCanvas {
|
||||||
|
/// `ratio` is the picture's width over height; `strokes` are the mask so far, oldest first.
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
picture: impl Into<SharedString>,
|
||||||
|
ratio: f32,
|
||||||
|
strokes: impl IntoIterator<Item = MaskStroke>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
picture: picture.into(),
|
||||||
|
ratio: checked_ratio(ratio),
|
||||||
|
strokes: strokes.into_iter().collect(),
|
||||||
|
radius: 0.04,
|
||||||
|
erase: false,
|
||||||
|
on_stroke: None,
|
||||||
|
on_brush: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The brush's radius as a share of the picture's width, and whether it erases.
|
||||||
|
pub fn brush(mut self, radius: f32, erase: bool) -> Self {
|
||||||
|
assert!(
|
||||||
|
(REACH.0..=REACH.1).contains(&radius),
|
||||||
|
"a brush radius of {radius}"
|
||||||
|
);
|
||||||
|
self.radius = radius;
|
||||||
|
self.erase = erase;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets each stroke as the pointer lifts.
|
||||||
|
pub fn on_stroke(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(MaskStroke, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_stroke = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the brush the keys set: its radius and whether it erases.
|
||||||
|
pub fn on_brush(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(f32, bool, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_brush = Some(Rc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The strokes a mask last drew, its rows, and the rectangles they made.
|
||||||
|
type Shape = (
|
||||||
|
Vec<MaskStroke>,
|
||||||
|
usize,
|
||||||
|
Rc<Vec<(usize, usize, usize, usize)>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The mask's rectangles, made again only when its strokes or rows change, not as the pointer moves.
|
||||||
|
fn shaped(
|
||||||
|
cache: &Entity<Shape>,
|
||||||
|
live: Vec<MaskStroke>,
|
||||||
|
rows: usize,
|
||||||
|
ratio: f32,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Rc<Vec<(usize, usize, usize, usize)>> {
|
||||||
|
let (kept, kept_rows, rects) = cache.read(cx);
|
||||||
|
if *kept == live && *kept_rows == rows {
|
||||||
|
return rects.clone();
|
||||||
|
}
|
||||||
|
let rects = Rc::new(spans(&raster(&live, COLUMNS, rows, ratio), COLUMNS));
|
||||||
|
cache.update(cx, |cache, _| *cache = (live, rows, rects.clone()));
|
||||||
|
rects
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `at` as shares of `bounds`.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for InpaintCanvas {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let drawing =
|
||||||
|
window.use_keyed_state((self.id.clone(), "drawing"), cx, |_, _| Drawing::default());
|
||||||
|
let pointer =
|
||||||
|
window.use_keyed_state(
|
||||||
|
(self.id.clone(), "pointer"),
|
||||||
|
cx,
|
||||||
|
|_, _| None::<Point<Pixels>>,
|
||||||
|
);
|
||||||
|
let cache: Entity<Shape> =
|
||||||
|
window.use_keyed_state((self.id.clone(), "shape"), cx, |_, _| {
|
||||||
|
(Vec::new(), 0, Rc::new(Vec::new()))
|
||||||
|
});
|
||||||
|
let focus = tab_stop((self.id.clone(), "focus").into(), true, window, cx);
|
||||||
|
let (bounds, under) = (drawing.read(cx).bounds, drawing.read(cx).stroke.clone());
|
||||||
|
let (radius, erase) = (self.radius, self.erase);
|
||||||
|
let mut live = self.strokes;
|
||||||
|
if !under.is_empty() {
|
||||||
|
let points = under.iter().map(|at| share(bounds, *at)).collect();
|
||||||
|
live.push(MaskStroke {
|
||||||
|
points,
|
||||||
|
radius,
|
||||||
|
erase,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let rows = ((COLUMNS as f32 / self.ratio).round() as usize).max(1);
|
||||||
|
let rects = shaped(&cache, live, rows, self.ratio, cx);
|
||||||
|
let theme = cx.theme();
|
||||||
|
let colors = theme.colors.clone();
|
||||||
|
let (wash, corner) = (colors.accent.alpha(0.45), theme.radius(Radius::Lg));
|
||||||
|
let ring = pointer
|
||||||
|
.read(cx)
|
||||||
|
.filter(|_| bounds.size.width > Pixels::ZERO)
|
||||||
|
.map(|at| {
|
||||||
|
let span = bounds.size.width * radius * 2.0;
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(at.x - bounds.left() - span / 2.0)
|
||||||
|
.top(at.y - bounds.top() - span / 2.0)
|
||||||
|
.size(span)
|
||||||
|
.rounded_full()
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.on_media)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.rounded_full()
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.backdrop),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let (measure, hover, leave, keys) = (
|
||||||
|
drawing.clone(),
|
||||||
|
pointer.clone(),
|
||||||
|
pointer,
|
||||||
|
self.on_brush.clone(),
|
||||||
|
);
|
||||||
|
let (id, on_stroke) = (self.id.clone(), self.on_stroke.clone());
|
||||||
|
let area = framed(self.ratio, cx)
|
||||||
|
.id(self.id.clone())
|
||||||
|
.track_focus(&focus)
|
||||||
|
.rounded(corner)
|
||||||
|
.border_1()
|
||||||
|
.border_color(transparent_black())
|
||||||
|
.focus_ring(cx)
|
||||||
|
.cursor_crosshair()
|
||||||
|
.child(
|
||||||
|
Image::new((self.id.clone(), "picture"), source(&self.picture))
|
||||||
|
.size_full()
|
||||||
|
.rounded(corner),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
canvas(
|
||||||
|
move |bounds, _, cx| {
|
||||||
|
if measure.read(cx).bounds != bounds {
|
||||||
|
measure.update(cx, |drawing, _| drawing.bounds = bounds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |bounds, _, window, _| {
|
||||||
|
let scale = window.scale_factor();
|
||||||
|
let snap = |at: f32| Pixels::from((at * scale).round() / scale);
|
||||||
|
let (width, height) =
|
||||||
|
(f32::from(bounds.size.width), f32::from(bounds.size.height));
|
||||||
|
let (left, top) = (f32::from(bounds.left()), f32::from(bounds.top()));
|
||||||
|
for (first, past, from, to) in rects.iter() {
|
||||||
|
let x0 = snap(left + width * *from as f32 / COLUMNS as f32);
|
||||||
|
let x1 = snap(left + width * *to as f32 / COLUMNS as f32);
|
||||||
|
let y0 = snap(top + height * *first as f32 / rows as f32);
|
||||||
|
let y1 = snap(top + height * *past as f32 / rows as f32);
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds::new(point(x0, y0), size(x1 - x0, y1 - y0)),
|
||||||
|
wash,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.children(ring)
|
||||||
|
.on_mouse_move(move |event, _, cx| {
|
||||||
|
hover.update(cx, |pointer, cx| {
|
||||||
|
*pointer = Some(event.position);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_hover(move |hovered, _, cx| {
|
||||||
|
if !hovered {
|
||||||
|
leave.update(cx, |pointer, cx| {
|
||||||
|
*pointer = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_key_down(move |event, window, cx| {
|
||||||
|
let Some(keys) = &keys else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (next, swap) = match event.keystroke.key.as_str() {
|
||||||
|
"[" => ((radius * 0.8).max(REACH.0), erase),
|
||||||
|
"]" => ((radius * 1.25).min(REACH.1), erase),
|
||||||
|
"x" => (radius, !erase),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
cx.stop_propagation();
|
||||||
|
log::info!("inpaint canvas: brush {next:.3}, erase {swap}");
|
||||||
|
keys(next, swap, window, cx)
|
||||||
|
});
|
||||||
|
let sized = drawing.clone();
|
||||||
|
pen(area, &drawing, move |stroke, window, cx| {
|
||||||
|
let Some(on_stroke) = &on_stroke else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let bounds = sized.read(cx).bounds;
|
||||||
|
let points = stroke.iter().map(|at| share(bounds, *at)).collect();
|
||||||
|
log::info!("inpaint canvas {id:?}: a stroke of {} points", stroke.len());
|
||||||
|
on_stroke(
|
||||||
|
MaskStroke {
|
||||||
|
points,
|
||||||
|
radius,
|
||||||
|
erase,
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{MaskStroke, raster, spans};
|
||||||
|
|
||||||
|
fn stroke(points: &[(f32, f32)], radius: f32, erase: bool) -> MaskStroke {
|
||||||
|
MaskStroke {
|
||||||
|
points: points.to_vec(),
|
||||||
|
radius,
|
||||||
|
erase,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count(cells: &[bool]) -> usize {
|
||||||
|
cells.iter().filter(|on| **on).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_dab_covers_a_disc_as_wide_as_tall() {
|
||||||
|
let cells = raster(&[stroke(&[(0.5, 0.5)], 0.1, false)], 20, 10, 2.0);
|
||||||
|
assert_eq!(count(&cells), 12, "two cells of reach on square cells");
|
||||||
|
assert!(
|
||||||
|
cells[3 * 20 + 9] && cells[4 * 20 + 8],
|
||||||
|
"as far down as across"
|
||||||
|
);
|
||||||
|
assert!(!cells[3 * 20 + 8], "the disc's corner stays out");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_later_stroke_wins_and_an_erasing_one_clears() {
|
||||||
|
let paint = stroke(&[(0.5, 0.5)], 0.1, false);
|
||||||
|
let erase = stroke(&[(0.5, 0.5)], 0.05, true);
|
||||||
|
assert_eq!(
|
||||||
|
count(&raster(&[paint.clone(), erase.clone()], 20, 10, 2.0)),
|
||||||
|
8
|
||||||
|
);
|
||||||
|
assert_eq!(count(&raster(&[erase, paint], 20, 10, 2.0)), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_line_covers_a_band_with_round_ends() {
|
||||||
|
let cells = raster(
|
||||||
|
&[stroke(&[(0.2, 0.5), (0.8, 0.5)], 0.05, false)],
|
||||||
|
20,
|
||||||
|
10,
|
||||||
|
2.0,
|
||||||
|
);
|
||||||
|
assert_eq!(count(&cells), 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runs_that_match_below_merge_into_one_rectangle() {
|
||||||
|
let cells = [
|
||||||
|
false, true, true, false, //
|
||||||
|
false, true, true, false, //
|
||||||
|
true, true, false, false,
|
||||||
|
];
|
||||||
|
assert_eq!(spans(&cells, 4), [(0, 2, 1, 3), (2, 3, 0, 2)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
mod brush;
|
||||||
mod compare;
|
mod compare;
|
||||||
mod grid;
|
mod grid;
|
||||||
|
mod mask;
|
||||||
mod prompt;
|
mod prompt;
|
||||||
mod queue;
|
mod queue;
|
||||||
mod ratio;
|
mod ratio;
|
||||||
@@ -9,8 +11,10 @@ mod style;
|
|||||||
mod tests;
|
mod tests;
|
||||||
mod variation;
|
mod variation;
|
||||||
|
|
||||||
|
pub use brush::MaskBrush;
|
||||||
pub use compare::{ABCompareView, Verdict};
|
pub use compare::{ABCompareView, Verdict};
|
||||||
pub use grid::{GenerationGrid, Outcome};
|
pub use grid::{GenerationGrid, Outcome};
|
||||||
|
pub use mask::{InpaintCanvas, MaskStroke};
|
||||||
pub use prompt::PromptEnhancer;
|
pub use prompt::PromptEnhancer;
|
||||||
pub use queue::{GenerationQueue, Job, JobState};
|
pub use queue::{GenerationQueue, Job, JobState};
|
||||||
pub use ratio::AspectRatioPicker;
|
pub use ratio::AspectRatioPicker;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use gpui::{
|
||||||
|
Context, Entity, IntoElement, Modifiers, MouseButton, ParentElement, Render, Styled,
|
||||||
|
TestAppContext, VisualTestContext, Window, div, point, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{press, settle, setup, tab};
|
||||||
|
use crate::generative::{InpaintCanvas, MaskBrush, MaskStroke};
|
||||||
|
|
||||||
|
/// A canvas 400 wide over a picture twice as wide as tall, its brush and brush panel, and the strokes and brushes heard.
|
||||||
|
struct Painting {
|
||||||
|
strokes: Vec<MaskStroke>,
|
||||||
|
brush: (f32, bool),
|
||||||
|
undone: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Painting {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let (stroked, keyed, set, undo) = (cx.entity(), cx.entity(), cx.entity(), cx.entity());
|
||||||
|
let (radius, erase) = self.brush;
|
||||||
|
div()
|
||||||
|
.w(px(400.0))
|
||||||
|
.child(
|
||||||
|
InpaintCanvas::new("canvas", "missing.jpg", 2.0, self.strokes.clone())
|
||||||
|
.brush(radius, erase)
|
||||||
|
.on_stroke(move |stroke, _, cx| {
|
||||||
|
stroked.update(cx, |view, cx| {
|
||||||
|
view.strokes.push(stroke);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_brush(move |radius, erase, _, cx| {
|
||||||
|
keyed.update(cx, |view, cx| {
|
||||||
|
view.brush = (radius, erase);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
MaskBrush::new("brush", radius, erase, self.strokes.len())
|
||||||
|
.on_brush(move |radius, erase, _, cx| {
|
||||||
|
set.update(cx, |view, cx| {
|
||||||
|
view.brush = (radius, erase);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_undo(move |_, cx| {
|
||||||
|
undo.update(cx, |view, cx| {
|
||||||
|
view.strokes.pop();
|
||||||
|
view.undone += 1;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn painting(cx: &mut TestAppContext) -> (Entity<Painting>, &mut VisualTestContext) {
|
||||||
|
setup(cx);
|
||||||
|
let (view, cx) = cx.add_window_view(|_, _| Painting {
|
||||||
|
strokes: Vec::new(),
|
||||||
|
brush: (0.04, false),
|
||||||
|
undone: 0,
|
||||||
|
});
|
||||||
|
cx.update(|window, _| window.activate_window());
|
||||||
|
settle(cx);
|
||||||
|
(view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_drag_lands_a_stroke_in_shares_of_the_picture(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx) = painting(cx);
|
||||||
|
let none = Modifiers::none();
|
||||||
|
cx.simulate_mouse_down(point(px(100.0), px(50.0)), MouseButton::Left, none);
|
||||||
|
for x in [150.0, 200.0, 250.0] {
|
||||||
|
cx.simulate_mouse_move(point(px(x), px(50.0)), MouseButton::Left, none);
|
||||||
|
}
|
||||||
|
cx.simulate_mouse_up(point(px(300.0), px(50.0)), MouseButton::Left, none);
|
||||||
|
settle(cx);
|
||||||
|
let strokes = view.read_with(cx, |view, _| view.strokes.clone());
|
||||||
|
assert_eq!(strokes.len(), 1, "{strokes:?}");
|
||||||
|
let (first, last) = (
|
||||||
|
strokes[0].points[0],
|
||||||
|
*strokes[0].points.last().expect("points"),
|
||||||
|
);
|
||||||
|
let near = |a: (f32, f32), b: (f32, f32)| (a.0 - b.0).abs() < 0.01 && (a.1 - b.1).abs() < 0.01;
|
||||||
|
assert!(
|
||||||
|
near(first, (0.25, 0.25)) && near(last, (0.75, 0.25)),
|
||||||
|
"{first:?} to {last:?}"
|
||||||
|
);
|
||||||
|
assert_eq!((strokes[0].radius, strokes[0].erase), (0.04, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn keys_on_the_canvas_size_the_brush_and_swap_to_erasing(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx) = painting(cx);
|
||||||
|
tab(1, cx);
|
||||||
|
press("]", cx);
|
||||||
|
press("x", cx);
|
||||||
|
let brush = view.read_with(cx, |view, _| view.brush);
|
||||||
|
assert!((brush.0 - 0.05).abs() < 1e-6 && brush.1, "{brush:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn undo_waits_for_a_stroke(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx) = painting(cx);
|
||||||
|
tab(4, cx);
|
||||||
|
press("enter", cx);
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| view.undone),
|
||||||
|
0,
|
||||||
|
"nothing to undo yet"
|
||||||
|
);
|
||||||
|
view.update(cx, |view, cx| {
|
||||||
|
view.strokes.push(MaskStroke {
|
||||||
|
points: vec![(0.5, 0.5)],
|
||||||
|
radius: 0.04,
|
||||||
|
erase: false,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
cx.update(|window, _| window.blur());
|
||||||
|
tab(4, cx);
|
||||||
|
press("enter", cx);
|
||||||
|
assert_eq!(
|
||||||
|
view.read_with(cx, |view, _| (view.undone, view.strokes.len())),
|
||||||
|
(1, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use gpui::{KeyBinding, KeyUpEvent, Keystroke, TestAppContext, VisualTestContext}
|
|||||||
|
|
||||||
use crate::{forms, primitives::FocusNext, theme::Theme};
|
use crate::{forms, primitives::FocusNext, theme::Theme};
|
||||||
|
|
||||||
|
mod canvas;
|
||||||
mod results;
|
mod results;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -227,8 +227,8 @@
|
|||||||
|
|
||||||
## T25c Generative · Canvas
|
## T25c Generative · Canvas
|
||||||
|
|
||||||
- [ ] InpaintCanvas
|
- [x] InpaintCanvas
|
||||||
- [ ] MaskBrush
|
- [x] MaskBrush
|
||||||
|
|
||||||
## T25d Generative · Sound and motion
|
## T25d Generative · Sound and motion
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user