Canvas · Tools: select, move and snap, resize, draw, pen, text, brush
ToolLayer lies over an InfiniteCanvas and carries its tools. Select takes the topmost shape a press reaches, Shift adds or drops one, a drag moves the selection and snaps its nearest edge or middle to another shape's within reach, a handle resizes it, Shift keeping a corner's shape, and a drag on empty space draws a SelectionBox that takes what lies wholly inside. The shape tools draw by a drag toward any corner, Shift keeping them square; the pen adds a point per press and a double press ends the path; text drops a line where pressed; the brush strokes a path its width. A drag may end in the layer or out of it; each gesture ends once and asks the owner. SelectionBox, TransformHandles and SnapIndicator draw the marks. ToolPalette picks a tool from icons named in tips; BrushSettings sets a brush's width and hue; HistoryPanel lists the steps done, the undone ones quiet, and takes the drawing to a step on Enter or a double press. The model changes: lines and arrows end at a named corner, so they run either way, and paths carry their width. Three Lucide icons: hexagon, slash and move-up-right. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea99d30e3b
commit
49a205ea35
20 files changed
+1812
-34
No files matched your search
@@ -0,0 +1,127 @@
|
||||
use gpui::{App, Hsla, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, div};
|
||||
|
||||
use super::{
|
||||
edit::Handle,
|
||||
shape::Guide,
|
||||
view::{Frame, Viewport},
|
||||
};
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
/// A box in view pixels as an absolute element.
|
||||
fn placed(frame: &Frame) -> gpui::Div {
|
||||
div()
|
||||
.absolute()
|
||||
.left(Pixels::from(frame.x))
|
||||
.top(Pixels::from(frame.y))
|
||||
.w(Pixels::from(frame.w))
|
||||
.h(Pixels::from(frame.h))
|
||||
}
|
||||
|
||||
/// The marquee a drag on empty canvas draws, in view pixels: a faint wash in the accent and its edge.
|
||||
#[derive(IntoElement)]
|
||||
pub struct SelectionBox {
|
||||
frame: Frame,
|
||||
}
|
||||
|
||||
impl SelectionBox {
|
||||
pub fn new(frame: Frame) -> Self {
|
||||
Self { frame }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SelectionBox {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let accent = cx.theme().colors.accent;
|
||||
placed(&self.frame)
|
||||
.bg(accent.alpha(0.08))
|
||||
.border_1()
|
||||
.border_color(accent)
|
||||
}
|
||||
}
|
||||
|
||||
/// A selection's frame in view pixels with a square handle at each corner and side.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TransformHandles {
|
||||
frame: Frame,
|
||||
}
|
||||
|
||||
impl TransformHandles {
|
||||
pub fn new(frame: Frame) -> Self {
|
||||
Self { frame }
|
||||
}
|
||||
}
|
||||
|
||||
/// Where each handle's square sits, in view pixels, for a frame and a handle `side` wide.
|
||||
pub(crate) fn handle_boxes(frame: &Frame, side: f32) -> Vec<(Handle, Frame)> {
|
||||
Handle::ALL
|
||||
.into_iter()
|
||||
.map(|handle| {
|
||||
let (sx, sy) = handle.at();
|
||||
let (x, y) = (
|
||||
frame.x + frame.w * sx - side / 2.0,
|
||||
frame.y + frame.h * sy - side / 2.0,
|
||||
);
|
||||
(handle, Frame::new(x, y, side, side))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl RenderOnce for TransformHandles {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (accent, surface) = (theme.colors.accent, theme.colors.surface);
|
||||
let side = f32::from(theme.canvas().handle.to_pixels(window.rem_size()));
|
||||
let squares = handle_boxes(&self.frame, side)
|
||||
.into_iter()
|
||||
.map(move |(_, frame)| placed(&frame).bg(surface).border_1().border_color(accent));
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.child(placed(&self.frame).border_1().border_color(accent))
|
||||
.children(squares)
|
||||
}
|
||||
}
|
||||
|
||||
/// The lines a moving shape snaps to, across the whole view.
|
||||
#[derive(IntoElement)]
|
||||
pub struct SnapIndicator {
|
||||
guides: Vec<Guide>,
|
||||
viewport: Viewport,
|
||||
}
|
||||
|
||||
impl SnapIndicator {
|
||||
pub fn new(guides: impl IntoIterator<Item = Guide>, viewport: Viewport) -> Self {
|
||||
Self {
|
||||
guides: guides.into_iter().collect(),
|
||||
viewport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SnapIndicator {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let line: Hsla = cx.theme().colors.hue(3, "snap");
|
||||
let view = self.viewport;
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.children(self.guides.into_iter().map(move |guide| {
|
||||
match guide {
|
||||
Guide::Vertical(x) => div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.left(Pixels::from(view.to_view((x, 0.0)).0))
|
||||
.border_l_1()
|
||||
.border_color(line),
|
||||
Guide::Horizontal(y) => div()
|
||||
.absolute()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.top(Pixels::from(view.to_view((0.0, y)).1))
|
||||
.border_t_1()
|
||||
.border_color(line),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user