Canvas · Tools: single handles, a wrapping palette, the pen ends with its tool

Handles resize one shape, so a selection of several shows its outline
and no handles; a press there no longer falls through to a marquee.
ToggleGroup wraps its faces when narrow, so the ToolPalette stays in a
280px box. A pen path left open ends when the tool changes: two points
or more go to the owner after the frame, a lone point is dropped, and
the open path paints only while the pen is the tool.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 07:41:10 -04:00
1 parent c474a3eb33
commit af93eece28
6 files changed
+139 -23

No files matched your search

+9 -7
View File
@@ -4,14 +4,15 @@ use gpui::{
div, point, px,
};
use super::{Frame, InfiniteCanvas, MiniMap, Viewport, ZoomControls};
use super::{Frame, InfiniteCanvas, MiniMap, Tool, Viewport, ZoomControls};
use crate::{primitives::FocusNext, theme::Theme};
mod tools;
/// A view that shows one canvas part and keeps the viewports and words it heard.
struct Stage {
part: fn(Entity<Stage>) -> AnyElement,
part: fn(&Stage, Entity<Stage>) -> AnyElement,
tool: Tool,
heard: Vec<Viewport>,
said: Vec<String>,
}
@@ -21,7 +22,7 @@ impl Render for Stage {
div()
.w(px(400.0))
.h(px(300.0))
.child((self.part)(cx.entity()))
.child((self.part)(self, cx.entity()))
}
}
@@ -34,7 +35,7 @@ fn settle(cx: &mut VisualTestContext) {
}
fn stage(
part: fn(Entity<Stage>) -> AnyElement,
part: fn(&Stage, Entity<Stage>) -> AnyElement,
cx: &mut TestAppContext,
) -> (Entity<Stage>, &mut VisualTestContext) {
cx.update(|cx| {
@@ -44,6 +45,7 @@ fn stage(
});
let (host, cx) = cx.add_window_view(|_, _| Stage {
part,
tool: Tool::Select,
heard: Vec::new(),
said: Vec::new(),
});
@@ -73,7 +75,7 @@ fn heard(host: &Entity<Stage>, cx: &mut VisualTestContext) -> Vec<Viewport> {
host.read_with(cx, |stage, _| stage.heard.clone())
}
fn plane(owner: Entity<Stage>) -> AnyElement {
fn plane(_: &Stage, owner: Entity<Stage>) -> AnyElement {
InfiniteCanvas::new("plane", Viewport::new(0.0, 0.0, 1.0))
.on_viewport(move |view, _, cx| hear(&owner, view, cx))
.into_any_element()
@@ -122,7 +124,7 @@ fn a_drag_on_empty_space_pans(cx: &mut TestAppContext) {
);
}
fn zoom(owner: Entity<Stage>) -> AnyElement {
fn zoom(_: &Stage, owner: Entity<Stage>) -> AnyElement {
ZoomControls::new("zoom", 1.0)
.on_zoom(move |zoom, _, cx| hear(&owner, Viewport::new(0.0, 0.0, zoom), cx))
.into_any_element()
@@ -146,7 +148,7 @@ fn the_zoom_buttons_step_and_the_percent_resets(cx: &mut TestAppContext) {
assert_eq!(zooms, [0.75, 1.0, 1.5]);
}
fn map(owner: Entity<Stage>) -> AnyElement {
fn map(_: &Stage, owner: Entity<Stage>) -> AnyElement {
MiniMap::new(
"map",
[Frame::new(0.0, 0.0, 400.0, 280.0)],
+81 -7
View File
@@ -1,10 +1,12 @@
use gpui::{
AnyElement, Entity, IntoElement, Modifiers, MouseButton, MouseDownEvent, MouseUpEvent,
TestAppContext, VisualTestContext, point, px,
ParentElement, Styled, TestAppContext, VisualTestContext, div, point, px,
};
use super::{Stage, said, say, settle, stage};
use crate::canvas::{Frame, InfiniteCanvas, Shape, ShapeKind, Tool, ToolLayer, Viewport};
use crate::canvas::{
Frame, InfiniteCanvas, Shape, ShapeKind, Tool, ToolLayer, ToolPalette, Viewport,
};
fn at(x: f32, y: f32) -> gpui::Point<gpui::Pixels> {
point(px(x), px(y))
@@ -91,14 +93,14 @@ fn layer(tool: Tool, selected: &[&'static str], owner: Entity<Stage>) -> AnyElem
#[gpui::test]
fn a_shape_tool_draws_what_the_drag_spans(cx: &mut TestAppContext) {
let (host, cx) = stage(|owner| layer(Tool::Ellipse, &[], owner), cx);
let (host, cx) = stage(|_, owner| layer(Tool::Ellipse, &[], owner), cx);
drag((110.0, 120.0), (190.0, 180.0), cx);
assert_eq!(said(&host, cx), ["draw Ellipse 110 120 80 60"]);
}
#[gpui::test]
fn a_selected_shape_moves_and_snaps_to_its_neighbour(cx: &mut TestAppContext) {
let (host, cx) = stage(|owner| layer(Tool::Select, &["a"], owner), cx);
let (host, cx) = stage(|_, owner| layer(Tool::Select, &["a"], owner), cx);
drag((50.0, 50.0), (147.0, 50.0), cx);
assert_eq!(
said(&host, cx),
@@ -109,7 +111,7 @@ fn a_selected_shape_moves_and_snaps_to_its_neighbour(cx: &mut TestAppContext) {
#[gpui::test]
fn a_drag_on_empty_space_selects_what_it_holds(cx: &mut TestAppContext) {
let (host, cx) = stage(|owner| layer(Tool::Select, &[], owner), cx);
let (host, cx) = stage(|_, owner| layer(Tool::Select, &[], owner), cx);
drag((350.0, 150.0), (150.0, -10.0), cx);
assert_eq!(
said(&host, cx),
@@ -120,17 +122,89 @@ fn a_drag_on_empty_space_selects_what_it_holds(cx: &mut TestAppContext) {
#[gpui::test]
fn a_corner_handle_resizes_the_selection(cx: &mut TestAppContext) {
let (host, cx) = stage(|owner| layer(Tool::Select, &["a"], owner), cx);
let (host, cx) = stage(|_, owner| layer(Tool::Select, &["a"], owner), cx);
drag((100.0, 100.0), (150.0, 120.0), cx);
assert_eq!(said(&host, cx), ["resize a 150 120"]);
}
#[gpui::test]
fn the_pen_adds_a_point_a_press_and_a_double_press_ends_the_path(cx: &mut TestAppContext) {
let (host, cx) = stage(|owner| layer(Tool::Pen, &[], owner), cx);
let (host, cx) = stage(|_, owner| layer(Tool::Pen, &[], owner), cx);
for (x, y) in [(110.0, 150.0), (160.0, 150.0), (160.0, 200.0)] {
press(x, y, 1, cx);
}
press(160.0, 200.0, 2, cx);
assert_eq!(said(&host, cx), ["draw path of 3 110 150 50 50"]);
}
#[gpui::test]
fn a_two_shape_selection_shows_no_handles(cx: &mut TestAppContext) {
let (_, cx) = stage(|_, owner| layer(Tool::Select, &["a", "b"], owner), cx);
assert!(
cx.debug_bounds("handle North").is_none(),
"handles resize one shape"
);
press(150.0, 0.0, 1, cx);
assert!(cx.debug_bounds("handle North").is_none());
}
#[gpui::test]
fn one_selected_shape_shows_its_handles(cx: &mut TestAppContext) {
let (_, cx) = stage(|_, owner| layer(Tool::Select, &["a"], owner), cx);
let north = cx
.debug_bounds("handle North")
.expect("a handle on the top side");
assert_eq!(north.center(), at(50.0, 0.0));
}
fn switch(host: &Entity<Stage>, tool: Tool, cx: &mut VisualTestContext) {
host.update(cx, |stage, cx| {
stage.tool = tool;
cx.notify();
});
settle(cx);
}
#[gpui::test]
fn a_path_left_open_goes_to_the_owner_when_the_tool_changes(cx: &mut TestAppContext) {
let (host, cx) = stage(|stage, owner| layer(stage.tool, &[], owner), cx);
switch(&host, Tool::Pen, cx);
for (x, y) in [(110.0, 150.0), (160.0, 150.0), (160.0, 200.0)] {
press(x, y, 1, cx);
}
switch(&host, Tool::Select, cx);
assert_eq!(said(&host, cx), ["draw path of 3 110 150 50 50"]);
switch(&host, Tool::Pen, cx);
press(120.0, 160.0, 1, cx);
switch(&host, Tool::Hand, cx);
switch(&host, Tool::Pen, cx);
press(130.0, 170.0, 1, cx);
press(130.0, 170.0, 2, cx);
assert_eq!(
said(&host, cx),
["draw path of 3 110 150 50 50"],
"a lone point is dropped, so the next path starts afresh"
);
}
fn palette(_: &Stage, _: Entity<Stage>) -> AnyElement {
div()
.w(px(280.0))
.child(ToolPalette::new("palette", Tool::Select))
.into_any_element()
}
#[gpui::test]
fn the_palette_wraps_inside_a_narrow_box(cx: &mut TestAppContext) {
let (_, cx) = stage(palette, cx);
for tool in Tool::ALL {
let selector = format!("toggle {}", tool.words()).leak();
let face = cx.debug_bounds(selector).expect("a face per tool");
assert!(
face.right() <= px(280.0),
"{} ends at {:?}",
tool.words(),
face.right()
);
}
}