1mod font;
26mod geometry;
27mod input;
28mod paint;
29mod render_perf;
30mod scene;
31
32use std::cell::Cell;
33use std::collections::{HashMap, HashSet};
34use std::hash::{DefaultHasher, Hash, Hasher};
35use std::ops::Range;
36use std::rc::Rc;
37use std::sync::Arc;
38
39pub use font::Font;
40use geometry::*;
41use input::WhiteboardInputElement;
42use paint::*;
43use render_perf::WorldViewport;
44pub use scene::*;
45
46use gpui::{
47 AnyElement, AnyView, App, AppContext, Bounds, Context, CursorStyle, Div, ElementId,
48 ElementInputHandler, Entity, EntityInputHandler, FocusHandle, GlobalElementId, Hsla,
49 InspectorElementId, InteractiveElement, IntoElement, KeyDownEvent, LayoutId, MouseButton,
50 MouseDownEvent, MouseMoveEvent, MouseUpEvent, ObjectFit, ParentElement, PathBuilder,
51 PinchEvent, Pixels, Point, Render, Rgba, ScrollDelta, ScrollWheelEvent, SharedString,
52 StatefulInteractiveElement, Style, Styled, StyledImage, TransformationMatrix, UTF16Selection,
53 Window, canvas, div, fill, hsla, linear_color_stop, linear_gradient, point, px, relative, rgba,
54 size,
55};
56use serde::{Deserialize, Serialize};
57
58const MIN_ZOOM: f32 = 0.1;
61const MAX_ZOOM: f32 = 8.0;
62const GRID: f32 = 24.0;
64const MIN_DOT_SPACING: f32 = 16.0;
66const DOT: f32 = 2.0;
68const LINE_PX: f32 = 16.0;
70const VIEWPORT_CULL_MARGIN_PX: f32 = 96.0;
71
72fn accepts_wheel_input(read_only: bool) -> bool {
73 !read_only
74}
75const NIB: f32 = 2.5;
79const WIDTH_PRESETS: [f32; 5] = [1.0, 2.5, 4.0, 6.0, 9.0];
82const WIDTH_MIN: f32 = 1.0;
84const WIDTH_MAX: f32 = 20.0;
85const WIDTH_SLIDER_W: f32 = 156.0;
87const MIN_POINT_PX: f32 = 2.0;
89const SELECT_PAD: f32 = 6.0;
91const UNDO_CAP: usize = 50;
93const HANDLE_HALF: f32 = 4.0;
95const HANDLE_GRAB: f32 = 10.0;
97const CONNECTOR_BUTTON_GAP: f32 = 24.0;
99const CONNECTOR_BUTTON_SIZE: f32 = 20.0;
100const SV_W: f32 = 216.0;
102const SV_H: f32 = 140.0;
103const HUE_H: f32 = 14.0;
104const ROT_EPS: f32 = 0.05;
108const ROT_SNAP: f32 = 0.105;
111const TEXT_SIZE: f32 = 18.0;
113
114const LABEL_PAD: f32 = 8.0;
117
118const HIGHLIGHT_DEFAULT: u32 = 0xffe06680;
121const TEXT_CHAR_W: f32 = 0.55;
124const TEXT_LINE_H: f32 = 1.3;
125const EMBED_W: f32 = 210.0;
127const EMBED_H: f32 = 76.0;
128const MINDMAP_ROOT_W: f32 = 196.0;
129const MINDMAP_ROOT_H: f32 = 60.0;
130const MINDMAP_NODE_W: f32 = 164.0;
131const MINDMAP_NODE_H: f32 = 48.0;
132const MINDMAP_BRANCH_GAP_X: f32 = 120.0;
133const MINDMAP_BRANCH_GAP_Y: f32 = 84.0;
134const FLOWCHART_NODE_W: f32 = 180.0;
135const FLOWCHART_NODE_H: f32 = 52.0;
136const FLOWCHART_GAP_Y: f32 = 92.0;
137const FLOWCHART_BRANCH_GAP_X: f32 = 240.0;
138const IMAGE_PLACE_PX: f32 = 280.0;
140
141#[derive(Clone, Copy, PartialEq, Eq, Debug)]
143pub enum Tool {
144 Pan,
146 Select,
147 Pen,
148 Rect,
149 Ellipse,
150 Diamond,
151 Triangle,
152 RoundRect,
153 Star,
154 Hexagon,
155 Line,
156 Arrow,
157 DashedArrow,
158 Text,
159 MindMap,
160 Flowchart,
161 Embed,
162 Image,
163}
164
165impl Tool {
166 fn glyph(self) -> &'static str {
169 match self {
170 Tool::Pan => "☞",
173 Tool::Select => "↖",
174 Tool::Pen => "✎",
175 Tool::Rect => "▭",
176 Tool::Ellipse => "◯",
177 Tool::Diamond => "◇",
178 Tool::Triangle => "△",
179 Tool::RoundRect => "▢",
180 Tool::Star => "☆",
181 Tool::Hexagon => "⬡",
182 Tool::Line => "╱",
183 Tool::Arrow => "↗",
184 Tool::DashedArrow => "⇢",
185 Tool::Text => "T",
186 Tool::MindMap => "◎",
187 Tool::Flowchart => "⇅",
188 Tool::Embed => "▤",
189 Tool::Image => "▦",
190 }
191 }
192
193 fn label(self) -> &'static str {
196 match self {
197 Tool::Pan => "Pan — drag to move (H)",
198 Tool::Select => "Select (V)",
199 Tool::Pen => "Pen (P)",
200 Tool::Rect => "Rectangle (R)",
201 Tool::Ellipse => "Ellipse (O)",
202 Tool::Diamond => "Diamond (D)",
203 Tool::Triangle => "Triangle (G)",
204 Tool::RoundRect => "Rounded rectangle (U)",
205 Tool::Star => "Star (S)",
206 Tool::Hexagon => "Hexagon (X)",
207 Tool::Line => "Line (L)",
208 Tool::Arrow => "Arrow (A)",
209 Tool::DashedArrow => "Dashed arrow (K)",
210 Tool::Text => "Text (T)",
211 Tool::MindMap => "Mind map (M)",
212 Tool::Flowchart => "Flowchart (F)",
213 Tool::Embed => "Page card",
214 Tool::Image => "Image (I) — click to place",
215 }
216 }
217
218 fn shortcut(key: &str) -> Option<Tool> {
220 Some(match key {
221 "h" => Tool::Pan,
222 "v" => Tool::Select,
223 "p" => Tool::Pen,
224 "r" => Tool::Rect,
225 "o" => Tool::Ellipse,
226 "d" => Tool::Diamond,
227 "g" => Tool::Triangle,
228 "u" => Tool::RoundRect,
229 "s" => Tool::Star,
230 "x" => Tool::Hexagon,
231 "l" => Tool::Line,
232 "a" => Tool::Arrow,
233 "k" => Tool::DashedArrow,
234 "t" => Tool::Text,
235 "m" => Tool::MindMap,
236 "f" => Tool::Flowchart,
237 "i" => Tool::Image,
238 _ => return None,
239 })
240 }
241
242 fn icon(self) -> Option<(&'static str, &'static [u8])> {
249 const PAN: &[u8] = include_bytes!("../assets/icons/pan.svg");
250 const SELECT: &[u8] = include_bytes!("../assets/icons/select.svg");
251 const PEN: &[u8] = include_bytes!("../assets/icons/pen.svg");
252 const RECT: &[u8] = include_bytes!("../assets/icons/rect.svg");
253 const ELLIPSE: &[u8] = include_bytes!("../assets/icons/ellipse.svg");
254 const DIAMOND: &[u8] = include_bytes!("../assets/icons/diamond.svg");
255 const TRIANGLE: &[u8] = include_bytes!("../assets/icons/triangle.svg");
256 const ROUND_RECT: &[u8] = include_bytes!("../assets/icons/round-rect.svg");
257 const STAR: &[u8] = include_bytes!("../assets/icons/star.svg");
258 const HEXAGON: &[u8] = include_bytes!("../assets/icons/hexagon.svg");
259 const LINE: &[u8] = include_bytes!("../assets/icons/line.svg");
260 const ARROW: &[u8] = include_bytes!("../assets/icons/arrow.svg");
261 const TEXT: &[u8] = include_bytes!("../assets/icons/text.svg");
262 const MINDMAP: &[u8] = include_bytes!("../assets/icons/mindmap.svg");
263 const FLOWCHART: &[u8] = include_bytes!("../assets/icons/flowchart.svg");
264 const EMBED: &[u8] = include_bytes!("../assets/icons/embed.svg");
265 const IMAGE: &[u8] = include_bytes!("../assets/icons/image.svg");
266 match self {
267 Tool::Pan => Some(("wb-icon-pan", PAN)),
268 Tool::Select => Some(("wb-icon-select", SELECT)),
269 Tool::Pen => Some(("wb-icon-pen", PEN)),
270 Tool::Rect => Some(("wb-icon-rect", RECT)),
271 Tool::Ellipse => Some(("wb-icon-ellipse", ELLIPSE)),
272 Tool::Diamond => Some(("wb-icon-diamond", DIAMOND)),
273 Tool::Triangle => Some(("wb-icon-triangle", TRIANGLE)),
274 Tool::RoundRect => Some(("wb-icon-round-rect", ROUND_RECT)),
275 Tool::Star => Some(("wb-icon-star", STAR)),
276 Tool::Hexagon => Some(("wb-icon-hexagon", HEXAGON)),
277 Tool::Line => Some(("wb-icon-line", LINE)),
278 Tool::Arrow => Some(("wb-icon-arrow", ARROW)),
279 Tool::DashedArrow => None,
280 Tool::Text => Some(("wb-icon-text", TEXT)),
281 Tool::MindMap => Some(("wb-icon-mindmap", MINDMAP)),
282 Tool::Flowchart => Some(("wb-icon-flowchart", FLOWCHART)),
283 Tool::Embed => Some(("wb-icon-embed", EMBED)),
284 Tool::Image => Some(("wb-icon-image", IMAGE)),
285 }
286 }
287}
288
289#[derive(Clone, Copy, PartialEq, Eq)]
293enum ToolGroup {
294 Shapes,
296 Lines,
298 PagesImages,
300}
301
302impl ToolGroup {
303 const ALL: [ToolGroup; 3] = [ToolGroup::Shapes, ToolGroup::Lines, ToolGroup::PagesImages];
304
305 fn tools(self) -> &'static [Tool] {
307 match self {
308 ToolGroup::Shapes => &[
309 Tool::Rect,
310 Tool::RoundRect,
311 Tool::Ellipse,
312 Tool::Diamond,
313 Tool::Triangle,
314 Tool::Hexagon,
315 Tool::Star,
316 ],
317 ToolGroup::Lines => &[Tool::Pen, Tool::Line, Tool::Arrow, Tool::DashedArrow],
318 ToolGroup::PagesImages => &[Tool::MindMap, Tool::Flowchart, Tool::Embed, Tool::Image],
319 }
320 }
321
322 fn contains(self, t: Tool) -> bool {
323 self.tools().contains(&t)
324 }
325
326 fn representative(self) -> Tool {
328 match self {
329 ToolGroup::Shapes => Tool::Rect,
330 ToolGroup::Lines => Tool::Arrow,
331 ToolGroup::PagesImages => Tool::Flowchart,
332 }
333 }
334
335 fn label(self) -> &'static str {
336 match self {
337 ToolGroup::Shapes => "Shapes",
338 ToolGroup::Lines => "Lines",
339 ToolGroup::PagesImages => "Pages & images",
340 }
341 }
342}
343
344fn svg_icon(key: &'static str, bytes: &'static [u8], color: Hsla, sz: f32) -> impl IntoElement {
348 canvas(
349 |_, _, _| {},
350 move |bounds, _, window, cx| {
351 let _ = window.paint_svg(
352 bounds,
353 SharedString::from(key),
354 Some(bytes),
355 TransformationMatrix::default(),
356 color,
357 cx,
358 );
359 },
360 )
361 .w(px(sz))
362 .h(px(sz))
363}
364
365fn toolbar_divider(color: Hsla, vertical: bool) -> gpui::AnyElement {
367 let d = div().bg(color);
368 if vertical {
370 d.h(px(1.0)).w(px(16.0)).my(px(3.0))
371 } else {
372 d.w(px(1.0)).h(px(16.0)).mx(px(3.0))
373 }
374 .into_any_element()
375}
376
377struct Tip {
381 text: SharedString,
382 fg: Hsla,
383 bg: Hsla,
384 border: Hsla,
385}
386
387impl Render for Tip {
388 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
389 div().pt(px(16.0)).child(
392 div()
393 .px(px(6.0))
394 .py(px(2.0))
395 .rounded(px(4.0))
396 .border_1()
397 .border_color(self.border)
398 .bg(self.bg)
399 .text_color(self.fg)
400 .text_size(px(11.0))
401 .child(self.text.clone()),
402 )
403 }
404}
405
406#[derive(Clone, Debug)]
409pub struct WhiteboardStyle {
410 pub bg: Hsla,
412 pub grid: Hsla,
414 pub text: Hsla,
416 pub ink: Hsla,
418 pub panel: Hsla,
420 pub panel_strong: Hsla,
423 pub accent: Hsla,
425 pub selection: Hsla,
427 pub swatches: Vec<Hsla>,
430}
431
432pub type WhiteboardStyleFn = Rc<dyn Fn() -> WhiteboardStyle>;
435
436pub type ChangeFn = Rc<dyn Fn(String, &mut Window, &mut App)>;
439
440pub type PlaceEmbedFn = Rc<dyn Fn(f32, f32, &mut Window, &mut App)>;
443
444pub type OpenPageFn = Rc<dyn Fn(i64, &mut Window, &mut App)>;
446
447pub type SaveTemplateFn = Rc<dyn Fn(String, &mut Window, &mut App)>;
451
452pub type DeleteTemplateFn = Rc<dyn Fn(i64, &mut Window, &mut App)>;
454
455pub type CopyFn = Rc<dyn Fn(String, &mut Window, &mut App)>;
459
460pub type PasteFn = Rc<dyn Fn(&mut Window, &mut App) -> Option<String>>;
465
466pub type SavedColorsFn = Rc<dyn Fn(Vec<u32>, &mut Window, &mut App)>;
470
471pub type ImageFn = Rc<dyn Fn(&str, f32, &mut Window, &mut App) -> Option<gpui::ImageSource>>;
477
478pub type PlaceImageFn = Rc<dyn Fn(f32, f32, &mut Window, &mut App)>;
481
482pub type DropFilesFn = Rc<dyn Fn(Vec<std::path::PathBuf>, f32, f32, &mut Window, &mut App)>;
485
486#[derive(Clone, Copy, Debug, PartialEq, Eq)]
489pub enum FontPick {
490 Upload,
492 Default,
494}
495
496pub type PickFontFn = Rc<dyn Fn(FontPick, &mut Window, &mut App)>;
500
501pub type MoveToolbarFn = Rc<dyn Fn(Option<(f32, f32)>, bool, &mut Window, &mut App)>;
507
508pub type ExpandEmbedFn = Rc<dyn Fn(&mut Window, &mut App)>;
511
512#[derive(Clone, Debug)]
517pub struct Template {
518 pub id: i64,
519 pub name: String,
520 pub elements: Vec<Element>,
521}
522
523impl Template {
524 pub fn from_json(id: i64, name: impl Into<String>, elements_json: &str) -> Self {
528 Template {
529 id,
530 name: name.into(),
531 elements: serde_json::from_str(elements_json).unwrap_or_default(),
532 }
533 }
534}
535
536struct Pending {
538 anchor: [f32; 2],
539 kind: ElementKind,
540}
541
542#[derive(Clone, Copy, PartialEq)]
544struct ConnectPoint {
545 id: u64,
546 index: usize,
547 pos: [f32; 2],
548}
549
550#[derive(Clone, Copy)]
552struct ConnectDrag {
553 from: ConnectPoint,
554}
555
556#[derive(Clone, Copy, Debug, Default, PartialEq)]
557struct AlignmentGuides {
558 vertical: Option<f32>,
559 horizontal: Option<f32>,
560}
561
562struct Resizing {
564 id: u64,
565 handle: ResizeHandle,
567 anchor: [f32; 2],
569 from: [f32; 2],
571 grab: [f32; 2],
574 orig: ElementKind,
576}
577
578#[derive(Clone, Copy)]
581enum ResizeHandle {
582 Corner,
584 EdgeX,
586 EdgeY,
588}
589
590struct GroupResizing {
595 handle: ResizeHandle,
597 anchor: [f32; 2],
599 from: [f32; 2],
601 grab: [f32; 2],
603 orig: Vec<(u64, ElementKind)>,
605}
606
607#[derive(Clone, Copy)]
609struct EndpointDrag {
610 id: u64,
611 which: usize,
613}
614
615#[derive(Clone, Copy)]
618struct Rotating {
619 center: [f32; 2],
621 start_pointer: f32,
623 applied: f32,
625 base: Option<f32>,
629}
630
631enum HandleGrab {
633 Corner(Resizing),
634 Endpoint(EndpointDrag),
635 Rotate,
636 GroupCorner(GroupResizing),
637}
638
639#[derive(Clone, Copy, PartialEq)]
641enum PickerTarget {
642 Stroke,
644 Fill,
646 Text,
648}
649
650#[derive(Clone, Copy)]
653struct Picker {
654 target: PickerTarget,
655 h: f32,
656 s: f32,
657 v: f32,
658 a: f32,
659}
660
661#[derive(Clone, Copy, PartialEq)]
663enum PickerDrag {
664 Sv,
666 Hue,
668 Alpha,
670 Width,
672}
673
674pub struct WhiteboardView {
677 scene: Scene,
678 style: WhiteboardStyleFn,
679 read_only: bool,
680 on_change: Option<ChangeFn>,
681 on_place_embed: Option<PlaceEmbedFn>,
682 on_open: Option<OpenPageFn>,
683 on_save_template: Option<SaveTemplateFn>,
684 on_delete_template: Option<DeleteTemplateFn>,
685 on_image: Option<ImageFn>,
686 on_place_image: Option<PlaceImageFn>,
687 on_drop_files: Option<DropFilesFn>,
688 on_copy: Option<CopyFn>,
689 on_paste: Option<PasteFn>,
690 on_save_colors: Option<SavedColorsFn>,
691 on_pick_font: Option<PickFontFn>,
692 on_move_toolbar: Option<MoveToolbarFn>,
693 saved_colors: Vec<u32>,
696 templates: Vec<Template>,
699 context_menu: Option<Point<Pixels>>,
702 ctx_text_sub: bool,
704 format_flyout: bool,
706 font: Font,
709 text_layout_cache: HashMap<u64, CachedTextLayout>,
712 label_layout_cache: HashMap<u64, CachedLabelLayout>,
713 tool: Tool,
714 focus: FocusHandle,
716 editing: Option<u64>,
718 caret: usize,
720 sel_anchor: usize,
723 text_selecting: bool,
725 marked_range: Option<Range<usize>>,
727 bounds: Rc<Cell<Bounds<Pixels>>>,
730 pending: Option<Pending>,
732 selected: Vec<u64>,
734 marquee: Option<([f32; 2], [f32; 2])>,
736 hovered_connector: Option<ConnectPoint>,
738 connecting: Option<ConnectDrag>,
740 drag_from: Option<[f32; 2]>,
744 move_origin: [f32; 2],
747 moved: bool,
749 alignment_guides: AlignmentGuides,
751 resizing: Option<Resizing>,
753 group_resizing: Option<GroupResizing>,
755 endpoint: Option<EndpointDrag>,
757 rotating: Option<Rotating>,
759 active_stroke: Option<u32>,
761 active_fill: Option<u32>,
763 active_text: Option<u32>,
765 pending_style: Option<RunStyle>,
768 active_width: f32,
771 picker: Option<Picker>,
773 open_group: Option<ToolGroup>,
775 width_open: bool,
777 font_open: bool,
779 templates_open: bool,
781 picker_drag: Option<PickerDrag>,
783 picker_bounds: Rc<Cell<Bounds<Pixels>>>,
786 sv_bounds: Rc<Cell<Bounds<Pixels>>>,
787 hue_bounds: Rc<Cell<Bounds<Pixels>>>,
788 alpha_bounds: Rc<Cell<Bounds<Pixels>>>,
789 width_panel_bounds: Rc<Cell<Bounds<Pixels>>>,
792 width_bounds: Rc<Cell<Bounds<Pixels>>>,
793 toolbar_bounds: Rc<Cell<Bounds<Pixels>>>,
797 toolbar_grip_bounds: Rc<Cell<Bounds<Pixels>>>,
798 toolbar_pos: Option<(f32, f32)>,
801 toolbar_vertical: bool,
804 toolbar_drag: Option<(f32, f32)>,
806 history: Vec<Scene>,
808 redo: Vec<Scene>,
809 panning: bool,
811 last: Point<Pixels>,
813 next_id: u64,
815 dirty: bool,
817}
818
819pub struct BoardEmbedView {
823 board: Entity<WhiteboardView>,
824 style: WhiteboardStyleFn,
825 on_expand: Option<ExpandEmbedFn>,
826}
827
828pub struct BoardThumbnailView {
831 snapshot: LocalThumbnailSnapshot,
832 style: WhiteboardStyleFn,
833 font: Font,
834}
835
836impl WhiteboardView {
837 pub fn new(scene: Scene, style: WhiteboardStyleFn, cx: &mut Context<Self>) -> Self {
839 let next_id = scene
840 .elements
841 .iter()
842 .map(|e| e.id)
843 .max()
844 .map_or(0, |m| m + 1);
845 Self {
846 scene,
847 style,
848 read_only: false,
849 on_change: None,
850 on_place_embed: None,
851 on_open: None,
852 on_save_template: None,
853 on_delete_template: None,
854 on_image: None,
855 on_place_image: None,
856 on_drop_files: None,
857 on_copy: None,
858 on_paste: None,
859 on_save_colors: None,
860 on_pick_font: None,
861 on_move_toolbar: None,
862 saved_colors: Vec::new(),
863 templates: Vec::new(),
864 context_menu: None,
865 ctx_text_sub: false,
866 format_flyout: false,
867 font: Font::default(),
868 text_layout_cache: HashMap::new(),
869 label_layout_cache: HashMap::new(),
870 tool: Tool::Pan,
871 focus: cx.focus_handle(),
872 editing: None,
873 caret: 0,
874 sel_anchor: 0,
875 text_selecting: false,
876 marked_range: None,
877 bounds: Rc::new(Cell::new(Bounds::default())),
878 pending: None,
879 selected: Vec::new(),
880 marquee: None,
881 hovered_connector: None,
882 connecting: None,
883 drag_from: None,
884 move_origin: [0.0, 0.0],
885 moved: false,
886 alignment_guides: AlignmentGuides::default(),
887 resizing: None,
888 group_resizing: None,
889 endpoint: None,
890 rotating: None,
891 active_stroke: None,
892 active_fill: None,
893 active_text: None,
894 pending_style: None,
895 active_width: NIB,
896 picker: None,
897 open_group: None,
898 width_open: false,
899 font_open: false,
900 templates_open: false,
901 picker_drag: None,
902 picker_bounds: Rc::new(Cell::new(Bounds::default())),
903 sv_bounds: Rc::new(Cell::new(Bounds::default())),
904 hue_bounds: Rc::new(Cell::new(Bounds::default())),
905 alpha_bounds: Rc::new(Cell::new(Bounds::default())),
906 width_panel_bounds: Rc::new(Cell::new(Bounds::default())),
907 width_bounds: Rc::new(Cell::new(Bounds::default())),
908 toolbar_bounds: Rc::new(Cell::new(Bounds::default())),
909 toolbar_grip_bounds: Rc::new(Cell::new(Bounds::default())),
910 toolbar_pos: None,
911 toolbar_vertical: false,
912 toolbar_drag: None,
913 history: Vec::new(),
914 redo: Vec::new(),
915 panning: false,
916 last: Point::default(),
917 next_id,
918 dirty: false,
919 }
920 }
921
922 pub fn new_read_only(scene: Scene, style: WhiteboardStyleFn, cx: &mut Context<Self>) -> Self {
925 let mut this = Self::new(scene, style, cx);
926 this.read_only = true;
927 this.tool = Tool::Pan;
928 this
929 }
930
931 pub fn set_on_change(&mut self, f: ChangeFn) {
933 self.on_change = Some(f);
934 }
935
936 pub fn set_on_place_embed(&mut self, f: PlaceEmbedFn) {
938 self.on_place_embed = Some(f);
939 }
940
941 pub fn set_on_open(&mut self, f: OpenPageFn) {
943 self.on_open = Some(f);
944 }
945
946 pub fn set_on_save_template(&mut self, f: SaveTemplateFn) {
948 self.on_save_template = Some(f);
949 }
950
951 pub fn set_on_delete_template(&mut self, f: DeleteTemplateFn) {
953 self.on_delete_template = Some(f);
954 }
955
956 pub fn set_on_image(&mut self, f: ImageFn) {
958 self.on_image = Some(f);
959 }
960
961 pub fn set_on_place_image(&mut self, f: PlaceImageFn) {
963 self.on_place_image = Some(f);
964 }
965
966 pub fn set_on_drop_files(&mut self, f: DropFilesFn) {
968 self.on_drop_files = Some(f);
969 }
970
971 pub fn set_on_copy(&mut self, f: CopyFn) {
973 self.on_copy = Some(f);
974 }
975
976 pub fn set_on_paste(&mut self, f: PasteFn) {
979 self.on_paste = Some(f);
980 }
981
982 pub fn set_on_save_colors(&mut self, f: SavedColorsFn) {
984 self.on_save_colors = Some(f);
985 }
986
987 pub fn set_on_pick_font(&mut self, f: PickFontFn) {
991 self.on_pick_font = Some(f);
992 }
993
994 pub fn set_on_move_toolbar(&mut self, f: MoveToolbarFn) {
996 self.on_move_toolbar = Some(f);
997 }
998
999 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
1002 self.read_only = read_only;
1003 if read_only {
1004 self.tool = Tool::Pan;
1005 self.selected.clear();
1006 self.editing = None;
1007 self.pending = None;
1008 self.connecting = None;
1009 self.hovered_connector = None;
1010 self.context_menu = None;
1011 self.open_group = None;
1012 self.font_open = false;
1013 self.width_open = false;
1014 self.templates_open = false;
1015 self.picker = None;
1016 self.format_flyout = false;
1017 self.text_selecting = false;
1018 self.marked_range = None;
1019 }
1020 cx.notify();
1021 }
1022
1023 pub fn read_only(&self) -> bool {
1025 self.read_only
1026 }
1027
1028 pub fn set_toolbar_pos(&mut self, pos: Option<(f32, f32)>, cx: &mut Context<Self>) {
1031 self.toolbar_pos = pos;
1032 cx.notify();
1033 }
1034
1035 pub fn set_toolbar_vertical(&mut self, vertical: bool, cx: &mut Context<Self>) {
1038 self.toolbar_vertical = vertical;
1039 cx.notify();
1040 }
1041
1042 fn toggle_toolbar_orientation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1045 self.toolbar_vertical = !self.toolbar_vertical;
1046 if let Some(f) = self.on_move_toolbar.clone() {
1047 f(self.toolbar_pos, self.toolbar_vertical, window, cx);
1048 }
1049 cx.notify();
1050 }
1051
1052 fn clamp_toolbar(&self, x: f32, y: f32) -> (f32, f32) {
1054 let board = self.bounds.get().size;
1055 let pill = self.toolbar_bounds.get().size;
1056 let maxx = (f32::from(board.width) - f32::from(pill.width)).max(0.0);
1057 let maxy = (f32::from(board.height) - f32::from(pill.height)).max(0.0);
1058 (x.clamp(0.0, maxx), y.clamp(0.0, maxy))
1059 }
1060
1061 fn start_toolbar_drag(
1064 &mut self,
1065 p: Point<Pixels>,
1066 double: bool,
1067 window: &mut Window,
1068 cx: &mut Context<Self>,
1069 ) {
1070 if double {
1071 self.toolbar_drag = None;
1072 self.toolbar_pos = None;
1073 if let Some(f) = self.on_move_toolbar.clone() {
1074 f(None, self.toolbar_vertical, window, cx);
1075 }
1076 cx.notify();
1077 return;
1078 }
1079 self.picker = None;
1081 self.open_group = None;
1082 self.width_open = false;
1083 self.font_open = false;
1084 self.templates_open = false;
1085 self.context_menu = None;
1086 self.focus.focus(window, cx);
1088 let pill = self.toolbar_bounds.get().origin;
1089 self.toolbar_drag = Some((
1090 f32::from(pill.x) - f32::from(p.x),
1091 f32::from(pill.y) - f32::from(p.y),
1092 ));
1093 cx.notify();
1094 }
1095
1096 fn drag_toolbar(&mut self, p: Point<Pixels>, cx: &mut Context<Self>) {
1098 let Some((ox, oy)) = self.toolbar_drag else {
1099 return;
1100 };
1101 let board = self.bounds.get().origin;
1102 let x = f32::from(p.x) + ox - f32::from(board.x);
1103 let y = f32::from(p.y) + oy - f32::from(board.y);
1104 self.toolbar_pos = Some(self.clamp_toolbar(x, y));
1105 cx.notify();
1106 }
1107
1108 fn commit_toolbar_drag(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1110 if self.toolbar_drag.take().is_none() {
1111 return;
1112 }
1113 if let Some(f) = self.on_move_toolbar.clone() {
1114 f(self.toolbar_pos, self.toolbar_vertical, window, cx);
1115 }
1116 }
1117
1118 pub fn set_saved_colors(&mut self, colors: Vec<u32>, cx: &mut Context<Self>) {
1121 self.saved_colors = colors;
1122 cx.notify();
1123 }
1124
1125 fn save_current_color(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1128 if let Some(c) = self.picker_u32()
1129 && !self.saved_colors.contains(&c)
1130 {
1131 self.saved_colors.push(c);
1132 if let Some(f) = self.on_save_colors.clone() {
1133 f(self.saved_colors.clone(), window, cx);
1134 }
1135 }
1136 cx.notify();
1137 }
1138
1139 fn remove_saved_color(&mut self, c: u32, window: &mut Window, cx: &mut Context<Self>) {
1141 self.saved_colors.retain(|&x| x != c);
1142 if let Some(f) = self.on_save_colors.clone() {
1143 f(self.saved_colors.clone(), window, cx);
1144 }
1145 cx.notify();
1146 }
1147
1148 pub fn set_templates(&mut self, templates: Vec<Template>, cx: &mut Context<Self>) {
1151 self.templates = templates;
1152 cx.notify();
1153 }
1154
1155 pub fn set_font(&mut self, font: Font, cx: &mut Context<Self>) {
1158 self.font = font;
1159 self.text_layout_cache.clear();
1160 self.label_layout_cache.clear();
1161 cx.notify();
1162 }
1163
1164 fn tip(
1167 &self,
1168 text: impl Into<SharedString>,
1169 ) -> impl Fn(&mut Window, &mut App) -> AnyView + 'static {
1170 let style_fn = self.style.clone();
1171 let text = text.into();
1172 move |_window, cx| {
1173 let s = style_fn();
1174 let text = text.clone();
1175 cx.new(move |_| Tip {
1176 text,
1177 fg: s.ink,
1178 bg: s.panel,
1179 border: s.grid,
1180 })
1181 .into()
1182 }
1183 }
1184
1185 pub fn add_embed(
1190 &mut self,
1191 page_id: i64,
1192 title: impl Into<String>,
1193 x: f32,
1194 y: f32,
1195 cx: &mut Context<Self>,
1196 ) {
1197 self.push_undo();
1198 let id = self.next_id;
1199 self.next_id += 1;
1200 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
1201 self.scene.elements.push(Element {
1202 id,
1203 kind: ElementKind::Embed(EmbedGeom {
1204 page_id,
1205 title: title.into(),
1206 x,
1207 y,
1208 w: EMBED_W / zoom,
1209 h: EMBED_H / zoom,
1210 }),
1211 stroke: None,
1212 fill: None,
1213 label: None,
1214 label_color: None,
1215 styles: Vec::new(),
1216 mindmap: None,
1217 });
1218 self.selected = vec![id];
1219 self.tool = Tool::Select;
1220 cx.notify();
1221 }
1222
1223 pub fn add_image_at(
1230 &mut self,
1231 src: impl Into<String>,
1232 px_w: f32,
1233 px_h: f32,
1234 cx_world: f32,
1235 cy_world: f32,
1236 cx: &mut Context<Self>,
1237 ) {
1238 self.push_undo();
1239 let id = self.next_id;
1240 self.next_id += 1;
1241 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
1242 let longest = px_w.max(px_h).max(1.0);
1243 let scale = IMAGE_PLACE_PX / longest / zoom;
1244 let (w, h) = (px_w * scale, px_h * scale);
1245 self.scene.elements.push(Element {
1246 id,
1247 kind: ElementKind::Image(ImageGeom {
1248 src: src.into(),
1249 x: cx_world - w / 2.0,
1250 y: cy_world - h / 2.0,
1251 w,
1252 h,
1253 rotation: 0.0,
1254 }),
1255 stroke: None,
1256 fill: None,
1257 label: None,
1258 label_color: None,
1259 styles: Vec::new(),
1260 mindmap: None,
1261 });
1262 self.selected = vec![id];
1263 self.tool = Tool::Select;
1264 cx.notify();
1265 }
1266
1267 pub fn add_mindmap_seed(&mut self, center_x: f32, center_y: f32, cx: &mut Context<Self>) {
1272 self.push_undo();
1273 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
1274 let root_w = MINDMAP_ROOT_W / zoom;
1275 let root_h = MINDMAP_ROOT_H / zoom;
1276 let node_w = MINDMAP_NODE_W / zoom;
1277 let node_h = MINDMAP_NODE_H / zoom;
1278 let gap_x = MINDMAP_BRANCH_GAP_X / zoom;
1279 let gap_y = MINDMAP_BRANCH_GAP_Y / zoom;
1280 let stroke = Some(0x2563ebff);
1281 let root_fill = Some(0xdbeafeff);
1282 let node_fill = Some(0xffffffff);
1283
1284 let add_node = |scene: &mut Scene,
1285 next_id: &mut u64,
1286 x: f32,
1287 y: f32,
1288 w: f32,
1289 h: f32,
1290 label: &str,
1291 fill: Option<u32>,
1292 mindmap: Option<MindMapNodeMeta>| {
1293 let id = *next_id;
1294 *next_id += 1;
1295 scene.elements.push(Element {
1296 id,
1297 kind: ElementKind::RoundRect(BoxGeom {
1298 x,
1299 y,
1300 w,
1301 h,
1302 width: NIB / zoom,
1303 rotation: 0.0,
1304 }),
1305 stroke,
1306 fill,
1307 label: Some(label.to_string()),
1308 label_color: Some(0x0f172aff),
1309 styles: Vec::new(),
1310 mindmap,
1311 });
1312 id
1313 };
1314
1315 let root_id = add_node(
1316 &mut self.scene,
1317 &mut self.next_id,
1318 center_x - root_w / 2.0,
1319 center_y - root_h / 2.0,
1320 root_w,
1321 root_h,
1322 "Central topic",
1323 root_fill,
1324 Some(MindMapNodeMeta {
1325 parent: None,
1326 side: MindMapSide::Right,
1327 order: 0,
1328 root_direction: MindMapRootDirection::Both,
1329 connector_style: MindMapConnectorStyle::Bezier,
1330 }),
1331 );
1332 let right_top_id = add_node(
1333 &mut self.scene,
1334 &mut self.next_id,
1335 center_x + root_w / 2.0 + gap_x,
1336 center_y - gap_y - node_h / 2.0,
1337 node_w,
1338 node_h,
1339 "Branch 1",
1340 node_fill,
1341 Some(MindMapNodeMeta {
1342 parent: Some(root_id),
1343 side: MindMapSide::Right,
1344 order: 0,
1345 root_direction: MindMapRootDirection::Both,
1346 connector_style: MindMapConnectorStyle::Bezier,
1347 }),
1348 );
1349 let right_bottom_id = add_node(
1350 &mut self.scene,
1351 &mut self.next_id,
1352 center_x + root_w / 2.0 + gap_x,
1353 center_y + gap_y - node_h / 2.0,
1354 node_w,
1355 node_h,
1356 "Branch 2",
1357 node_fill,
1358 Some(MindMapNodeMeta {
1359 parent: Some(root_id),
1360 side: MindMapSide::Right,
1361 order: 1,
1362 root_direction: MindMapRootDirection::Both,
1363 connector_style: MindMapConnectorStyle::Bezier,
1364 }),
1365 );
1366 let left_top_id = add_node(
1367 &mut self.scene,
1368 &mut self.next_id,
1369 center_x - root_w / 2.0 - gap_x - node_w,
1370 center_y - gap_y - node_h / 2.0,
1371 node_w,
1372 node_h,
1373 "Branch 3",
1374 node_fill,
1375 Some(MindMapNodeMeta {
1376 parent: Some(root_id),
1377 side: MindMapSide::Left,
1378 order: 0,
1379 root_direction: MindMapRootDirection::Both,
1380 connector_style: MindMapConnectorStyle::Bezier,
1381 }),
1382 );
1383 let left_bottom_id = add_node(
1384 &mut self.scene,
1385 &mut self.next_id,
1386 center_x - root_w / 2.0 - gap_x - node_w,
1387 center_y + gap_y - node_h / 2.0,
1388 node_w,
1389 node_h,
1390 "Branch 4",
1391 node_fill,
1392 Some(MindMapNodeMeta {
1393 parent: Some(root_id),
1394 side: MindMapSide::Left,
1395 order: 1,
1396 root_direction: MindMapRootDirection::Both,
1397 connector_style: MindMapConnectorStyle::Bezier,
1398 }),
1399 );
1400
1401 let add_branch = |scene: &mut Scene,
1402 next_id: &mut u64,
1403 from_id: u64,
1404 from_connector: usize,
1405 to_id: u64,
1406 to_connector: usize| {
1407 let start_anchor = SegmentAnchor {
1408 element_id: from_id,
1409 connector: from_connector,
1410 };
1411 let end_anchor = SegmentAnchor {
1412 element_id: to_id,
1413 connector: to_connector,
1414 };
1415 let [x1, y1] =
1416 connector_world_pos_in(&scene.elements, start_anchor).unwrap_or([0.0, 0.0]);
1417 let [x2, y2] =
1418 connector_world_pos_in(&scene.elements, end_anchor).unwrap_or([0.0, 0.0]);
1419 let id = *next_id;
1420 *next_id += 1;
1421 scene.elements.push(Element {
1422 id,
1423 kind: ElementKind::Arrow(SegGeom {
1424 x1,
1425 y1,
1426 x2,
1427 y2,
1428 width: NIB / zoom,
1429 style: SegmentStyle::Solid,
1430 start_anchor: Some(start_anchor),
1431 end_anchor: Some(end_anchor),
1432 }),
1433 stroke,
1434 fill: None,
1435 label: None,
1436 label_color: None,
1437 styles: Vec::new(),
1438 mindmap: None,
1439 });
1440 };
1441
1442 add_branch(
1443 &mut self.scene,
1444 &mut self.next_id,
1445 root_id,
1446 1,
1447 right_top_id,
1448 3,
1449 );
1450 add_branch(
1451 &mut self.scene,
1452 &mut self.next_id,
1453 root_id,
1454 1,
1455 right_bottom_id,
1456 3,
1457 );
1458 add_branch(
1459 &mut self.scene,
1460 &mut self.next_id,
1461 root_id,
1462 3,
1463 left_top_id,
1464 1,
1465 );
1466 add_branch(
1467 &mut self.scene,
1468 &mut self.next_id,
1469 root_id,
1470 3,
1471 left_bottom_id,
1472 1,
1473 );
1474
1475 self.selected = vec![root_id];
1476 self.tool = Tool::Select;
1477 cx.notify();
1478 }
1479
1480 pub fn add_mindmap_seed_at_viewport_center(&mut self, cx: &mut Context<Self>) {
1481 let center = self.viewport_center();
1482 self.add_mindmap_seed(center[0], center[1], cx);
1483 }
1484
1485 pub fn add_flowchart_seed(&mut self, center_x: f32, center_y: f32, cx: &mut Context<Self>) {
1489 self.push_undo();
1490 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
1491 let node_w = FLOWCHART_NODE_W / zoom;
1492 let node_h = FLOWCHART_NODE_H / zoom;
1493 let gap_y = FLOWCHART_GAP_Y / zoom;
1494 let branch_gap_x = FLOWCHART_BRANCH_GAP_X / zoom;
1495 let stroke = Some(0x0f172aff);
1496 let fill = Some(0xffffffff);
1497
1498 let add_box = |scene: &mut Scene, next_id: &mut u64, kind: ElementKind, label: &str| {
1499 let id = *next_id;
1500 *next_id += 1;
1501 scene.elements.push(Element {
1502 id,
1503 kind,
1504 stroke,
1505 fill,
1506 label: Some(label.to_string()),
1507 label_color: Some(0x0f172aff),
1508 styles: Vec::new(),
1509 mindmap: None,
1510 });
1511 id
1512 };
1513 let add_arrow = |scene: &mut Scene,
1514 next_id: &mut u64,
1515 from_id: u64,
1516 from_connector: usize,
1517 to_id: u64,
1518 to_connector: usize| {
1519 let start_anchor = SegmentAnchor {
1520 element_id: from_id,
1521 connector: from_connector,
1522 };
1523 let end_anchor = SegmentAnchor {
1524 element_id: to_id,
1525 connector: to_connector,
1526 };
1527 let [x1, y1] =
1528 connector_world_pos_in(&scene.elements, start_anchor).unwrap_or([0.0, 0.0]);
1529 let [x2, y2] =
1530 connector_world_pos_in(&scene.elements, end_anchor).unwrap_or([0.0, 0.0]);
1531 let id = *next_id;
1532 *next_id += 1;
1533 scene.elements.push(Element {
1534 id,
1535 kind: ElementKind::Arrow(SegGeom {
1536 x1,
1537 y1,
1538 x2,
1539 y2,
1540 width: NIB / zoom,
1541 style: SegmentStyle::Solid,
1542 start_anchor: Some(start_anchor),
1543 end_anchor: Some(end_anchor),
1544 }),
1545 stroke,
1546 fill: None,
1547 label: None,
1548 label_color: None,
1549 styles: Vec::new(),
1550 mindmap: None,
1551 });
1552 };
1553
1554 let start_id = add_box(
1555 &mut self.scene,
1556 &mut self.next_id,
1557 ElementKind::Ellipse(BoxGeom {
1558 x: center_x - node_w / 2.0,
1559 y: center_y - gap_y - node_h * 1.5,
1560 w: node_w,
1561 h: node_h,
1562 width: NIB / zoom,
1563 rotation: 0.0,
1564 }),
1565 "Start",
1566 );
1567 let process_id = add_box(
1568 &mut self.scene,
1569 &mut self.next_id,
1570 ElementKind::RoundRect(BoxGeom {
1571 x: center_x - node_w / 2.0,
1572 y: center_y - node_h / 2.0,
1573 w: node_w,
1574 h: node_h,
1575 width: NIB / zoom,
1576 rotation: 0.0,
1577 }),
1578 "Process",
1579 );
1580 let decision_id = add_box(
1581 &mut self.scene,
1582 &mut self.next_id,
1583 ElementKind::Diamond(BoxGeom {
1584 x: center_x - node_w / 2.0,
1585 y: center_y + gap_y - node_h / 2.0,
1586 w: node_w,
1587 h: node_h,
1588 width: NIB / zoom,
1589 rotation: 0.0,
1590 }),
1591 "Decision",
1592 );
1593 let branch_yes_id = add_box(
1594 &mut self.scene,
1595 &mut self.next_id,
1596 ElementKind::RoundRect(BoxGeom {
1597 x: center_x + branch_gap_x - node_w / 2.0,
1598 y: center_y + gap_y - node_h / 2.0,
1599 w: node_w,
1600 h: node_h,
1601 width: NIB / zoom,
1602 rotation: 0.0,
1603 }),
1604 "Yes",
1605 );
1606 let branch_no_id = add_box(
1607 &mut self.scene,
1608 &mut self.next_id,
1609 ElementKind::RoundRect(BoxGeom {
1610 x: center_x - branch_gap_x - node_w / 2.0,
1611 y: center_y + gap_y - node_h / 2.0,
1612 w: node_w,
1613 h: node_h,
1614 width: NIB / zoom,
1615 rotation: 0.0,
1616 }),
1617 "No",
1618 );
1619 let end_id = add_box(
1620 &mut self.scene,
1621 &mut self.next_id,
1622 ElementKind::Ellipse(BoxGeom {
1623 x: center_x - node_w / 2.0,
1624 y: center_y + gap_y * 2.0 + node_h * 0.5,
1625 w: node_w,
1626 h: node_h,
1627 width: NIB / zoom,
1628 rotation: 0.0,
1629 }),
1630 "End",
1631 );
1632
1633 add_arrow(
1634 &mut self.scene,
1635 &mut self.next_id,
1636 start_id,
1637 2,
1638 process_id,
1639 0,
1640 );
1641 add_arrow(
1642 &mut self.scene,
1643 &mut self.next_id,
1644 process_id,
1645 2,
1646 decision_id,
1647 0,
1648 );
1649 add_arrow(
1650 &mut self.scene,
1651 &mut self.next_id,
1652 decision_id,
1653 1,
1654 branch_yes_id,
1655 3,
1656 );
1657 add_arrow(
1658 &mut self.scene,
1659 &mut self.next_id,
1660 decision_id,
1661 3,
1662 branch_no_id,
1663 1,
1664 );
1665 add_arrow(
1666 &mut self.scene,
1667 &mut self.next_id,
1668 branch_yes_id,
1669 2,
1670 end_id,
1671 1,
1672 );
1673 add_arrow(
1674 &mut self.scene,
1675 &mut self.next_id,
1676 branch_no_id,
1677 2,
1678 end_id,
1679 3,
1680 );
1681
1682 self.selected = vec![process_id];
1683 self.tool = Tool::Select;
1684 cx.notify();
1685 }
1686
1687 pub fn add_flowchart_seed_at_viewport_center(&mut self, cx: &mut Context<Self>) {
1688 let center = self.viewport_center();
1689 self.add_flowchart_seed(center[0], center[1], cx);
1690 }
1691
1692 fn mindmap_meta(&self, id: u64) -> Option<MindMapNodeMeta> {
1693 self.scene
1694 .elements
1695 .iter()
1696 .find(|element| element.id == id)
1697 .and_then(|element| element.mindmap)
1698 }
1699
1700 fn is_mindmap_node(&self, id: u64) -> bool {
1701 self.mindmap_meta(id).is_some()
1702 }
1703
1704 fn is_mindmap_root(&self, id: u64) -> bool {
1705 self.mindmap_meta(id)
1706 .is_some_and(|meta| meta.parent.is_none())
1707 }
1708
1709 fn selected_mindmap_root(&self) -> Option<u64> {
1710 self.selected_single()
1711 .filter(|id| self.is_mindmap_root(*id))
1712 }
1713
1714 fn mindmap_root_direction(&self, root_id: u64) -> MindMapRootDirection {
1715 self.mindmap_meta(root_id)
1716 .map(|meta| meta.root_direction)
1717 .unwrap_or_default()
1718 }
1719
1720 fn mindmap_connector_style_for_root(&self, root_id: u64) -> MindMapConnectorStyle {
1721 self.mindmap_meta(root_id)
1722 .map(|meta| meta.connector_style)
1723 .unwrap_or_default()
1724 }
1725
1726 fn mindmap_root_of(&self, id: u64) -> Option<u64> {
1727 let mut current = id;
1728 loop {
1729 let meta = self.mindmap_meta(current)?;
1730 match meta.parent {
1731 Some(parent) => current = parent,
1732 None => return Some(current),
1733 }
1734 }
1735 }
1736
1737 fn mindmap_children(&self, parent: u64, side: MindMapSide) -> Vec<u64> {
1738 let mut children: Vec<(usize, u64)> = self
1739 .scene
1740 .elements
1741 .iter()
1742 .filter_map(|element| {
1743 let meta = element.mindmap?;
1744 (meta.parent == Some(parent) && meta.side == side)
1745 .then_some((meta.order, element.id))
1746 })
1747 .collect();
1748 children.sort_by_key(|(order, id)| (*order, *id));
1749 children.into_iter().map(|(_, id)| id).collect()
1750 }
1751
1752 fn set_mindmap_node_side(&mut self, id: u64, side: MindMapSide) {
1753 if let Some(element) = self
1754 .scene
1755 .elements
1756 .iter_mut()
1757 .find(|element| element.id == id)
1758 && let Some(meta) = &mut element.mindmap
1759 {
1760 meta.side = side;
1761 }
1762 self.sync_mindmap_parent_link(id);
1763 }
1764
1765 fn sync_mindmap_parent_link(&mut self, child_id: u64) {
1766 let Some(meta) = self.mindmap_meta(child_id) else {
1767 return;
1768 };
1769 let Some(parent_id) = meta.parent else {
1770 return;
1771 };
1772 let parent_connector = match meta.side {
1773 MindMapSide::Right => 1,
1774 MindMapSide::Left => 3,
1775 };
1776 let child_connector = match meta.side {
1777 MindMapSide::Right => 3,
1778 MindMapSide::Left => 1,
1779 };
1780 for element in &mut self.scene.elements {
1781 let segment = match &mut element.kind {
1782 ElementKind::Line(segment) | ElementKind::Arrow(segment) => segment,
1783 _ => continue,
1784 };
1785 let start_id = segment.start_anchor.map(|anchor| anchor.element_id);
1786 let end_id = segment.end_anchor.map(|anchor| anchor.element_id);
1787 let links_parent_child = matches!((start_id, end_id), (Some(a), Some(b)) if (a == parent_id && b == child_id) || (a == child_id && b == parent_id));
1788 if !links_parent_child {
1789 continue;
1790 }
1791 if let Some(anchor) = &mut segment.start_anchor {
1792 if anchor.element_id == parent_id {
1793 anchor.connector = parent_connector;
1794 } else if anchor.element_id == child_id {
1795 anchor.connector = child_connector;
1796 }
1797 }
1798 if let Some(anchor) = &mut segment.end_anchor {
1799 if anchor.element_id == parent_id {
1800 anchor.connector = parent_connector;
1801 } else if anchor.element_id == child_id {
1802 anchor.connector = child_connector;
1803 }
1804 }
1805 }
1806 self.sync_segment_anchors_for(&[parent_id, child_id]);
1807 }
1808
1809 fn sync_mindmap_links_for_root(&mut self, root_id: u64) {
1810 let child_ids: Vec<u64> = self
1811 .scene
1812 .elements
1813 .iter()
1814 .filter_map(|element| {
1815 let meta = element.mindmap?;
1816 meta.parent?;
1817 (self.mindmap_root_of(element.id) == Some(root_id)).then_some(element.id)
1818 })
1819 .collect();
1820 for child_id in &child_ids {
1821 let Some(meta) = self.mindmap_meta(*child_id) else {
1822 continue;
1823 };
1824 let Some(parent_id) = meta.parent else {
1825 continue;
1826 };
1827 let parent_connector = match meta.side {
1828 MindMapSide::Right => 1,
1829 MindMapSide::Left => 3,
1830 };
1831 let child_connector = match meta.side {
1832 MindMapSide::Right => 3,
1833 MindMapSide::Left => 1,
1834 };
1835 for element in &mut self.scene.elements {
1836 let segment = match &mut element.kind {
1837 ElementKind::Line(segment) | ElementKind::Arrow(segment) => segment,
1838 _ => continue,
1839 };
1840 let start_id = segment.start_anchor.map(|anchor| anchor.element_id);
1841 let end_id = segment.end_anchor.map(|anchor| anchor.element_id);
1842 let links_parent_child = matches!((start_id, end_id), (Some(a), Some(b)) if (a == parent_id && b == *child_id) || (a == *child_id && b == parent_id));
1843 if !links_parent_child {
1844 continue;
1845 }
1846 if let Some(anchor) = &mut segment.start_anchor {
1847 if anchor.element_id == parent_id {
1848 anchor.connector = parent_connector;
1849 } else if anchor.element_id == *child_id {
1850 anchor.connector = child_connector;
1851 }
1852 }
1853 if let Some(anchor) = &mut segment.end_anchor {
1854 if anchor.element_id == parent_id {
1855 anchor.connector = parent_connector;
1856 } else if anchor.element_id == *child_id {
1857 anchor.connector = child_connector;
1858 }
1859 }
1860 }
1861 }
1862 self.sync_segment_anchors_for(&child_ids);
1863 }
1864
1865 fn ordered_mindmap_children(&self, parent: u64) -> Vec<u64> {
1866 let mut children: Vec<(f32, f32, usize, u64)> = self
1867 .scene
1868 .elements
1869 .iter()
1870 .filter_map(|element| {
1871 let meta = element.mindmap?;
1872 (meta.parent == Some(parent)).then(|| {
1873 let (x, y, _, h, _) =
1874 box_like(&element.kind).unwrap_or((0.0, 0.0, 0.0, 0.0, 0.0));
1875 (y + h / 2.0, x, meta.order, element.id)
1876 })
1877 })
1878 .collect();
1879 children.sort_by(|a, b| {
1880 a.0.total_cmp(&b.0)
1881 .then_with(|| a.1.total_cmp(&b.1))
1882 .then_with(|| a.2.cmp(&b.2))
1883 .then_with(|| a.3.cmp(&b.3))
1884 });
1885 children.into_iter().map(|(_, _, _, id)| id).collect()
1886 }
1887
1888 fn set_mindmap_children_side(&mut self, parent: u64, side: MindMapSide) {
1889 for child_id in self.ordered_mindmap_children(parent) {
1890 self.set_mindmap_node_side(child_id, side);
1891 }
1892 }
1893
1894 fn set_mindmap_children_side_alternating(&mut self, parent: u64) {
1895 for (index, child_id) in self
1896 .ordered_mindmap_children(parent)
1897 .into_iter()
1898 .enumerate()
1899 {
1900 let side = if index % 2 == 0 {
1901 MindMapSide::Right
1902 } else {
1903 MindMapSide::Left
1904 };
1905 self.set_mindmap_node_side(child_id, side);
1906 }
1907 }
1908
1909 fn reindex_mindmap_children(&mut self, parent: u64) {
1910 for side in [MindMapSide::Left, MindMapSide::Right] {
1911 let children = self.mindmap_children(parent, side);
1912 for (order, child_id) in children.into_iter().enumerate() {
1913 if let Some(element) = self
1914 .scene
1915 .elements
1916 .iter_mut()
1917 .find(|element| element.id == child_id)
1918 && let Some(meta) = &mut element.mindmap
1919 {
1920 meta.order = order;
1921 }
1922 self.reindex_mindmap_children(child_id);
1923 }
1924 }
1925 }
1926
1927 fn set_mindmap_root_direction(
1928 &mut self,
1929 root_id: u64,
1930 direction: MindMapRootDirection,
1931 cx: &mut Context<Self>,
1932 ) {
1933 if let Some(element) = self
1934 .scene
1935 .elements
1936 .iter_mut()
1937 .find(|element| element.id == root_id)
1938 && let Some(meta) = &mut element.mindmap
1939 {
1940 meta.root_direction = direction;
1941 }
1942 match direction {
1943 MindMapRootDirection::Left => {
1944 self.set_mindmap_children_side(root_id, MindMapSide::Left)
1945 }
1946 MindMapRootDirection::Right => {
1947 self.set_mindmap_children_side(root_id, MindMapSide::Right)
1948 }
1949 MindMapRootDirection::Both => self.set_mindmap_children_side_alternating(root_id),
1950 }
1951 self.reindex_mindmap_children(root_id);
1952 self.relayout_mindmap_tree(root_id);
1953 cx.notify();
1954 }
1955
1956 fn set_mindmap_connector_style(
1957 &mut self,
1958 root_id: u64,
1959 style: MindMapConnectorStyle,
1960 cx: &mut Context<Self>,
1961 ) {
1962 if let Some(element) = self
1963 .scene
1964 .elements
1965 .iter_mut()
1966 .find(|element| element.id == root_id)
1967 && let Some(meta) = &mut element.mindmap
1968 {
1969 meta.connector_style = style;
1970 }
1971 cx.notify();
1972 }
1973
1974 fn set_mindmap_node_position(&mut self, id: u64, x: f32, y: f32) {
1975 if let Some(element) = self
1976 .scene
1977 .elements
1978 .iter_mut()
1979 .find(|element| element.id == id)
1980 && let ElementKind::RoundRect(geom) = &mut element.kind
1981 {
1982 geom.x = x;
1983 geom.y = y;
1984 }
1985 }
1986
1987 fn mindmap_node_size(&self, id: u64, zoom: f32) -> (f32, f32) {
1988 self.scene
1989 .elements
1990 .iter()
1991 .find(|element| element.id == id)
1992 .and_then(|element| box_like(&element.kind).map(|(_, _, w, h, _)| (w, h)))
1993 .unwrap_or((MINDMAP_NODE_W / zoom, MINDMAP_NODE_H / zoom))
1994 }
1995
1996 fn side_stack_height(&self, parent: u64, side: MindMapSide, zoom: f32) -> f32 {
1997 let children = self.mindmap_children(parent, side);
1998 if children.is_empty() {
1999 return 0.0;
2000 }
2001 let gap_y = MINDMAP_BRANCH_GAP_Y / zoom;
2002 children
2003 .into_iter()
2004 .enumerate()
2005 .fold(0.0, |acc, (index, child_id)| {
2006 acc + if index > 0 { gap_y } else { 0.0 }
2007 + self.mindmap_subtree_height(child_id, zoom)
2008 })
2009 }
2010
2011 fn mindmap_subtree_height(&self, id: u64, zoom: f32) -> f32 {
2012 let (_, node_h) = self.mindmap_node_size(id, zoom);
2013 node_h.max(
2014 self.side_stack_height(id, MindMapSide::Left, zoom)
2015 .max(self.side_stack_height(id, MindMapSide::Right, zoom)),
2016 )
2017 }
2018
2019 fn relayout_mindmap_subtree(&mut self, node_id: u64, moved: &mut Vec<u64>, zoom: f32) {
2020 self.relayout_mindmap_children(node_id, MindMapSide::Left, moved, zoom);
2021 self.relayout_mindmap_children(node_id, MindMapSide::Right, moved, zoom);
2022 }
2023
2024 fn relayout_mindmap_children(
2025 &mut self,
2026 parent_id: u64,
2027 side: MindMapSide,
2028 moved: &mut Vec<u64>,
2029 zoom: f32,
2030 ) {
2031 let children = self.mindmap_children(parent_id, side);
2032 if children.is_empty() {
2033 return;
2034 }
2035 let Some((px, py, pw, ph, _)) = self
2036 .scene
2037 .elements
2038 .iter()
2039 .find(|element| element.id == parent_id)
2040 .and_then(|element| box_like(&element.kind))
2041 else {
2042 return;
2043 };
2044 let gap_x = MINDMAP_BRANCH_GAP_X / zoom;
2045 let gap_y = MINDMAP_BRANCH_GAP_Y / zoom;
2046 let total_h = children
2047 .iter()
2048 .enumerate()
2049 .fold(0.0, |acc, (index, child_id)| {
2050 acc + if index > 0 { gap_y } else { 0.0 }
2051 + self.mindmap_subtree_height(*child_id, zoom)
2052 });
2053 let mut cursor_y = py + ph / 2.0 - total_h / 2.0;
2054 for child_id in children {
2055 let subtree_h = self.mindmap_subtree_height(child_id, zoom);
2056 let (cw, ch) = self.mindmap_node_size(child_id, zoom);
2057 let cy = cursor_y + subtree_h / 2.0;
2058 let x = match side {
2059 MindMapSide::Right => px + pw + gap_x,
2060 MindMapSide::Left => px - gap_x - cw,
2061 };
2062 self.set_mindmap_node_position(child_id, x, cy - ch / 2.0);
2063 moved.push(child_id);
2064 self.relayout_mindmap_subtree(child_id, moved, zoom);
2065 cursor_y += subtree_h + gap_y;
2066 }
2067 }
2068
2069 fn relayout_mindmap_tree(&mut self, root_id: u64) {
2070 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
2071 let mut moved = vec![root_id];
2072 self.relayout_mindmap_subtree(root_id, &mut moved, zoom);
2073 self.sync_mindmap_links_for_root(root_id);
2074 self.sync_segment_anchors_for(&moved);
2075 }
2076
2077 fn bump_mindmap_sibling_orders(&mut self, parent: u64, side: MindMapSide, from_order: usize) {
2078 for element in &mut self.scene.elements {
2079 if let Some(meta) = &mut element.mindmap
2080 && meta.parent == Some(parent)
2081 && meta.side == side
2082 && meta.order >= from_order
2083 {
2084 meta.order += 1;
2085 }
2086 }
2087 }
2088
2089 fn create_mindmap_node(
2090 &mut self,
2091 parent: u64,
2092 side: MindMapSide,
2093 order: usize,
2094 label: &str,
2095 ) -> u64 {
2096 self.bump_mindmap_sibling_orders(parent, side, order);
2097 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
2098 let id = self.next_id;
2099 self.next_id += 1;
2100 let w = MINDMAP_NODE_W / zoom;
2101 let h = MINDMAP_NODE_H / zoom;
2102 let (x, y) = self
2103 .scene
2104 .elements
2105 .iter()
2106 .find(|element| element.id == parent)
2107 .and_then(|element| box_like(&element.kind))
2108 .map(|(px, py, pw, ph, _)| match side {
2109 MindMapSide::Right => (
2110 px + pw + MINDMAP_BRANCH_GAP_X / zoom,
2111 py + ph / 2.0 - h / 2.0,
2112 ),
2113 MindMapSide::Left => (
2114 px - MINDMAP_BRANCH_GAP_X / zoom - w,
2115 py + ph / 2.0 - h / 2.0,
2116 ),
2117 })
2118 .unwrap_or((0.0, 0.0));
2119 self.scene.elements.push(Element {
2120 id,
2121 kind: ElementKind::RoundRect(BoxGeom {
2122 x,
2123 y,
2124 w,
2125 h,
2126 width: NIB / zoom,
2127 rotation: 0.0,
2128 }),
2129 stroke: Some(0x2563ebff),
2130 fill: Some(0xffffffff),
2131 label: Some(label.to_string()),
2132 label_color: Some(0x0f172aff),
2133 styles: Vec::new(),
2134 mindmap: Some(MindMapNodeMeta {
2135 parent: Some(parent),
2136 side,
2137 order,
2138 root_direction: MindMapRootDirection::Both,
2139 connector_style: MindMapConnectorStyle::Bezier,
2140 }),
2141 });
2142 let start_anchor = SegmentAnchor {
2143 element_id: parent,
2144 connector: match side {
2145 MindMapSide::Right => 1,
2146 MindMapSide::Left => 3,
2147 },
2148 };
2149 let end_anchor = SegmentAnchor {
2150 element_id: id,
2151 connector: match side {
2152 MindMapSide::Right => 3,
2153 MindMapSide::Left => 1,
2154 },
2155 };
2156 let [x1, y1] = connector_world_pos_in(&self.scene.elements, start_anchor).unwrap_or([x, y]);
2157 let [x2, y2] = connector_world_pos_in(&self.scene.elements, end_anchor).unwrap_or([x, y]);
2158 let line_id = self.next_id;
2159 self.next_id += 1;
2160 self.scene.elements.push(Element {
2161 id: line_id,
2162 kind: ElementKind::Arrow(SegGeom {
2163 x1,
2164 y1,
2165 x2,
2166 y2,
2167 width: NIB / zoom,
2168 style: SegmentStyle::Solid,
2169 start_anchor: Some(start_anchor),
2170 end_anchor: Some(end_anchor),
2171 }),
2172 stroke: Some(0x2563ebff),
2173 fill: None,
2174 label: None,
2175 label_color: None,
2176 styles: Vec::new(),
2177 mindmap: None,
2178 });
2179 if let Some(root_id) = self.mindmap_root_of(parent) {
2180 self.relayout_mindmap_tree(root_id);
2181 }
2182 id
2183 }
2184
2185 fn add_mindmap_relative(
2186 &mut self,
2187 source_id: u64,
2188 sibling: bool,
2189 window: &mut Window,
2190 cx: &mut Context<Self>,
2191 ) -> bool {
2192 let Some(meta) = self.mindmap_meta(source_id) else {
2193 return false;
2194 };
2195 let (parent, side, order) = if sibling {
2196 match meta.parent {
2197 Some(parent) => (parent, meta.side, meta.order + 1),
2198 None => (
2199 source_id,
2200 MindMapSide::Right,
2201 self.mindmap_children(source_id, MindMapSide::Right).len(),
2202 ),
2203 }
2204 } else {
2205 (
2206 source_id,
2207 meta.side,
2208 self.mindmap_children(source_id, meta.side).len(),
2209 )
2210 };
2211 self.push_undo();
2212 let new_id = self.create_mindmap_node(parent, side, order, "");
2213 self.selected = vec![new_id];
2214 self.begin_text_edit(new_id, 0, window, cx);
2215 self.dirty = true;
2216 cx.notify();
2217 true
2218 }
2219
2220 fn mindmap_connector_style_for_element(
2221 &self,
2222 kind: &ElementKind,
2223 ) -> Option<MindMapConnectorStyle> {
2224 let seg = match kind {
2225 ElementKind::Line(seg) | ElementKind::Arrow(seg) => seg,
2226 _ => return None,
2227 };
2228 let start_root = seg
2229 .start_anchor
2230 .and_then(|anchor| self.mindmap_root_of(anchor.element_id));
2231 let end_root = seg
2232 .end_anchor
2233 .and_then(|anchor| self.mindmap_root_of(anchor.element_id));
2234 match (start_root, end_root) {
2235 (Some(a), Some(b)) if a == b => Some(self.mindmap_connector_style_for_root(a)),
2236 _ => None,
2237 }
2238 }
2239
2240 pub fn viewport_center(&self) -> [f32; 2] {
2243 let b = self.bounds.get();
2244 let cam = self.scene.camera;
2245 let z = cam.zoom.max(MIN_ZOOM);
2246 [
2247 cam.x + f32::from(b.size.width) / 2.0 / z,
2248 cam.y + f32::from(b.size.height) / 2.0 / z,
2249 ]
2250 }
2251
2252 pub fn scene(&self) -> &Scene {
2254 &self.scene
2255 }
2256
2257 pub fn local_thumbnail_spec(
2264 &self,
2265 width_px: f32,
2266 height_px: f32,
2267 ) -> Option<LocalThumbnailSpec> {
2268 self.local_thumbnail_spec_for_mode(LocalThumbnailMode::Auto, width_px, height_px)
2269 }
2270
2271 pub fn local_thumbnail_snapshot(
2272 &self,
2273 width_px: f32,
2274 height_px: f32,
2275 ) -> Option<LocalThumbnailSnapshot> {
2276 self.local_thumbnail_snapshot_for_mode(LocalThumbnailMode::Auto, width_px, height_px)
2277 }
2278
2279 pub fn local_thumbnail_spec_for_mode(
2280 &self,
2281 mode: LocalThumbnailMode,
2282 width_px: f32,
2283 height_px: f32,
2284 ) -> Option<LocalThumbnailSpec> {
2285 let scene_bounds = self.scene_bbox();
2286 let (anchor_element_id, focus) = match mode {
2287 LocalThumbnailMode::Auto => self
2288 .selection_bbox()
2289 .map(|bb| (self.selected_single(), bb))
2290 .or_else(|| self.viewport_world_bbox().map(|bb| (None, bb)))
2291 .or_else(|| scene_bounds.map(|bb| (None, bb)))?,
2292 LocalThumbnailMode::Selection => {
2293 let bb = self.selection_bbox()?;
2294 (self.selected_single(), bb)
2295 }
2296 LocalThumbnailMode::Viewport => (None, self.viewport_world_bbox()?),
2297 LocalThumbnailMode::AllContent => (None, scene_bounds?),
2298 LocalThumbnailMode::Element(id) => (Some(id), self.element_bbox(id)?),
2299 };
2300 Some(self.thumbnail_spec_from_bbox(
2301 anchor_element_id,
2302 focus,
2303 scene_bounds,
2304 width_px,
2305 height_px,
2306 ))
2307 }
2308
2309 pub fn local_thumbnail_snapshot_for_mode(
2310 &self,
2311 mode: LocalThumbnailMode,
2312 width_px: f32,
2313 height_px: f32,
2314 ) -> Option<LocalThumbnailSnapshot> {
2315 Some(LocalThumbnailSnapshot {
2316 scene: self.scene.clone(),
2317 spec: self.local_thumbnail_spec_for_mode(mode, width_px, height_px)?,
2318 })
2319 }
2320
2321 fn selected_single(&self) -> Option<u64> {
2324 match self.selected.as_slice() {
2325 [id] => Some(*id),
2326 _ => None,
2327 }
2328 }
2329
2330 fn is_selected(&self, id: u64) -> bool {
2331 self.selected.contains(&id)
2332 }
2333
2334 fn element_bbox(&self, id: u64) -> Option<(f32, f32, f32, f32)> {
2335 self.scene
2336 .elements
2337 .iter()
2338 .find(|e| e.id == id)
2339 .map(|e| bbox(&e.kind))
2340 }
2341
2342 fn scene_bbox(&self) -> Option<(f32, f32, f32, f32)> {
2343 scene_bbox_for_local_thumbnail(&self.scene)
2344 }
2345
2346 fn viewport_world_bbox(&self) -> Option<(f32, f32, f32, f32)> {
2347 let b = self.bounds.get();
2348 let vw = f32::from(b.size.width);
2349 let vh = f32::from(b.size.height);
2350 if vw <= 1.0 || vh <= 1.0 {
2351 return None;
2352 }
2353 let cam = self.scene.camera;
2354 let z = cam.zoom.max(MIN_ZOOM);
2355 Some((cam.x, cam.y, cam.x + vw / z, cam.y + vh / z))
2356 }
2357
2358 fn render_viewport(&self, fallback_size: Option<gpui::Size<Pixels>>) -> Option<WorldViewport> {
2359 let bounds = self.bounds.get();
2360 let size = if f32::from(bounds.size.width) > 1.0 && f32::from(bounds.size.height) > 1.0 {
2361 bounds.size
2362 } else {
2363 fallback_size?
2364 };
2365 let camera = self.scene.camera;
2366 WorldViewport::from_canvas(
2367 f32::from(size.width),
2368 f32::from(size.height),
2369 camera.x,
2370 camera.y,
2371 camera.zoom.max(MIN_ZOOM),
2372 VIEWPORT_CULL_MARGIN_PX,
2373 )
2374 }
2375
2376 fn thumbnail_spec_from_bbox(
2377 &self,
2378 anchor_element_id: Option<u64>,
2379 focus: (f32, f32, f32, f32),
2380 scene_bounds: Option<(f32, f32, f32, f32)>,
2381 width_px: f32,
2382 height_px: f32,
2383 ) -> LocalThumbnailSpec {
2384 local_thumbnail_spec_from_bbox(anchor_element_id, focus, scene_bounds, width_px, height_px)
2385 }
2386
2387 fn selection_bbox(&self) -> Option<(f32, f32, f32, f32)> {
2389 let mut it = self
2390 .scene
2391 .elements
2392 .iter()
2393 .filter(|e| self.selected.contains(&e.id))
2394 .map(|e| bbox(&e.kind));
2395 let first = it.next()?;
2396 Some(it.fold(first, |a, b| {
2397 (a.0.min(b.0), a.1.min(b.1), a.2.max(b.2), a.3.max(b.3))
2398 }))
2399 }
2400
2401 fn aligned_move_delta(&self, dx: f32, dy: f32) -> (f32, f32, AlignmentGuides) {
2402 const SNAP_PX: f32 = 6.0;
2403 let Some(selection) = self.selection_bbox() else {
2404 return (dx, dy, AlignmentGuides::default());
2405 };
2406 let threshold = SNAP_PX / self.scene.camera.zoom.max(MIN_ZOOM);
2407 let moving_x = [
2408 selection.0 + dx,
2409 (selection.0 + selection.2) / 2.0 + dx,
2410 selection.2 + dx,
2411 ];
2412 let moving_y = [
2413 selection.1 + dy,
2414 (selection.1 + selection.3) / 2.0 + dy,
2415 selection.3 + dy,
2416 ];
2417 let mut best_x: Option<(f32, f32)> = None;
2418 let mut best_y: Option<(f32, f32)> = None;
2419
2420 for element in self
2421 .scene
2422 .elements
2423 .iter()
2424 .filter(|element| !self.selected.contains(&element.id))
2425 {
2426 let bb = bbox(&element.kind);
2427 for moving in moving_x {
2428 for target in [bb.0, (bb.0 + bb.2) / 2.0, bb.2] {
2429 let correction = target - moving;
2430 if correction.abs() <= threshold
2431 && best_x.is_none_or(|(best, _)| correction.abs() < best.abs())
2432 {
2433 best_x = Some((correction, target));
2434 }
2435 }
2436 }
2437 for moving in moving_y {
2438 for target in [bb.1, (bb.1 + bb.3) / 2.0, bb.3] {
2439 let correction = target - moving;
2440 if correction.abs() <= threshold
2441 && best_y.is_none_or(|(best, _)| correction.abs() < best.abs())
2442 {
2443 best_y = Some((correction, target));
2444 }
2445 }
2446 }
2447 }
2448
2449 (
2450 dx + best_x.map_or(0.0, |(correction, _)| correction),
2451 dy + best_y.map_or(0.0, |(correction, _)| correction),
2452 AlignmentGuides {
2453 vertical: best_x.map(|(_, target)| target),
2454 horizontal: best_y.map(|(_, target)| target),
2455 },
2456 )
2457 }
2458
2459 fn sync_segment_anchors_for(&mut self, changed_ids: &[u64]) {
2460 if changed_ids.is_empty() {
2461 return;
2462 }
2463 let elements = self.scene.elements.clone();
2464 for element in &mut self.scene.elements {
2465 let segment = match &mut element.kind {
2466 ElementKind::Line(segment) | ElementKind::Arrow(segment) => segment,
2467 _ => continue,
2468 };
2469 if let Some(anchor) = segment.start_anchor
2470 && changed_ids.contains(&anchor.element_id)
2471 && let Some(pos) = connector_world_pos_in(&elements, anchor)
2472 {
2473 segment.x1 = pos[0];
2474 segment.y1 = pos[1];
2475 }
2476 if let Some(anchor) = segment.end_anchor
2477 && changed_ids.contains(&anchor.element_id)
2478 && let Some(pos) = connector_world_pos_in(&elements, anchor)
2479 {
2480 segment.x2 = pos[0];
2481 segment.y2 = pos[1];
2482 }
2483 }
2484 }
2485
2486 fn detach_segment_bindings_for_move(&mut self, ids: &[u64]) {
2487 for element in &mut self.scene.elements {
2488 if !ids.contains(&element.id) {
2489 continue;
2490 }
2491 if let ElementKind::Line(segment) | ElementKind::Arrow(segment) = &mut element.kind {
2492 if segment
2493 .start_anchor
2494 .is_some_and(|anchor| !ids.contains(&anchor.element_id))
2495 {
2496 segment.start_anchor = None;
2497 }
2498 if segment
2499 .end_anchor
2500 .is_some_and(|anchor| !ids.contains(&anchor.element_id))
2501 {
2502 segment.end_anchor = None;
2503 }
2504 }
2505 }
2506 }
2507
2508 fn set_segment_endpoint_anchor(
2509 &mut self,
2510 segment_id: u64,
2511 endpoint: usize,
2512 anchor: Option<SegmentAnchor>,
2513 ) {
2514 let pos = anchor.and_then(|anchor| {
2515 connector_world_pos_in(&self.scene.elements, anchor).map(|pos| (anchor, pos))
2516 });
2517 let Some(element) = self
2518 .scene
2519 .elements
2520 .iter_mut()
2521 .find(|element| element.id == segment_id)
2522 else {
2523 return;
2524 };
2525 let segment = match &mut element.kind {
2526 ElementKind::Line(segment) | ElementKind::Arrow(segment) => segment,
2527 _ => return,
2528 };
2529 if endpoint == 0 {
2530 segment.start_anchor = anchor;
2531 if let Some((_, pos)) = pos {
2532 segment.x1 = pos[0];
2533 segment.y1 = pos[1];
2534 }
2535 } else {
2536 segment.end_anchor = anchor;
2537 if let Some((_, pos)) = pos {
2538 segment.x2 = pos[0];
2539 segment.y2 = pos[1];
2540 }
2541 }
2542 }
2543
2544 fn group_rotatable(&self) -> bool {
2547 self.selected.len() > 1
2548 && self
2549 .scene
2550 .elements
2551 .iter()
2552 .any(|e| self.selected.contains(&e.id) && rotatable(&e.kind))
2553 }
2554
2555 pub fn tool(&self) -> Tool {
2557 self.tool
2558 }
2559
2560 pub fn set_tool(&mut self, tool: Tool, cx: &mut Context<Self>) {
2563 if self.read_only {
2564 self.tool = Tool::Pan;
2565 self.selected.clear();
2566 self.open_group = None;
2567 cx.notify();
2568 return;
2569 }
2570 self.open_group = None;
2571 if self.tool != tool {
2572 self.tool = tool;
2573 if tool != Tool::Select {
2574 self.selected.clear();
2575 }
2576 }
2577 cx.notify();
2578 }
2579
2580 pub fn reset_view(&mut self, cx: &mut Context<Self>) {
2582 self.scene.camera = Camera::default();
2583 self.dirty = true;
2584 cx.notify();
2585 }
2586
2587 pub fn zoom_in(&mut self, cx: &mut Context<Self>) {
2589 self.zoom_centered(1.2, cx);
2590 }
2591 pub fn zoom_out(&mut self, cx: &mut Context<Self>) {
2592 self.zoom_centered(1.0 / 1.2, cx);
2593 }
2594
2595 fn zoom_centered(&mut self, factor: f32, cx: &mut Context<Self>) {
2596 let b = self.bounds.get();
2597 let rx = f32::from(b.size.width) / 2.0;
2598 let ry = f32::from(b.size.height) / 2.0;
2599 self.scene.camera.zoom_about(rx, ry, factor);
2600 self.dirty = true;
2601 cx.notify();
2602 }
2603
2604 fn push_undo(&mut self) {
2606 self.history.push(self.scene.clone());
2607 if self.history.len() > UNDO_CAP {
2608 self.history.remove(0);
2609 }
2610 self.redo.clear();
2611 }
2612
2613 pub fn undo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2615 if let Some(prev) = self.history.pop() {
2616 self.redo.push(std::mem::replace(&mut self.scene, prev));
2617 self.selected.clear();
2618 self.dirty = true;
2619 cx.notify();
2620 self.flush(window, cx);
2621 }
2622 }
2623
2624 pub fn redo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2626 if let Some(next) = self.redo.pop() {
2627 self.history.push(std::mem::replace(&mut self.scene, next));
2628 self.selected.clear();
2629 self.dirty = true;
2630 cx.notify();
2631 self.flush(window, cx);
2632 }
2633 }
2634
2635 fn delete_selected(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2637 if self.selected.is_empty() {
2638 return;
2639 }
2640 self.push_undo();
2641 let gone = std::mem::take(&mut self.selected);
2642 self.scene.elements.retain(|e| !gone.contains(&e.id));
2643 self.editing = None;
2644 self.dirty = true;
2645 cx.notify();
2646 self.flush(window, cx);
2647 }
2648
2649 fn reorder_selection(&mut self, op: ZOrder, window: &mut Window, cx: &mut Context<Self>) {
2654 if self.selected.is_empty() {
2655 return;
2656 }
2657 let sel = self.selected.clone();
2658 let on = |id: u64| sel.contains(&id);
2659 self.push_undo();
2660 let before: Vec<u64> = self.scene.elements.iter().map(|e| e.id).collect();
2661 let els = &mut self.scene.elements;
2662 match op {
2663 ZOrder::ToFront => els.sort_by_key(|e| on(e.id)),
2666 ZOrder::ToBack => els.sort_by_key(|e| !on(e.id)),
2667 ZOrder::Forward => {
2671 for i in (0..els.len().saturating_sub(1)).rev() {
2672 if on(els[i].id) && !on(els[i + 1].id) {
2673 els.swap(i, i + 1);
2674 }
2675 }
2676 }
2677 ZOrder::Backward => {
2678 for i in 1..els.len() {
2679 if on(els[i].id) && !on(els[i - 1].id) {
2680 els.swap(i, i - 1);
2681 }
2682 }
2683 }
2684 }
2685 if self.scene.elements.iter().map(|e| e.id).eq(before) {
2686 self.history.pop(); return;
2688 }
2689 self.dirty = true;
2690 cx.notify();
2691 self.flush(window, cx);
2692 }
2693
2694 fn flush(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2696 if !self.dirty {
2697 return;
2698 }
2699 self.dirty = false;
2700 if let Some(f) = self.on_change.clone() {
2701 f(self.scene.to_json(), window, cx);
2702 }
2703 }
2704
2705 fn selection_json(&self) -> Option<String> {
2713 let sel: Vec<&Element> = self
2714 .scene
2715 .elements
2716 .iter()
2717 .filter(|e| self.selected.contains(&e.id))
2718 .collect();
2719 if sel.is_empty() {
2720 return None;
2721 }
2722 let (minx, miny) = sel
2723 .iter()
2724 .fold((f32::INFINITY, f32::INFINITY), |(mx, my), e| {
2725 let (x0, y0, ..) = bbox(&e.kind);
2726 (mx.min(x0), my.min(y0))
2727 });
2728 let elems: Vec<Element> = sel
2729 .iter()
2730 .map(|e| {
2731 let mut c = (*e).clone();
2732 translate(&mut c.kind, -minx, -miny);
2733 c
2734 })
2735 .collect();
2736 serde_json::to_string(&elems).ok()
2737 }
2738
2739 fn save_selection_as_template(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2741 self.context_menu = None;
2742 if let Some(json) = self.selection_json()
2743 && let Some(f) = self.on_save_template.clone()
2744 {
2745 f(json, window, cx);
2746 }
2747 cx.notify();
2748 }
2749
2750 fn apply_template(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
2753 let Some(elems) = self.templates.get(index).map(|t| t.elements.clone()) else {
2754 return;
2755 };
2756 self.templates_open = false;
2757 self.stamp_elements(&elems, window, cx);
2758 }
2759
2760 fn stamp_elements(&mut self, elems: &[Element], window: &mut Window, cx: &mut Context<Self>) {
2765 if elems.is_empty() {
2766 return;
2767 }
2768 self.open_group = None;
2769 self.push_undo();
2770 let b = self.bounds.get();
2772 let cam = self.scene.camera;
2773 let z = cam.zoom.max(MIN_ZOOM);
2774 let (tw, th) = elements_extent(elems);
2775 let off = [
2776 cam.x + (f32::from(b.size.width) / 2.0) / z - tw / 2.0,
2777 cam.y + (f32::from(b.size.height) / 2.0) / z - th / 2.0,
2778 ];
2779 let mut new_ids = Vec::with_capacity(elems.len());
2780 for e in elems {
2781 let mut c = e.clone();
2782 translate(&mut c.kind, off[0], off[1]);
2783 c.id = self.next_id;
2784 self.next_id += 1;
2785 new_ids.push(c.id);
2786 self.scene.elements.push(c);
2787 }
2788 self.selected = new_ids;
2789 self.tool = Tool::Select;
2790 self.dirty = true;
2791 self.flush(window, cx);
2792 cx.notify();
2793 }
2794
2795 fn copy_selection(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
2799 let Some(json) = self.selection_json() else {
2800 return false;
2801 };
2802 if let Some(f) = self.on_copy.clone() {
2803 f(json, window, cx);
2804 }
2805 true
2806 }
2807
2808 pub fn paste_elements(&mut self, json: &str, window: &mut Window, cx: &mut Context<Self>) {
2813 if let Ok(elems) = serde_json::from_str::<Vec<Element>>(json) {
2814 self.stamp_elements(&elems, window, cx);
2815 }
2816 }
2817
2818 fn delete_template(&mut self, id: i64, window: &mut Window, cx: &mut Context<Self>) {
2821 if let Some(f) = self.on_delete_template.clone() {
2822 f(id, window, cx);
2823 }
2824 }
2825
2826 fn template_card(
2831 &self,
2832 index: usize,
2833 ink: Hsla,
2834 text: Hsla,
2835 grid: Hsla,
2836 bg: Hsla,
2837 cx: &mut Context<Self>,
2838 ) -> gpui::AnyElement {
2839 let t = &self.templates[index];
2840 let id = t.id;
2841 let name: SharedString = t.name.clone().into();
2842 let elems = t.elements.clone();
2843 let (tw, th) = elements_extent(&elems);
2844 let preview = canvas(
2845 |_, _, _| {},
2846 move |bounds, _, window: &mut Window, _: &mut App| {
2847 let pad = 8.0;
2848 let aw = f32::from(bounds.size.width) - 2.0 * pad;
2849 let ah = f32::from(bounds.size.height) - 2.0 * pad;
2850 if tw <= 0.0 || th <= 0.0 || aw <= 0.0 || ah <= 0.0 {
2851 return;
2852 }
2853 let scale = (aw / tw).min(ah / th).min(1.0);
2856 let ox = (f32::from(bounds.size.width) - tw * scale) / 2.0;
2857 let oy = (f32::from(bounds.size.height) - th * scale) / 2.0;
2858 let cam = Camera {
2859 x: -ox / scale,
2860 y: -oy / scale,
2861 zoom: scale,
2862 };
2863 for e in &elems {
2864 let stroke = e.stroke.map_or(ink, u32_to_hsla);
2865 let fill = e.fill.map(u32_to_hsla);
2866 paint_element(&e.kind, None, cam, bounds.origin, stroke, fill, window);
2867 }
2868 },
2869 )
2870 .size_full();
2871 div()
2872 .id(("wb-template", index))
2873 .flex()
2874 .flex_col()
2875 .items_center()
2876 .gap(px(5.0))
2877 .p(px(6.0))
2878 .rounded(px(8.0))
2879 .hover(|s| s.bg(grid))
2880 .child(
2881 div()
2882 .w(px(150.0))
2883 .h(px(104.0))
2884 .rounded(px(6.0))
2885 .bg(bg)
2886 .border_1()
2887 .border_color(grid)
2888 .child(preview),
2889 )
2890 .child(
2891 div()
2892 .w(px(150.0))
2893 .h(px(15.0))
2894 .overflow_hidden()
2895 .text_size(px(11.0))
2896 .text_color(text)
2897 .child(name),
2898 )
2899 .on_click(
2900 cx.listener(move |this, _ev, window, cx| this.apply_template(index, window, cx)),
2901 )
2902 .on_mouse_down(
2903 MouseButton::Right,
2904 cx.listener(move |this, _ev, window, cx| this.delete_template(id, window, cx)),
2905 )
2906 .into_any_element()
2907 }
2908
2909 fn seed_color(&self, target: PickerTarget) -> u32 {
2914 let from_sel = self
2915 .selected_single()
2916 .and_then(|id| self.scene.elements.iter().find(|e| e.id == id))
2917 .and_then(|e| match target {
2918 PickerTarget::Stroke => e.stroke,
2919 PickerTarget::Fill => e.fill,
2920 PickerTarget::Text => e.label_color,
2921 });
2922 let active = match target {
2923 PickerTarget::Stroke => self.active_stroke,
2924 PickerTarget::Fill => self.active_fill,
2925 PickerTarget::Text => self.active_text,
2926 };
2927 from_sel.or(active).unwrap_or(0x4080f0ff)
2928 }
2929
2930 fn seed_picker(&mut self, target: PickerTarget) {
2932 let c = self.seed_color(target);
2933 let (h, s, v) = u32_to_hsv(c);
2934 self.picker = Some(Picker {
2935 target,
2936 h,
2937 s,
2938 v,
2939 a: u32_alpha(c),
2940 });
2941 }
2942
2943 fn toggle_picker(&mut self, cx: &mut Context<Self>) {
2946 self.open_group = None;
2947 self.templates_open = false;
2948 self.width_open = false;
2949 self.font_open = false;
2950 if self.picker.is_some() {
2951 self.picker = None;
2952 } else {
2953 self.seed_picker(PickerTarget::Stroke);
2954 }
2955 cx.notify();
2956 }
2957
2958 fn toggle_width(&mut self, cx: &mut Context<Self>) {
2960 self.picker = None;
2961 self.open_group = None;
2962 self.templates_open = false;
2963 self.context_menu = None;
2964 self.font_open = false;
2965 self.width_open = !self.width_open;
2966 cx.notify();
2967 }
2968
2969 fn set_width_live(&mut self, w: f32, cx: &mut Context<Self>) {
2973 self.active_width = w;
2974 if !self.selected.is_empty() {
2975 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
2976 let sel = self.selected.clone();
2977 for e in self.scene.elements.iter_mut() {
2978 if sel.contains(&e.id) {
2979 set_kind_width(&mut e.kind, w / zoom);
2980 }
2981 }
2982 self.dirty = true;
2983 }
2984 cx.notify();
2985 }
2986
2987 fn set_width(&mut self, w: f32, window: &mut Window, cx: &mut Context<Self>) {
2990 self.width_open = false;
2991 if !self.selected.is_empty() {
2992 self.push_undo();
2993 }
2994 self.set_width_live(w, cx);
2995 self.flush(window, cx);
2996 }
2997
2998 fn width_from_frac(frac: f32) -> f32 {
3000 let w = WIDTH_MIN + frac.clamp(0.0, 1.0) * (WIDTH_MAX - WIDTH_MIN);
3001 (w * 2.0).round() / 2.0
3002 }
3003
3004 fn toggle_group(&mut self, group: ToolGroup, cx: &mut Context<Self>) {
3007 self.picker = None;
3008 self.templates_open = false;
3009 self.width_open = false;
3010 self.font_open = false;
3011 self.open_group = if self.open_group == Some(group) {
3012 None
3013 } else {
3014 Some(group)
3015 };
3016 cx.notify();
3017 }
3018
3019 fn toggle_templates(&mut self, cx: &mut Context<Self>) {
3021 self.picker = None;
3022 self.open_group = None;
3023 self.width_open = false;
3024 self.context_menu = None;
3025 self.font_open = false;
3026 self.templates_open = !self.templates_open;
3027 cx.notify();
3028 }
3029
3030 fn toggle_font(&mut self, cx: &mut Context<Self>) {
3033 self.picker = None;
3034 self.open_group = None;
3035 self.width_open = false;
3036 self.templates_open = false;
3037 self.context_menu = None;
3038 self.font_open = !self.font_open;
3039 cx.notify();
3040 }
3041
3042 fn set_picker_target(&mut self, target: PickerTarget, cx: &mut Context<Self>) {
3045 if self.picker.map(|p| p.target) != Some(target) {
3046 self.seed_picker(target);
3047 cx.notify();
3048 }
3049 }
3050
3051 fn picker_target(&self) -> PickerTarget {
3053 self.picker.map_or(PickerTarget::Stroke, |p| p.target)
3054 }
3055
3056 fn set_color_live(&mut self, color: Option<u32>, cx: &mut Context<Self>) {
3060 let target = self.picker_target();
3061 match target {
3062 PickerTarget::Stroke => self.active_stroke = color,
3063 PickerTarget::Fill => self.active_fill = color,
3064 PickerTarget::Text => self.active_text = color,
3065 }
3066 if !self.selected.is_empty() {
3067 let sel = self.selected.clone();
3068 for e in self.scene.elements.iter_mut() {
3069 if !sel.contains(&e.id) {
3070 continue;
3071 }
3072 match target {
3073 PickerTarget::Stroke => e.stroke = color,
3074 PickerTarget::Fill => {
3076 if is_closed_shape(&e.kind) {
3077 e.fill = color;
3078 }
3079 }
3080 PickerTarget::Text => {
3081 if is_closed_shape(&e.kind) {
3082 e.label_color = color;
3083 }
3084 }
3085 }
3086 }
3087 self.dirty = true;
3088 }
3089 cx.notify();
3090 }
3091
3092 fn pick_color(&mut self, color: Option<u32>, window: &mut Window, cx: &mut Context<Self>) {
3095 if !self.selected.is_empty() {
3096 self.push_undo();
3097 }
3098 if let (Some(c), Some(p)) = (color, self.picker.as_mut()) {
3099 let (h, s, v) = u32_to_hsv(c);
3100 if s > 0.0 {
3102 p.h = h;
3103 }
3104 p.s = s;
3105 p.v = v;
3106 p.a = u32_alpha(c);
3107 }
3108 self.set_color_live(color, cx);
3109 self.flush(window, cx);
3110 }
3111
3112 fn sv_from_pos(&self, pos: Point<Pixels>) -> (f32, f32) {
3114 let b = self.sv_bounds.get();
3115 let w = f32::from(b.size.width).max(1.0);
3116 let h = f32::from(b.size.height).max(1.0);
3117 let s = ((f32::from(pos.x) - f32::from(b.origin.x)) / w).clamp(0.0, 1.0);
3118 let v = 1.0 - ((f32::from(pos.y) - f32::from(b.origin.y)) / h).clamp(0.0, 1.0);
3119 (s, v)
3120 }
3121
3122 fn frac_x(&self, bounds: Bounds<Pixels>, pos: Point<Pixels>) -> f32 {
3124 let w = f32::from(bounds.size.width).max(1.0);
3125 ((f32::from(pos.x) - f32::from(bounds.origin.x)) / w).clamp(0.0, 1.0)
3126 }
3127
3128 fn picker_u32(&self) -> Option<u32> {
3130 self.picker.map(|p| hsva_to_u32(p.h, p.s, p.v, p.a))
3131 }
3132
3133 fn event_to_world(&self, p: Point<Pixels>) -> [f32; 2] {
3135 let (rx, ry) = self.relative(p);
3136 let (wx, wy) = self.scene.camera.screen_to_world(rx, ry);
3137 [wx, wy]
3138 }
3139
3140 fn handle_hit(&self, pos: Point<Pixels>) -> Option<HandleGrab> {
3145 let cam = self.scene.camera;
3146 let origin = self.bounds.get().origin;
3147 let cursor = self.event_to_world(pos);
3148 let near = |wx: f32, wy: f32, ox: f32, oy: f32| {
3149 let s = to_screen(wx, wy, cam, origin);
3150 let (dx, dy) = (
3151 f32::from(pos.x) - (f32::from(s.x) + ox),
3152 f32::from(pos.y) - (f32::from(s.y) + oy),
3153 );
3154 dx * dx + dy * dy <= HANDLE_GRAB * HANDLE_GRAB
3155 };
3156
3157 if self.selected.len() > 1 {
3160 let bb = self.selection_bbox()?;
3161 if self.group_rotatable() {
3162 let (rx, ry) = rotate_handle_for_bbox(bb, cam, origin);
3163 let (dx, dy) = (f32::from(pos.x) - rx, f32::from(pos.y) - ry);
3164 if dx * dx + dy * dy <= HANDLE_GRAB * HANDLE_GRAB {
3165 return Some(HandleGrab::Rotate);
3166 }
3167 }
3168 let wc = [(bb.0, bb.1), (bb.2, bb.1), (bb.0, bb.3), (bb.2, bb.3)];
3169 let collect_orig = || -> Vec<(u64, ElementKind)> {
3170 self.scene
3171 .elements
3172 .iter()
3173 .filter(|e| self.is_selected(e.id))
3174 .map(|e| (e.id, e.kind.clone()))
3175 .collect()
3176 };
3177 for i in 0..4 {
3178 if near(wc[i].0, wc[i].1, 0.0, 0.0) {
3179 let opp = wc[3 - i];
3180 return Some(HandleGrab::GroupCorner(GroupResizing {
3181 handle: ResizeHandle::Corner,
3182 anchor: [opp.0, opp.1],
3183 from: [wc[i].0, wc[i].1],
3184 grab: [wc[i].0 - cursor[0], wc[i].1 - cursor[1]],
3185 orig: collect_orig(),
3186 }));
3187 }
3188 }
3189 let (mx, my) = ((bb.0 + bb.2) / 2.0, (bb.1 + bb.3) / 2.0);
3192 let edges = [
3193 (ResizeHandle::EdgeX, [bb.0, my], (0.0, 0.0), [bb.2, my]),
3194 (ResizeHandle::EdgeX, [bb.2, my], (0.0, 0.0), [bb.0, my]),
3195 (ResizeHandle::EdgeY, [mx, bb.1], (0.0, 0.0), [mx, bb.3]),
3196 (ResizeHandle::EdgeY, [mx, bb.3], (0.0, 0.0), [mx, bb.1]),
3197 ];
3198 for (handle, from, (ox, oy), anchor) in edges {
3199 if near(from[0], from[1], ox, oy) {
3200 return Some(HandleGrab::GroupCorner(GroupResizing {
3201 handle,
3202 anchor,
3203 from,
3204 grab: [from[0] - cursor[0], from[1] - cursor[1]],
3205 orig: collect_orig(),
3206 }));
3207 }
3208 }
3209 return None;
3210 }
3211
3212 let id = self.selected_single()?;
3213 let kind = &self.scene.elements.iter().find(|e| e.id == id)?.kind;
3214
3215 if rotatable(kind) {
3217 let (rx, ry) = rotate_handle_screen(kind, cam, origin);
3218 let (dx, dy) = (f32::from(pos.x) - rx, f32::from(pos.y) - ry);
3219 if dx * dx + dy * dy <= HANDLE_GRAB * HANDLE_GRAB {
3220 return Some(HandleGrab::Rotate);
3221 }
3222 }
3223
3224 if let ElementKind::Line(s) | ElementKind::Arrow(s) = kind {
3225 for (which, (wx, wy)) in [(s.x1, s.y1), (s.x2, s.y2)].into_iter().enumerate() {
3226 if near(wx, wy, 0.0, 0.0) {
3227 return Some(HandleGrab::Endpoint(EndpointDrag { id, which }));
3228 }
3229 }
3230 return None;
3231 }
3232
3233 if let Some((x, y, w, h, rot)) = box_like(kind) {
3238 let cu = box_padded_corners(x, y, w, h, rot, 0.0);
3239 let cp = cu;
3240 let center = [x + w / 2.0, y + h / 2.0];
3241 let rotated = rot.abs() > ROT_EPS;
3242 for i in 0..4 {
3243 if near(cp[i][0], cp[i][1], 0.0, 0.0) {
3244 let anchor = if rotated { center } else { cu[(i + 2) % 4] };
3245 return Some(HandleGrab::Corner(Resizing {
3246 id,
3247 handle: ResizeHandle::Corner,
3248 anchor,
3249 from: cu[i],
3250 grab: [cu[i][0] - cursor[0], cu[i][1] - cursor[1]],
3251 orig: kind.clone(),
3252 }));
3253 }
3254 }
3255 if !rotated && !matches!(kind, ElementKind::Text(_)) {
3260 let mid = |a: [f32; 2], b: [f32; 2]| [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
3261 if let Some(r) = self.edge_handle_hit(
3262 id,
3263 kind,
3264 &near,
3265 cursor,
3266 mid(cu[0], cu[1]),
3267 mid(cu[1], cu[2]),
3268 mid(cu[2], cu[3]),
3269 mid(cu[3], cu[0]),
3270 ) {
3271 return Some(r);
3272 }
3273 }
3274 return None;
3275 }
3276
3277 let bb = bbox(kind);
3279 let wc = [(bb.0, bb.1), (bb.2, bb.1), (bb.0, bb.3), (bb.2, bb.3)];
3280 for i in 0..4 {
3281 if near(wc[i].0, wc[i].1, 0.0, 0.0) {
3282 let opp = wc[3 - i];
3283 return Some(HandleGrab::Corner(Resizing {
3284 id,
3285 handle: ResizeHandle::Corner,
3286 anchor: [opp.0, opp.1],
3287 from: [wc[i].0, wc[i].1],
3288 grab: [wc[i].0 - cursor[0], wc[i].1 - cursor[1]],
3289 orig: kind.clone(),
3290 }));
3291 }
3292 }
3293 let (mx, my) = ((bb.0 + bb.2) / 2.0, (bb.1 + bb.3) / 2.0);
3295 self.edge_handle_hit(
3296 id,
3297 kind,
3298 &near,
3299 cursor,
3300 [mx, bb.1],
3301 [bb.2, my],
3302 [mx, bb.3],
3303 [bb.0, my],
3304 )
3305 }
3306
3307 #[allow(clippy::too_many_arguments)]
3311 fn edge_handle_hit(
3312 &self,
3313 id: u64,
3314 kind: &ElementKind,
3315 near: &dyn Fn(f32, f32, f32, f32) -> bool,
3316 cursor: [f32; 2],
3317 top: [f32; 2],
3318 right: [f32; 2],
3319 bottom: [f32; 2],
3320 left: [f32; 2],
3321 ) -> Option<HandleGrab> {
3322 let edges = [
3323 (ResizeHandle::EdgeY, top, (0.0, 0.0), bottom),
3324 (ResizeHandle::EdgeY, bottom, (0.0, 0.0), top),
3325 (ResizeHandle::EdgeX, right, (0.0, 0.0), left),
3326 (ResizeHandle::EdgeX, left, (0.0, 0.0), right),
3327 ];
3328 for (handle, from, (ox, oy), anchor) in edges {
3329 if near(from[0], from[1], ox, oy) {
3330 return Some(HandleGrab::Corner(Resizing {
3331 id,
3332 handle,
3333 anchor,
3334 from,
3335 grab: [from[0] - cursor[0], from[1] - cursor[1]],
3336 orig: kind.clone(),
3337 }));
3338 }
3339 }
3340 None
3341 }
3342
3343 fn connector_at(&self, pos: Point<Pixels>) -> Option<ConnectPoint> {
3347 let origin = self.bounds.get().origin;
3348 let near_px = CONNECTOR_BUTTON_SIZE * 0.65;
3349 let (sx, sy) = (f32::from(pos.x), f32::from(pos.y));
3350 let id = self.selected_single()?;
3351 let element = self
3352 .scene
3353 .elements
3354 .iter()
3355 .find(|element| element.id == id && connector_capable(&element.kind))?;
3356 let points = connector_points(&element.kind);
3357 let buttons = connector_button_centers(&element.kind, self.scene.camera, origin);
3358 buttons.into_iter().enumerate().find_map(|(index, button)| {
3359 let dx = f32::from(button.x) - sx;
3360 let dy = f32::from(button.y) - sy;
3361 (dx * dx + dy * dy <= near_px * near_px).then_some(ConnectPoint {
3362 id,
3363 index,
3364 pos: points[index],
3365 })
3366 })
3367 }
3368
3369 fn update_hover_connector(&mut self, pos: Point<Pixels>, cx: &mut Context<Self>) {
3371 let next = if self.tool == Tool::Select
3372 && self.editing.is_none()
3373 && self.pending.is_none()
3374 && self.connecting.is_none()
3375 && self.drag_from.is_none()
3376 && self.resizing.is_none()
3377 && self.group_resizing.is_none()
3378 && self.endpoint.is_none()
3379 && self.rotating.is_none()
3380 && self.marquee.is_none()
3381 {
3382 self.connector_at(pos)
3383 } else {
3384 None
3385 };
3386 if self.hovered_connector != next {
3387 self.hovered_connector = next;
3388 cx.notify();
3389 }
3390 }
3391
3392 fn text_at(&self, p: [f32; 2], pad: f32) -> Option<u64> {
3394 self.scene
3395 .elements
3396 .iter()
3397 .rev()
3398 .find(|e| matches!(e.kind, ElementKind::Text(_)) && hit_test(&e.kind, p[0], p[1], pad))
3399 .map(|e| e.id)
3400 }
3401
3402 fn shape_at(&self, p: [f32; 2], pad: f32) -> Option<u64> {
3405 self.scene
3406 .elements
3407 .iter()
3408 .rev()
3409 .find(|e| is_closed_shape(&e.kind) && hit_test(&e.kind, p[0], p[1], pad))
3410 .map(|e| e.id)
3411 }
3412
3413 fn embed_at(&self, p: [f32; 2], pad: f32) -> Option<(u64, i64)> {
3415 self.scene
3416 .elements
3417 .iter()
3418 .rev()
3419 .find_map(|e| match &e.kind {
3420 ElementKind::Embed(em) if hit_test(&e.kind, p[0], p[1], pad) => {
3421 Some((e.id, em.page_id))
3422 }
3423 _ => None,
3424 })
3425 }
3426
3427 fn snap_connector_at(
3431 &self,
3432 pos: Point<Pixels>,
3433 source_id: u64,
3434 ) -> Option<(ConnectPoint, bool)> {
3435 const SHOW_DISTANCE_PX: f32 = 64.0;
3436 const SNAP_DISTANCE_PX: f32 = 20.0;
3437 let origin = self.bounds.get().origin;
3438 let (sx, sy) = (f32::from(pos.x), f32::from(pos.y));
3439 let world = self.event_to_world(pos);
3440 let target = self
3441 .scene
3442 .elements
3443 .iter()
3444 .rev()
3445 .filter(|element| element.id != source_id && connector_capable(&element.kind))
3446 .find(|element| {
3447 hit_test(
3448 &element.kind,
3449 world[0],
3450 world[1],
3451 SHOW_DISTANCE_PX / self.scene.camera.zoom.max(MIN_ZOOM),
3452 )
3453 })?;
3454 connector_points(&target.kind)
3455 .into_iter()
3456 .enumerate()
3457 .map(|(index, point)| {
3458 let screen = to_screen(point[0], point[1], self.scene.camera, origin);
3459 let dx = f32::from(screen.x) - sx;
3460 let dy = f32::from(screen.y) - sy;
3461 let distance_sq = dx * dx + dy * dy;
3462 (
3463 distance_sq,
3464 ConnectPoint {
3465 id: target.id,
3466 index,
3467 pos: point,
3468 },
3469 )
3470 })
3471 .min_by(|a, b| a.0.total_cmp(&b.0))
3472 .map(|(distance_sq, connector)| {
3473 (
3474 connector,
3475 distance_sq <= SNAP_DISTANCE_PX * SNAP_DISTANCE_PX,
3476 )
3477 })
3478 }
3479
3480 fn on_left_down(&mut self, ev: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
3481 if self.panning {
3482 return;
3483 }
3484 if self.read_only {
3485 self.panning = true;
3486 self.last = ev.position;
3487 return;
3488 }
3489
3490 if self.toolbar_grip_bounds.get().contains(&ev.position) {
3495 self.start_toolbar_drag(ev.position, ev.click_count >= 2, window, cx);
3496 return;
3497 }
3498 if self.toolbar_bounds.get().contains(&ev.position) {
3499 return;
3500 }
3501
3502 if self.context_menu.take().is_some() {
3505 cx.notify();
3506 return;
3507 }
3508 if self.open_group.is_some() {
3511 self.open_group = None;
3512 cx.notify();
3513 return;
3514 }
3515 if self.font_open {
3517 self.font_open = false;
3518 cx.notify();
3519 return;
3520 }
3521 if self.width_open {
3526 let pos = ev.position;
3527 if self.width_bounds.get().contains(&pos) {
3528 if !self.selected.is_empty() {
3529 self.push_undo();
3530 }
3531 self.picker_drag = Some(PickerDrag::Width);
3532 let w = Self::width_from_frac(self.frac_x(self.width_bounds.get(), pos));
3533 self.set_width_live(w, cx);
3534 return;
3535 }
3536 if self.width_panel_bounds.get().contains(&pos) {
3537 return;
3538 }
3539 self.width_open = false;
3540 cx.notify();
3541 return;
3542 }
3543
3544 if self.picker.is_some() {
3549 let pos = ev.position;
3550 if self.sv_bounds.get().contains(&pos) {
3551 if !self.selected.is_empty() {
3552 self.push_undo();
3553 }
3554 self.picker_drag = Some(PickerDrag::Sv);
3555 let (s, v) = self.sv_from_pos(pos);
3556 if let Some(p) = self.picker.as_mut() {
3557 (p.s, p.v) = (s, v);
3558 }
3559 if let Some(c) = self.picker_u32() {
3560 self.set_color_live(Some(c), cx);
3561 }
3562 return;
3563 }
3564 if self.hue_bounds.get().contains(&pos) {
3565 if !self.selected.is_empty() {
3566 self.push_undo();
3567 }
3568 self.picker_drag = Some(PickerDrag::Hue);
3569 let h = self.frac_x(self.hue_bounds.get(), pos);
3570 if let Some(p) = self.picker.as_mut() {
3571 p.h = h;
3572 }
3573 if let Some(c) = self.picker_u32() {
3574 self.set_color_live(Some(c), cx);
3575 }
3576 return;
3577 }
3578 if self.alpha_bounds.get().contains(&pos) {
3579 if !self.selected.is_empty() {
3580 self.push_undo();
3581 }
3582 self.picker_drag = Some(PickerDrag::Alpha);
3583 let a = self.frac_x(self.alpha_bounds.get(), pos);
3584 if let Some(p) = self.picker.as_mut() {
3585 p.a = a;
3586 }
3587 if let Some(c) = self.picker_u32() {
3588 self.set_color_live(Some(c), cx);
3589 }
3590 return;
3591 }
3592 if self.picker_bounds.get().contains(&pos) {
3593 return;
3594 }
3595 self.picker = None;
3596 cx.notify();
3597 return;
3598 }
3599
3600 self.focus.focus(window, cx);
3603
3604 let p = self.event_to_world(ev.position);
3605 let zoom = self.scene.camera.zoom.max(MIN_ZOOM);
3606
3607 if let Some(id) = self.editing {
3610 if self.point_in_editing_text(id, p) {
3611 self.place_caret_from_click(id, p, ev, window, cx);
3612 return;
3613 }
3614 self.commit_text(window, cx);
3615 }
3616
3617 if ev.modifiers.control {
3621 self.panning = true;
3622 self.last = ev.position;
3623 return;
3624 }
3625
3626 if ev.click_count >= 2 {
3627 self.pending = None;
3628 if let Some(id) = self.text_at(p, SELECT_PAD / zoom) {
3631 self.selected = vec![id];
3632 self.editing = Some(id);
3633 self.place_caret_from_click(id, p, ev, window, cx);
3634 return;
3635 }
3636 if let Some(id) = self.shape_at(p, SELECT_PAD / zoom) {
3637 self.selected = vec![id];
3638 self.editing = Some(id);
3639 self.place_caret_from_click(id, p, ev, window, cx);
3640 return;
3641 }
3642 if self.tool == Tool::Select {
3643 if let Some((id, page_id)) = self.embed_at(p, SELECT_PAD / zoom) {
3645 self.selected = vec![id];
3646 if let Some(f) = self.on_open.clone() {
3647 f(page_id, window, cx);
3648 }
3649 cx.notify();
3650 return;
3651 }
3652 }
3653 self.reset_view(cx);
3654 return;
3655 }
3656
3657 if self.tool != Tool::Select {
3662 let pad = SELECT_PAD / zoom;
3663 let hit = self
3664 .scene
3665 .elements
3666 .iter()
3667 .rev()
3668 .find(|element| hit_test(&element.kind, p[0], p[1], pad))
3669 .map(|element| element.id);
3670 if let Some(id) = hit {
3671 self.tool = Tool::Select;
3672 if ev.modifiers.shift {
3673 if let Some(index) = self.selected.iter().position(|&selected| selected == id) {
3674 self.selected.remove(index);
3675 } else {
3676 self.selected.push(id);
3677 }
3678 } else {
3679 self.selected = vec![id];
3680 }
3681 self.drag_from = None;
3682 cx.notify();
3683 return;
3684 }
3685 }
3686
3687 if self.tool == Tool::Pan {
3690 self.panning = true;
3691 self.last = ev.position;
3692 return;
3693 }
3694
3695 if self.tool == Tool::Text {
3696 if let Some(id) = self.text_at(p, SELECT_PAD / zoom) {
3700 self.selected = vec![id];
3701 cx.notify();
3702 } else {
3703 self.push_undo();
3704 let id = self.next_id;
3705 self.next_id += 1;
3706 self.scene.elements.push(Element {
3707 id,
3708 kind: ElementKind::Text(TextGeom {
3709 x: p[0],
3710 y: p[1],
3711 content: String::new(),
3712 size: TEXT_SIZE / zoom,
3713 rotation: 0.0,
3714 measured_w: 0.0,
3715 measured_h: 0.0,
3716 }),
3717 stroke: self.active_stroke,
3718 fill: None,
3719 label: None,
3720 label_color: None,
3721 styles: Vec::new(),
3722 mindmap: None,
3723 });
3724 self.selected = vec![id];
3725 self.begin_text_edit(id, 0, window, cx);
3726 self.dirty = true;
3727 cx.notify();
3728 }
3729 return;
3730 }
3731
3732 if self.tool == Tool::MindMap {
3733 self.add_mindmap_seed(p[0], p[1], cx);
3734 return;
3735 }
3736
3737 if self.tool == Tool::Flowchart {
3738 self.add_flowchart_seed(p[0], p[1], cx);
3739 return;
3740 }
3741
3742 if self.tool == Tool::Embed {
3743 if let Some(f) = self.on_place_embed.clone() {
3745 f(p[0], p[1], window, cx);
3746 }
3747 return;
3748 }
3749
3750 if self.tool == Tool::Image {
3751 if let Some(f) = self.on_place_image.clone() {
3753 f(p[0], p[1], window, cx);
3754 }
3755 return;
3756 }
3757
3758 if self.tool == Tool::Select {
3759 if let Some(cp) = self.connector_at(ev.position) {
3762 let width = self.active_width / zoom;
3763 self.pending = Some(Pending {
3764 anchor: cp.pos,
3765 kind: ElementKind::Arrow(SegGeom {
3766 x1: cp.pos[0],
3767 y1: cp.pos[1],
3768 x2: cp.pos[0],
3769 y2: cp.pos[1],
3770 width,
3771 style: SegmentStyle::Solid,
3772 start_anchor: Some(SegmentAnchor {
3773 element_id: cp.id,
3774 connector: cp.index,
3775 }),
3776 end_anchor: None,
3777 }),
3778 });
3779 self.connecting = Some(ConnectDrag { from: cp });
3780 self.hovered_connector = Some(cp);
3781 cx.notify();
3782 return;
3783 }
3784 if let Some(grab) = self.handle_hit(ev.position) {
3786 self.push_undo();
3787 match grab {
3788 HandleGrab::Corner(rs) => self.resizing = Some(rs),
3789 HandleGrab::GroupCorner(gr) => self.group_resizing = Some(gr),
3790 HandleGrab::Endpoint(ep) => self.endpoint = Some(ep),
3791 HandleGrab::Rotate => {
3792 if let Some(bb) = self.selection_bbox() {
3798 let center = [(bb.0 + bb.2) / 2.0, (bb.1 + bb.3) / 2.0];
3799 let base = match self.selected_single() {
3800 Some(id) => self
3801 .scene
3802 .elements
3803 .iter()
3804 .find(|e| e.id == id)
3805 .and_then(|e| reference_angle(&e.kind)),
3806 None => self
3807 .scene
3808 .elements
3809 .iter()
3810 .filter(|e| self.is_selected(e.id))
3811 .find_map(|e| reference_angle(&e.kind))
3812 .or(Some(0.0)),
3813 };
3814 let start_pointer = (p[1] - center[1]).atan2(p[0] - center[0]);
3815 self.rotating = Some(Rotating {
3816 center,
3817 start_pointer,
3818 applied: 0.0,
3819 base,
3820 });
3821 }
3822 }
3823 }
3824 cx.notify();
3825 return;
3826 }
3827 let pad = SELECT_PAD / zoom;
3829 let hit = self
3830 .scene
3831 .elements
3832 .iter()
3833 .rev()
3834 .find(|e| hit_test(&e.kind, p[0], p[1], pad))
3835 .map(|e| e.id);
3836 match hit {
3837 Some(id) if ev.modifiers.shift => {
3838 if let Some(pos) = self.selected.iter().position(|&s| s == id) {
3840 self.selected.remove(pos);
3841 } else {
3842 self.selected.push(id);
3843 }
3844 self.drag_from = None;
3845 }
3846 Some(id) => {
3847 if !self.is_selected(id) {
3851 self.selected = vec![id];
3852 }
3853 self.drag_from = Some(p);
3854 self.move_origin = self
3857 .selected
3858 .first()
3859 .and_then(|&pid| self.scene.elements.iter().find(|e| e.id == pid))
3860 .map(|e| {
3861 let (x, y, ..) = bbox(&e.kind);
3862 [x, y]
3863 })
3864 .unwrap_or(p);
3865 self.moved = false;
3866 }
3867 None => {
3868 if !ev.modifiers.shift {
3870 self.selected.clear();
3871 }
3872 self.marquee = Some((p, p));
3873 self.drag_from = None;
3874 }
3875 }
3876 cx.notify();
3877 return;
3878 }
3879
3880 let width = self.active_width / zoom;
3881 let anchor = if ev.modifiers.alt {
3884 [snap_grid(p[0]), snap_grid(p[1])]
3885 } else {
3886 p
3887 };
3888 let box0 = BoxGeom {
3890 x: anchor[0],
3891 y: anchor[1],
3892 w: 0.0,
3893 h: 0.0,
3894 width,
3895 rotation: 0.0,
3896 };
3897 let kind = match self.tool {
3898 Tool::Pen => ElementKind::Draw(Stroke {
3900 points: vec![p],
3901 width,
3902 }),
3903 Tool::Rect => ElementKind::Rect(box0),
3904 Tool::Ellipse => ElementKind::Ellipse(box0),
3905 Tool::Diamond => ElementKind::Diamond(box0),
3906 Tool::Triangle => ElementKind::Triangle(box0),
3907 Tool::RoundRect => ElementKind::RoundRect(box0),
3908 Tool::Star => ElementKind::Star(box0),
3909 Tool::Hexagon => ElementKind::Hexagon(box0),
3910 Tool::Line => ElementKind::Line(SegGeom {
3911 x1: anchor[0],
3912 y1: anchor[1],
3913 x2: anchor[0],
3914 y2: anchor[1],
3915 width,
3916 style: SegmentStyle::Solid,
3917 start_anchor: None,
3918 end_anchor: None,
3919 }),
3920 Tool::Arrow => ElementKind::Arrow(SegGeom {
3921 x1: anchor[0],
3922 y1: anchor[1],
3923 x2: anchor[0],
3924 y2: anchor[1],
3925 width,
3926 style: SegmentStyle::Solid,
3927 start_anchor: None,
3928 end_anchor: None,
3929 }),
3930 Tool::DashedArrow => ElementKind::Arrow(SegGeom {
3931 x1: anchor[0],
3932 y1: anchor[1],
3933 x2: anchor[0],
3934 y2: anchor[1],
3935 width,
3936 style: SegmentStyle::Dashed,
3937 start_anchor: None,
3938 end_anchor: None,
3939 }),
3940 Tool::Pan
3942 | Tool::Select
3943 | Tool::Text
3944 | Tool::MindMap
3945 | Tool::Flowchart
3946 | Tool::Embed
3947 | Tool::Image => return,
3948 };
3949 self.pending = Some(Pending { anchor, kind });
3950 cx.notify();
3951 }
3952
3953 fn on_left_up(&mut self, _ev: &MouseUpEvent, window: &mut Window, cx: &mut Context<Self>) {
3954 if self.toolbar_drag.is_some() {
3956 self.commit_toolbar_drag(window, cx);
3957 return;
3958 }
3959 if self.text_selecting {
3961 self.text_selecting = false;
3962 return;
3963 }
3964 if self.panning {
3966 self.panning = false;
3967 cx.notify();
3968 self.flush(window, cx);
3969 return;
3970 }
3971 if self.picker_drag.take().is_some() {
3973 self.flush(window, cx);
3974 return;
3975 }
3976 if self.resizing.take().is_some()
3977 || self.group_resizing.take().is_some()
3978 || self.endpoint.take().is_some()
3979 || self.rotating.take().is_some()
3980 {
3981 self.dirty = true;
3982 cx.notify();
3983 self.flush(window, cx);
3984 return;
3985 }
3986 if self.drag_from.take().is_some() {
3987 if self.moved {
3988 self.dirty = true;
3989 }
3990 self.moved = false;
3991 self.alignment_guides = AlignmentGuides::default();
3992 cx.notify();
3993 self.flush(window, cx);
3994 return;
3995 }
3996 if let Some((a, b)) = self.marquee.take() {
3998 let (x0, x1) = (a[0].min(b[0]), a[0].max(b[0]));
3999 let (y0, y1) = (a[1].min(b[1]), a[1].max(b[1]));
4000 for e in &self.scene.elements {
4001 let bb = bbox(&e.kind);
4002 let hits = bb.0 <= x1 && bb.2 >= x0 && bb.1 <= y1 && bb.3 >= y0;
4003 if hits && !self.selected.contains(&e.id) {
4004 self.selected.push(e.id);
4005 }
4006 }
4007 cx.notify();
4008 return;
4009 }
4010 if let Some(pending) = self.pending.take() {
4011 let completed_connection = self.connecting.take().is_some();
4012 if committable(&pending.kind) {
4013 self.push_undo();
4014 let id = self.next_id;
4015 self.next_id += 1;
4016 let fill = if is_closed_shape(&pending.kind) {
4018 self.active_fill
4019 } else {
4020 None
4021 };
4022 self.scene.elements.push(Element {
4023 id,
4024 kind: pending.kind,
4025 stroke: self.active_stroke,
4026 fill,
4027 label: None,
4028 label_color: self.active_text,
4029 styles: Vec::new(),
4030 mindmap: None,
4031 });
4032 if completed_connection {
4033 self.selected = vec![id];
4036 self.focus.focus(window, cx);
4037 }
4038 self.dirty = true;
4039 }
4040 cx.notify();
4041 }
4042 self.flush(window, cx);
4043 }
4044
4045 fn on_right_down(&mut self, ev: &MouseDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
4048 if self.read_only {
4049 self.context_menu = None;
4050 cx.notify();
4051 return;
4052 }
4053 if self.picker.is_some() && self.picker_bounds.get().contains(&ev.position) {
4056 return;
4057 }
4058 if self.selected.is_empty() && self.on_paste.is_none() {
4061 self.context_menu = None;
4062 } else {
4063 let b = self.bounds.get();
4064 self.context_menu = Some(point(
4065 ev.position.x - b.origin.x,
4066 ev.position.y - b.origin.y,
4067 ));
4068 self.ctx_text_sub = false;
4069 }
4070 cx.notify();
4071 }
4072
4073 fn try_paste(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
4077 if let Some(f) = self.on_paste.clone()
4078 && let Some(json) = f(window, cx)
4079 {
4080 self.paste_elements(&json, window, cx);
4081 true
4082 } else {
4083 false
4084 }
4085 }
4086
4087 fn paste_from_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4089 self.context_menu = None;
4090 self.try_paste(window, cx);
4091 }
4092
4093 fn on_middle_down(
4094 &mut self,
4095 ev: &MouseDownEvent,
4096 _window: &mut Window,
4097 _cx: &mut Context<Self>,
4098 ) {
4099 if self.pending.is_some()
4100 || self.drag_from.is_some()
4101 || self.resizing.is_some()
4102 || self.group_resizing.is_some()
4103 || self.endpoint.is_some()
4104 || self.rotating.is_some()
4105 || self.picker_drag.is_some()
4106 || self.marquee.is_some()
4107 {
4108 return;
4109 }
4110 self.panning = true;
4111 self.last = ev.position;
4112 }
4113
4114 fn on_middle_up(&mut self, _ev: &MouseUpEvent, window: &mut Window, cx: &mut Context<Self>) {
4115 if self.panning {
4116 self.panning = false;
4117 cx.notify();
4118 }
4119 self.flush(window, cx);
4120 }
4121
4122 fn on_move(&mut self, ev: &MouseMoveEvent, _window: &mut Window, cx: &mut Context<Self>) {
4123 if self.toolbar_drag.is_some() {
4125 self.drag_toolbar(ev.position, cx);
4126 return;
4127 }
4128 if self.text_selecting
4131 && let Some(id) = self.editing
4132 && let Some(tg) = self.edit_target(id)
4133 {
4134 let local = block_local(
4135 tg.x,
4136 tg.y,
4137 tg.rotation,
4138 tg.pivot,
4139 self.event_to_world(ev.position),
4140 );
4141 self.caret = self
4142 .font
4143 .index_at_wrapped(&tg.content, tg.size, tg.wrap, local);
4144 cx.notify();
4145 return;
4146 }
4147 if let Some(conn) = self.connecting {
4150 let target = self.snap_connector_at(ev.position, conn.from.id);
4151 let cur = if let Some((target, snapped)) = target {
4152 self.hovered_connector = Some(target);
4153 if snapped {
4154 target.pos
4155 } else {
4156 self.event_to_world(ev.position)
4157 }
4158 } else {
4159 self.hovered_connector = None;
4160 self.event_to_world(ev.position)
4161 };
4162 if let Some(pending) = self.pending.as_mut()
4163 && let ElementKind::Line(s) | ElementKind::Arrow(s) = &mut pending.kind
4164 {
4165 s.x1 = conn.from.pos[0];
4166 s.y1 = conn.from.pos[1];
4167 s.x2 = cur[0];
4168 s.y2 = cur[1];
4169 s.start_anchor = Some(SegmentAnchor {
4170 element_id: conn.from.id,
4171 connector: conn.from.index,
4172 });
4173 s.end_anchor = target.and_then(|(target, snapped)| {
4174 snapped.then_some(SegmentAnchor {
4175 element_id: target.id,
4176 connector: target.index,
4177 })
4178 });
4179 }
4180 cx.notify();
4181 return;
4182 }
4183 if let Some(drag) = self.picker_drag {
4186 let pos = ev.position;
4187 if drag == PickerDrag::Width {
4188 let w = Self::width_from_frac(self.frac_x(self.width_bounds.get(), pos));
4189 self.set_width_live(w, cx);
4190 return;
4191 }
4192 match drag {
4193 PickerDrag::Sv => {
4194 let (s, v) = self.sv_from_pos(pos);
4195 if let Some(p) = self.picker.as_mut() {
4196 (p.s, p.v) = (s, v);
4197 }
4198 }
4199 PickerDrag::Hue => {
4200 let h = self.frac_x(self.hue_bounds.get(), pos);
4201 if let Some(p) = self.picker.as_mut() {
4202 p.h = h;
4203 }
4204 }
4205 PickerDrag::Alpha => {
4206 let a = self.frac_x(self.alpha_bounds.get(), pos);
4207 if let Some(p) = self.picker.as_mut() {
4208 p.a = a;
4209 }
4210 }
4211 PickerDrag::Width => unreachable!("handled above"),
4212 }
4213 if let Some(c) = self.picker_u32() {
4214 self.set_color_live(Some(c), cx);
4215 }
4216 return;
4217 }
4218 if let Some(mut rot) = self.rotating.take() {
4220 let cur = self.event_to_world(ev.position);
4221 let ang = (cur[1] - rot.center[1]).atan2(cur[0] - rot.center[0]);
4222 let mut total = ang - rot.start_pointer;
4223 match rot.base {
4224 Some(base) => total = snap_angle(base + total, ev.modifiers.shift) - base,
4228 None => {
4230 if ev.modifiers.shift {
4231 let step = std::f32::consts::PI / 12.0;
4232 total = (total / step).round() * step;
4233 }
4234 }
4235 }
4236 let tau = std::f32::consts::TAU;
4239 let mut delta = total - rot.applied;
4240 delta -= (delta / tau).round() * tau;
4241 let sel = self.selected.clone();
4244 for e in self.scene.elements.iter_mut() {
4245 if sel.contains(&e.id) {
4246 rotate_element(&mut e.kind, rot.center[0], rot.center[1], delta);
4247 }
4248 }
4249 self.sync_segment_anchors_for(&sel);
4250 rot.applied += delta;
4251 self.rotating = Some(rot);
4252 cx.notify();
4253 return;
4254 }
4255 if let Some(gr) = self.group_resizing.take() {
4259 let cur = self.event_to_world(ev.position);
4260 let mut target = [cur[0] + gr.grab[0], cur[1] + gr.grab[1]];
4261 if ev.modifiers.alt {
4262 target = [snap_grid(target[0]), snap_grid(target[1])];
4263 }
4264 let (sx, sy) = match gr.handle {
4267 ResizeHandle::Corner => {
4268 let s = diagonal_scale(gr.anchor, gr.from, target);
4269 (s, s)
4270 }
4271 ResizeHandle::EdgeX => (axis_scale(gr.anchor[0], gr.from[0], target[0]), 1.0),
4272 ResizeHandle::EdgeY => (1.0, axis_scale(gr.anchor[1], gr.from[1], target[1])),
4273 };
4274 let font = self.font.clone();
4275 for (id, orig) in &gr.orig {
4276 let mut kind = orig.clone();
4277 resize_about(&mut kind, gr.anchor[0], gr.anchor[1], sx, sy);
4278 if let ElementKind::Text(t) = &mut kind {
4279 let (w, h) = font.measure(&t.content, t.size);
4280 (t.measured_w, t.measured_h) = (w, h);
4281 }
4282 if let Some(e) = self.scene.elements.iter_mut().find(|e| e.id == *id) {
4283 e.kind = kind;
4284 }
4285 }
4286 let changed: Vec<u64> = gr.orig.iter().map(|(id, _)| *id).collect();
4287 self.sync_segment_anchors_for(&changed);
4288 self.group_resizing = Some(gr);
4289 cx.notify();
4290 return;
4291 }
4292 if let Some(r) = self.resizing.as_ref() {
4294 let (id, handle, anchor, from, grab, mut kind) =
4295 (r.id, r.handle, r.anchor, r.from, r.grab, r.orig.clone());
4296 let cur = self.event_to_world(ev.position);
4297 let mut target = [cur[0] + grab[0], cur[1] + grab[1]];
4301 if ev.modifiers.alt {
4302 target = [snap_grid(target[0]), snap_grid(target[1])];
4303 }
4304 let (sx, sy) = match handle {
4305 ResizeHandle::EdgeX => (axis_scale(anchor[0], from[0], target[0]), 1.0),
4308 ResizeHandle::EdgeY => (1.0, axis_scale(anchor[1], from[1], target[1])),
4309 ResizeHandle::Corner => {
4316 let rotated = box_like(&kind).is_some_and(|(.., r)| r.abs() > ROT_EPS);
4317 let proportional = ev.modifiers.shift
4318 || rotated
4319 || matches!(kind, ElementKind::Text(_) | ElementKind::Image(_));
4320 if proportional {
4321 let s = diagonal_scale(anchor, from, target);
4322 (s, s)
4323 } else {
4324 (
4325 axis_scale(anchor[0], from[0], target[0]),
4326 axis_scale(anchor[1], from[1], target[1]),
4327 )
4328 }
4329 }
4330 };
4331 resize_about(&mut kind, anchor[0], anchor[1], sx, sy);
4332 let font = self.font.clone();
4333 if let Some(e) = self.scene.elements.iter_mut().find(|e| e.id == id) {
4334 e.kind = kind;
4335 if let ElementKind::Text(t) = &mut e.kind {
4337 let (w, h) = font.measure(&t.content, t.size);
4338 t.measured_w = w;
4339 t.measured_h = h;
4340 }
4341 }
4342 self.sync_segment_anchors_for(&[id]);
4343 cx.notify();
4344 return;
4345 }
4346 if let Some(ep) = self.endpoint {
4349 let cur = self.event_to_world(ev.position);
4350 let shift = ev.modifiers.shift;
4351 if let Some(e) = self.scene.elements.iter_mut().find(|e| e.id == ep.id)
4352 && let ElementKind::Line(s) | ElementKind::Arrow(s) = &mut e.kind
4353 {
4354 let (ox, oy) = if ep.which == 0 {
4355 (s.x2, s.y2)
4356 } else {
4357 (s.x1, s.y1)
4358 };
4359 let (nx, ny) = if shift {
4360 snap_45(ox, oy, cur[0], cur[1])
4361 } else if ev.modifiers.alt {
4362 (snap_grid(cur[0]), snap_grid(cur[1]))
4363 } else {
4364 (cur[0], cur[1])
4365 };
4366 if ep.which == 0 {
4367 s.x1 = nx;
4368 s.y1 = ny;
4369 s.start_anchor = None;
4370 } else {
4371 s.x2 = nx;
4372 s.y2 = ny;
4373 s.end_anchor = None;
4374 }
4375 }
4376 if !ev.modifiers.shift && !ev.modifiers.alt {
4377 if let Some((target, snapped)) = self.snap_connector_at(ev.position, ep.id)
4378 && snapped
4379 {
4380 self.hovered_connector = Some(target);
4381 self.set_segment_endpoint_anchor(
4382 ep.id,
4383 ep.which,
4384 Some(SegmentAnchor {
4385 element_id: target.id,
4386 connector: target.index,
4387 }),
4388 );
4389 } else {
4390 self.hovered_connector = None;
4391 }
4392 } else {
4393 self.hovered_connector = None;
4394 }
4395 cx.notify();
4396 return;
4397 }
4398 if let Some(from) = self.drag_from {
4405 let cur = self.event_to_world(ev.position);
4406 let target = move_target(self.move_origin, from, cur, ev.modifiers.alt);
4407 let cur_min = self
4410 .selected
4411 .first()
4412 .and_then(|&pid| self.scene.elements.iter().find(|e| e.id == pid))
4413 .map(|e| {
4414 let (x, y, ..) = bbox(&e.kind);
4415 [x, y]
4416 })
4417 .unwrap_or(self.move_origin);
4418 let (raw_dx, raw_dy) = (target[0] - cur_min[0], target[1] - cur_min[1]);
4419 let (dx, dy, guides) = if ev.modifiers.alt {
4420 (raw_dx, raw_dy, AlignmentGuides::default())
4421 } else {
4422 self.aligned_move_delta(raw_dx, raw_dy)
4423 };
4424 self.alignment_guides = guides;
4425 if dx != 0.0 || dy != 0.0 {
4426 if !self.moved {
4427 self.push_undo();
4428 self.moved = true;
4429 }
4430 let sel = self.selected.clone();
4431 self.detach_segment_bindings_for_move(&sel);
4432 for e in self.scene.elements.iter_mut() {
4433 if sel.contains(&e.id) {
4434 translate(&mut e.kind, dx, dy);
4435 }
4436 }
4437 self.sync_segment_anchors_for(&sel);
4438 cx.notify();
4439 }
4440 return;
4441 }
4442 if let Some((start, _)) = self.marquee {
4444 let cur = self.event_to_world(ev.position);
4445 self.marquee = Some((start, cur));
4446 cx.notify();
4447 return;
4448 }
4449 if self.pending.is_some() {
4451 let cur = self.event_to_world(ev.position);
4452 let z = self.scene.camera.zoom.max(MIN_ZOOM);
4453 let Some(pending) = self.pending.as_mut() else {
4454 return;
4455 };
4456 let anchor = pending.anchor;
4457 let c = if ev.modifiers.alt {
4460 [snap_grid(cur[0]), snap_grid(cur[1])]
4461 } else {
4462 cur
4463 };
4464 match &mut pending.kind {
4465 ElementKind::Draw(s) => {
4466 if let Some(last) = s.points.last() {
4467 let (ddx, ddy) = ((cur[0] - last[0]) * z, (cur[1] - last[1]) * z);
4468 if ddx * ddx + ddy * ddy < MIN_POINT_PX * MIN_POINT_PX {
4469 return;
4470 }
4471 }
4472 s.points.push(cur);
4473 }
4474 ElementKind::Rect(b)
4475 | ElementKind::Ellipse(b)
4476 | ElementKind::Diamond(b)
4477 | ElementKind::Triangle(b)
4478 | ElementKind::RoundRect(b)
4479 | ElementKind::Star(b)
4480 | ElementKind::Hexagon(b) => {
4481 b.x = anchor[0].min(c[0]);
4482 b.y = anchor[1].min(c[1]);
4483 b.w = (c[0] - anchor[0]).abs();
4484 b.h = (c[1] - anchor[1]).abs();
4485 }
4486 ElementKind::Line(s) | ElementKind::Arrow(s) => {
4487 s.x2 = c[0];
4488 s.y2 = c[1];
4489 }
4490 ElementKind::Text(_) | ElementKind::Embed(_) | ElementKind::Image(_) => {}
4492 }
4493 cx.notify();
4494 return;
4495 }
4496 self.update_hover_connector(ev.position, cx);
4497 if self.panning {
4499 let dx = f32::from(ev.position.x - self.last.x);
4500 let dy = f32::from(ev.position.y - self.last.y);
4501 self.last = ev.position;
4502 self.scene.camera.pan_by(dx, dy);
4503 self.dirty = true;
4504 cx.notify();
4505 }
4506 }
4507
4508 fn on_scroll(&mut self, ev: &ScrollWheelEvent, _window: &mut Window, cx: &mut Context<Self>) {
4509 let (dx, dy) = match ev.delta {
4510 ScrollDelta::Pixels(p) => (f32::from(p.x), f32::from(p.y)),
4511 ScrollDelta::Lines(p) => (p.x * LINE_PX, p.y * LINE_PX),
4512 };
4513 if ev.modifiers.platform || ev.modifiers.control {
4514 let (rx, ry) = self.relative(ev.position);
4515 let factor = (1.0 + dy * 0.0025).clamp(0.5, 2.0);
4516 self.scene.camera.zoom_about(rx, ry, factor);
4517 } else {
4518 self.scene.camera.pan_by(dx, dy);
4519 }
4520 self.dirty = true;
4521 cx.notify();
4522 }
4523
4524 fn on_pinch(&mut self, ev: &PinchEvent, _window: &mut Window, cx: &mut Context<Self>) {
4525 let (rx, ry) = self.relative(ev.position);
4526 self.scene.camera.zoom_about(rx, ry, 1.0 + ev.delta);
4527 self.dirty = true;
4528 cx.notify();
4529 }
4530
4531 fn relative(&self, p: Point<Pixels>) -> (f32, f32) {
4533 let o = self.bounds.get().origin;
4534 (f32::from(p.x - o.x), f32::from(p.y - o.y))
4535 }
4536
4537 fn edit_target(&self, id: u64) -> Option<EditTarget> {
4544 let e = self.scene.elements.iter().find(|e| e.id == id)?;
4545 match &e.kind {
4546 ElementKind::Text(t) => Some(EditTarget {
4547 content: t.content.clone(),
4548 size: t.size,
4549 wrap: None,
4550 x: t.x,
4551 y: t.y,
4552 rotation: t.rotation,
4553 pivot: [t.x + t.measured_w / 2.0, t.y + t.measured_h / 2.0],
4554 }),
4555 kind if is_closed_shape(kind) => {
4556 let (bx, by, bw, bh, rot) = box_like(kind)?;
4557 let label = e.label.clone().unwrap_or_default();
4558 let blk = shape_label_block(&self.font, kind, bx, by, bw, bh, &label);
4559 Some(EditTarget {
4560 content: label,
4561 size: blk.size,
4562 wrap: Some(blk.wrap),
4563 x: blk.x,
4564 y: blk.y,
4565 rotation: rot,
4566 pivot: [bx + bw / 2.0, by + bh / 2.0],
4567 })
4568 }
4569 _ => None,
4570 }
4571 }
4572
4573 fn sel_range(&self) -> (usize, usize) {
4576 (
4577 self.caret.min(self.sel_anchor),
4578 self.caret.max(self.sel_anchor),
4579 )
4580 }
4581
4582 fn move_caret(&mut self, to: usize, extend: bool, cx: &mut Context<Self>) {
4585 self.caret = to;
4586 if !extend {
4587 self.sel_anchor = to;
4588 }
4589 self.pending_style = None; cx.notify();
4591 }
4592
4593 fn replace_range(&mut self, id: u64, s: usize, e: usize, ins: &str, cx: &mut Context<Self>) {
4596 let pending = self.pending_style;
4597 let Some(el) = self.scene.elements.iter_mut().find(|el| el.id == id) else {
4598 return;
4599 };
4600 let insert_style = pending.unwrap_or_else(|| style_at(&el.styles, s.saturating_sub(1)));
4603 let edited = if let ElementKind::Text(t) = &mut el.kind {
4605 t.content.replace_range(s..e, ins);
4606 true
4607 } else if is_closed_shape(&el.kind) {
4608 el.label
4609 .get_or_insert_with(String::new)
4610 .replace_range(s..e, ins);
4611 true
4612 } else {
4613 false
4614 };
4615 if edited {
4616 el.styles = splice_styles(&el.styles, s, e, ins.len(), insert_style);
4618 self.caret = s + ins.len();
4619 self.sel_anchor = self.caret;
4620 self.marked_range = None;
4621 self.dirty = true;
4622 cx.notify();
4623 }
4624 }
4625
4626 fn replace_selection(&mut self, id: u64, ins: &str, cx: &mut Context<Self>) {
4628 let (s, e) = self.sel_range();
4629 self.replace_range(id, s, e, ins, cx);
4630 }
4631
4632 fn editing_content(&self) -> Option<String> {
4633 self.editing
4634 .and_then(|id| self.edit_target(id).map(|tg| tg.content))
4635 }
4636
4637 fn utf16_to_utf8_in(text: &str, offset: usize) -> usize {
4639 let mut utf8_offset = 0;
4640 let mut utf16_count = 0;
4641
4642 for ch in text.chars() {
4643 if utf16_count >= offset {
4644 break;
4645 }
4646 utf16_count += ch.len_utf16();
4647 utf8_offset += ch.len_utf8();
4648 }
4649
4650 utf8_offset
4651 }
4652
4653 fn utf8_to_utf16_in(text: &str, offset: usize) -> usize {
4654 let mut utf16_offset = 0;
4655 let mut utf8_count = 0;
4656
4657 for ch in text.chars() {
4658 if utf8_count >= offset {
4659 break;
4660 }
4661 utf8_count += ch.len_utf8();
4662 utf16_offset += ch.len_utf16();
4663 }
4664
4665 utf16_offset
4666 }
4667
4668 fn utf16_range_to_utf8_in(text: &str, range_utf16: &Range<usize>) -> Range<usize> {
4669 Self::utf16_to_utf8_in(text, range_utf16.start)
4670 ..Self::utf16_to_utf8_in(text, range_utf16.end)
4671 }
4672
4673 fn utf8_range_to_utf16_in(text: &str, range: &Range<usize>) -> Range<usize> {
4674 Self::utf8_to_utf16_in(text, range.start)..Self::utf8_to_utf16_in(text, range.end)
4675 }
4676
4677 fn replace_text_in_visible_range(
4681 &mut self,
4682 visible_range: Range<usize>,
4683 new_text: &str,
4684 selected_range_relative: Option<Range<usize>>,
4685 mark_inserted_text: bool,
4686 cx: &mut Context<Self>,
4687 ) {
4688 let Some(id) = self.editing else {
4689 return;
4690 };
4691 let insert_start = visible_range.start;
4692 self.replace_range(id, visible_range.start, visible_range.end, new_text, cx);
4693
4694 self.marked_range = if mark_inserted_text && !new_text.is_empty() {
4695 Some(insert_start..insert_start + new_text.len())
4696 } else {
4697 None
4698 };
4699 let selected_range = selected_range_relative
4700 .map(|relative| insert_start + relative.start..insert_start + relative.end);
4701 self.caret = selected_range
4702 .as_ref()
4703 .map(|range| range.end)
4704 .unwrap_or(insert_start + new_text.len());
4705 self.sel_anchor = selected_range
4706 .as_ref()
4707 .map(|range| range.start)
4708 .unwrap_or(self.caret);
4709 cx.notify();
4710 }
4711
4712 fn selection_style(&self) -> RunStyle {
4716 let Some(id) = self.editing else {
4717 return RunStyle::default();
4718 };
4719 let (s, e) = self.sel_range();
4720 if s >= e
4721 && let Some(p) = self.pending_style
4722 {
4723 return p;
4724 }
4725 self.scene
4726 .elements
4727 .iter()
4728 .find(|el| el.id == id)
4729 .map_or(RunStyle::default(), |el| active_style(&el.styles, s, e))
4730 }
4731
4732 fn apply_format(&mut self, format: Format, cx: &mut Context<Self>) {
4735 let Some(id) = self.editing else {
4736 return;
4737 };
4738 let (s, e) = self.sel_range();
4739 if s < e {
4740 if let Some(el) = self.scene.elements.iter_mut().find(|el| el.id == id) {
4741 el.styles = toggle_format(&el.styles, s, e, format);
4742 self.dirty = true;
4743 }
4744 } else {
4745 let mut p = self.selection_style();
4746 let on = !format.get(&p);
4747 format.set(&mut p, on);
4748 self.pending_style = Some(p);
4749 }
4750 cx.notify();
4751 }
4752
4753 fn apply_highlight(&mut self, color: u32, cx: &mut Context<Self>) {
4755 let Some(id) = self.editing else {
4756 return;
4757 };
4758 let (s, e) = self.sel_range();
4759 if s < e {
4760 if let Some(el) = self.scene.elements.iter_mut().find(|el| el.id == id) {
4761 el.styles = toggle_highlight(&el.styles, s, e, color);
4762 self.dirty = true;
4763 }
4764 } else {
4765 let mut p = self.selection_style();
4766 p.highlight = (p.highlight != Some(color)).then_some(color);
4767 self.pending_style = Some(p);
4768 }
4769 cx.notify();
4770 }
4771
4772 fn format_menu(
4776 &self,
4777 ink: Hsla,
4778 text: Hsla,
4779 grid: Hsla,
4780 bg: Hsla,
4781 cx: &mut Context<Self>,
4782 ) -> Div {
4783 let st = self.selection_style();
4784 let frow = |id: &'static str, label: &'static str, sc: &'static str, on: bool| {
4785 div()
4786 .id(id)
4787 .flex()
4788 .items_center()
4789 .gap(px(8.0))
4790 .px(px(10.0))
4791 .py(px(5.0))
4792 .mx(px(4.0))
4793 .rounded(px(6.0))
4794 .text_size(px(12.0))
4795 .text_color(ink)
4796 .hover(|s| s.bg(grid))
4797 .child(div().w(px(12.0)).child(if on { "✓" } else { "" }))
4798 .child(div().flex_1().child(label))
4799 .child(div().text_size(px(11.0)).text_color(text).child(sc))
4800 };
4801 div()
4802 .min_w(px(184.0))
4803 .py(px(4.0))
4804 .rounded(px(8.0))
4805 .bg(bg)
4806 .shadow_lg()
4807 .border_1()
4808 .border_color(grid)
4809 .flex()
4810 .flex_col()
4811 .child(
4812 frow("wb-fmt-bold", "Bold", "⌘B", st.bold)
4813 .on_click(cx.listener(|this, _ev, _w, cx| this.apply_format(Format::Bold, cx))),
4814 )
4815 .child(
4816 frow("wb-fmt-italic", "Italic", "⌘I", st.italic).on_click(
4817 cx.listener(|this, _ev, _w, cx| this.apply_format(Format::Italic, cx)),
4818 ),
4819 )
4820 .child(
4821 frow("wb-fmt-underline", "Underline", "⌘U", st.underline).on_click(
4822 cx.listener(|this, _ev, _w, cx| this.apply_format(Format::Underline, cx)),
4823 ),
4824 )
4825 .child(
4826 frow("wb-fmt-strike", "Strikethrough", "⇧⌘X", st.strike).on_click(
4827 cx.listener(|this, _ev, _w, cx| this.apply_format(Format::Strike, cx)),
4828 ),
4829 )
4830 .child(
4831 frow(
4832 "wb-fmt-highlight",
4833 "Highlight",
4834 "⇧⌘H",
4835 st.highlight.is_some(),
4836 )
4837 .on_click(
4838 cx.listener(|this, _ev, _w, cx| this.apply_highlight(HIGHLIGHT_DEFAULT, cx)),
4839 ),
4840 )
4841 }
4842
4843 fn caret_vertical(&self, content: &str, size: f32, wrap: Option<f32>, dir: i32) -> usize {
4846 let pos = self.font.caret_pos_wrapped(content, size, wrap, self.caret);
4847 let lh = self.font.measure("", size).1.max(1.0);
4848 let y = (pos[1] + dir as f32 * lh + lh * 0.5).max(0.0);
4850 self.font.index_at_wrapped(content, size, wrap, [pos[0], y])
4851 }
4852
4853 fn text_edit_key(&mut self, ev: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
4857 let Some(id) = self.editing else {
4858 return;
4859 };
4860 let Some(tg) = self.edit_target(id) else {
4861 self.commit_text(window, cx);
4862 return;
4863 };
4864 let (content, size, wrap) = (tg.content, tg.size, tg.wrap);
4865 self.caret = floor_boundary(&content, self.caret);
4867 self.sel_anchor = floor_boundary(&content, self.sel_anchor);
4868 let ks = &ev.keystroke;
4869 if ks.is_ime_in_progress() {
4870 return;
4871 }
4872 let cmd = ks.modifiers.platform || ks.modifiers.control;
4873 let shift = ks.modifiers.shift;
4874
4875 if ks.key == "escape" {
4876 self.commit_text(window, cx);
4877 return;
4878 }
4879 if cmd {
4880 match ks.key.as_str() {
4881 "a" => {
4882 self.sel_anchor = 0;
4883 self.caret = content.len();
4884 cx.notify();
4885 }
4886 "b" => self.apply_format(Format::Bold, cx),
4887 "i" => self.apply_format(Format::Italic, cx),
4888 "u" => self.apply_format(Format::Underline, cx),
4889 "x" if shift => self.apply_format(Format::Strike, cx),
4890 "h" if shift => self.apply_highlight(HIGHLIGHT_DEFAULT, cx),
4891 "c" | "x" => {
4892 let (s, e) = self.sel_range();
4893 if s < e {
4894 cx.write_to_clipboard(gpui::ClipboardItem::new_string(
4895 content[s..e].into(),
4896 ));
4897 if ks.key == "x" {
4898 self.replace_range(id, s, e, "", cx);
4899 }
4900 }
4901 }
4902 "v" => {
4903 if let Some(text) = cx.read_from_clipboard().and_then(|c| c.text()) {
4904 self.replace_selection(id, &text, cx);
4905 }
4906 }
4907 _ => cx.propagate(), }
4909 return;
4910 }
4911
4912 if ks.key == "tab" && self.is_mindmap_node(id) {
4913 self.commit_text(window, cx);
4914 self.add_mindmap_relative(id, false, window, cx);
4915 return;
4916 }
4917 if ks.key == "enter" && self.is_mindmap_node(id) {
4918 self.commit_text(window, cx);
4919 self.add_mindmap_relative(id, true, window, cx);
4920 return;
4921 }
4922
4923 match ks.key.as_str() {
4924 "left" => {
4925 let (s, e) = self.sel_range();
4926 let to = if !shift && s < e {
4927 s
4928 } else {
4929 caret_left(&content, self.caret)
4930 };
4931 self.move_caret(to, shift, cx);
4932 }
4933 "right" => {
4934 let (s, e) = self.sel_range();
4935 let to = if !shift && s < e {
4936 e
4937 } else {
4938 caret_right(&content, self.caret)
4939 };
4940 self.move_caret(to, shift, cx);
4941 }
4942 "up" => {
4943 let to = self.caret_vertical(&content, size, wrap, -1);
4944 self.move_caret(to, shift, cx);
4945 }
4946 "down" => {
4947 let to = self.caret_vertical(&content, size, wrap, 1);
4948 self.move_caret(to, shift, cx);
4949 }
4950 "home" => self.move_caret(line_start(&content, self.caret), shift, cx),
4951 "end" => self.move_caret(line_end(&content, self.caret), shift, cx),
4952 "backspace" => {
4953 let (s, e) = self.sel_range();
4954 if s < e {
4955 self.replace_range(id, s, e, "", cx);
4956 } else if self.caret > 0 {
4957 self.replace_range(id, caret_left(&content, self.caret), self.caret, "", cx);
4958 }
4959 }
4960 "delete" => {
4961 let (s, e) = self.sel_range();
4962 if s < e {
4963 self.replace_range(id, s, e, "", cx);
4964 } else if self.caret < content.len() {
4965 self.replace_range(id, self.caret, caret_right(&content, self.caret), "", cx);
4966 }
4967 }
4968 "enter" => self.replace_selection(id, "\n", cx),
4969 "tab" => cx.propagate(),
4970 _ => {
4971 if ks
4976 .key_char
4977 .as_deref()
4978 .is_none_or(|c| c.chars().next().is_none_or(|ch| ch.is_control()))
4979 {
4980 cx.propagate();
4981 }
4982 }
4983 }
4984 }
4985
4986 fn begin_text_edit(&mut self, id: u64, at: usize, window: &mut Window, cx: &mut Context<Self>) {
4988 self.editing = Some(id);
4989 self.caret = at;
4990 self.sel_anchor = at;
4991 self.marked_range = None;
4992 self.focus.focus(window, cx);
4993 }
4994
4995 fn point_in_editing_text(&self, id: u64, p: [f32; 2]) -> bool {
4997 let pad = SELECT_PAD / self.scene.camera.zoom.max(MIN_ZOOM);
4998 self.scene
4999 .elements
5000 .iter()
5001 .find(|e| e.id == id)
5002 .is_some_and(|e| {
5003 let (x0, y0, x1, y1) = bbox(&e.kind);
5004 p[0] >= x0 - pad && p[0] <= x1 + pad && p[1] >= y0 - pad && p[1] <= y1 + pad
5005 })
5006 }
5007
5008 fn place_caret_from_click(
5011 &mut self,
5012 id: u64,
5013 p: [f32; 2],
5014 ev: &MouseDownEvent,
5015 window: &mut Window,
5016 cx: &mut Context<Self>,
5017 ) {
5018 let Some(tg) = self.edit_target(id) else {
5019 return;
5020 };
5021 let local = block_local(tg.x, tg.y, tg.rotation, tg.pivot, p);
5022 let idx = self
5023 .font
5024 .index_at_wrapped(&tg.content, tg.size, tg.wrap, local);
5025 if ev.click_count >= 2 {
5026 let (s, e) = word_range(&tg.content, idx);
5027 self.sel_anchor = s;
5028 self.caret = e;
5029 self.text_selecting = false;
5030 } else {
5031 self.caret = idx;
5032 if !ev.modifiers.shift {
5033 self.sel_anchor = idx;
5034 }
5035 self.text_selecting = true;
5036 }
5037 self.marked_range = None;
5040 self.focus.focus(window, cx);
5041 cx.notify();
5042 }
5043
5044 fn commit_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {
5046 self.text_selecting = false;
5047 self.pending_style = None;
5048 self.marked_range = None;
5049 self.format_flyout = false;
5050 let Some(id) = self.editing.take() else {
5051 return;
5052 };
5053 if let Some(e) = self.scene.elements.iter_mut().find(|e| e.id == id)
5054 && is_closed_shape(&e.kind)
5055 && e.label.as_deref().is_none_or(|s| s.trim().is_empty())
5056 {
5057 e.label = None;
5059 }
5060 self.scene.elements.retain(|e| {
5062 e.id != id || !matches!(&e.kind, ElementKind::Text(t) if t.content.trim().is_empty())
5063 });
5064 self.dirty = true;
5065 cx.notify();
5066 self.flush(window, cx);
5067 }
5068
5069 fn handle_shortcut(
5075 &mut self,
5076 ev: &KeyDownEvent,
5077 window: &mut Window,
5078 cx: &mut Context<Self>,
5079 ) -> bool {
5080 let ks = &ev.keystroke;
5081 let cmd = ks.modifiers.platform || ks.modifiers.control;
5082 if cmd && ks.key == "z" {
5083 if ks.modifiers.shift {
5084 self.redo(window, cx);
5085 } else {
5086 self.undo(window, cx);
5087 }
5088 return true;
5089 }
5090 let close = ks.key == "]" || ks.key == "}";
5094 let open = ks.key == "[" || ks.key == "{";
5095 if cmd && (close || open) {
5096 if self.selected.is_empty() {
5097 return false;
5098 }
5099 let all_the_way = ks.modifiers.shift || ks.key == "}" || ks.key == "{";
5100 let op = match (close, all_the_way) {
5101 (true, true) => ZOrder::ToFront,
5102 (true, false) => ZOrder::Forward,
5103 (false, true) => ZOrder::ToBack,
5104 (false, false) => ZOrder::Backward,
5105 };
5106 self.reorder_selection(op, window, cx);
5107 return true;
5108 }
5109 if cmd && ks.key == "c" {
5114 self.copy_selection(window, cx);
5115 return true;
5116 }
5117 if cmd && ks.key == "x" {
5118 if self.copy_selection(window, cx) {
5119 self.delete_selected(window, cx);
5120 }
5121 return true;
5122 }
5123 if cmd && ks.key == "v" {
5124 return self.try_paste(window, cx);
5127 }
5128 if cmd || ks.modifiers.alt {
5129 return false;
5130 }
5131 if let Some(tool) = Tool::shortcut(&ks.key) {
5132 self.set_tool(tool, cx);
5133 return true;
5134 }
5135 match ks.key.as_str() {
5136 "backspace" | "delete" => self.delete_selected(window, cx),
5137 "escape" if !self.selected.is_empty() => {
5138 self.selected.clear();
5139 cx.notify();
5140 }
5141 _ => return false,
5142 }
5143 true
5144 }
5145
5146 fn on_key(&mut self, ev: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
5147 if self.read_only {
5148 cx.propagate();
5149 return;
5150 }
5151 if ev.keystroke.key == "escape" && self.connecting.is_some() {
5154 self.connecting = None;
5155 self.pending = None;
5156 self.hovered_connector = None;
5157 cx.notify();
5158 return;
5159 }
5160 if ev.keystroke.key == "escape" && (self.picker.is_some() || self.templates_open) {
5163 self.picker = None;
5164 self.templates_open = false;
5165 cx.notify();
5166 return;
5167 }
5168 if self.toolbar_drag.is_some() {
5171 let ks = &ev.keystroke;
5172 if ks.key == "r" && !(ks.modifiers.platform || ks.modifiers.control || ks.modifiers.alt)
5173 {
5174 self.toggle_toolbar_orientation(window, cx);
5175 }
5176 return;
5177 }
5178 if self.editing.is_none() {
5180 if !self.handle_shortcut(ev, window, cx) {
5181 cx.propagate();
5182 }
5183 return;
5184 }
5185 self.text_edit_key(ev, window, cx);
5187 }
5188}
5189
5190impl BoardEmbedView {
5191 pub fn new(scene: Scene, style: WhiteboardStyleFn, cx: &mut Context<Self>) -> Self {
5194 let board_style = style.clone();
5195 let board = cx.new(|cx| WhiteboardView::new_read_only(scene, board_style, cx));
5196 Self {
5197 board,
5198 style,
5199 on_expand: None,
5200 }
5201 }
5202
5203 pub fn board(&self) -> Entity<WhiteboardView> {
5205 self.board.clone()
5206 }
5207
5208 pub fn set_on_expand(&mut self, f: ExpandEmbedFn) {
5210 self.on_expand = Some(f);
5211 }
5212}
5213
5214impl Render for BoardEmbedView {
5215 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5216 let st = (self.style)();
5217 let ink = st.ink;
5218 let panel = st.panel_strong;
5219 let grid = st.grid;
5220 let accent = st.accent;
5221 let button = self.on_expand.as_ref().map(|_| {
5222 div()
5223 .id("board-embed-expand")
5224 .absolute()
5225 .top(px(10.0))
5226 .right(px(10.0))
5227 .h(px(30.0))
5228 .px(px(10.0))
5229 .flex()
5230 .items_center()
5231 .justify_center()
5232 .gap(px(6.0))
5233 .rounded(px(8.0))
5234 .bg(panel)
5235 .border_1()
5236 .border_color(grid.opacity(0.5))
5237 .hover(|s| s.bg(accent))
5238 .text_size(px(12.0))
5239 .text_color(ink)
5240 .child("↗")
5241 .child("Edit")
5242 .on_click(cx.listener(|this, _ev, window, cx| {
5243 if let Some(f) = this.on_expand.clone() {
5244 f(window, cx);
5245 }
5246 }))
5247 });
5248 div()
5249 .size_full()
5250 .relative()
5251 .child(self.board.clone())
5252 .children(button)
5253 }
5254}
5255
5256impl BoardThumbnailView {
5257 pub fn new(snapshot: LocalThumbnailSnapshot, style: WhiteboardStyleFn) -> Self {
5258 Self {
5259 snapshot,
5260 style,
5261 font: Font::default(),
5262 }
5263 }
5264
5265 pub fn snapshot(&self) -> &LocalThumbnailSnapshot {
5266 &self.snapshot
5267 }
5268
5269 pub fn set_snapshot(&mut self, snapshot: LocalThumbnailSnapshot) {
5270 self.snapshot = snapshot;
5271 }
5272}
5273
5274impl Render for BoardThumbnailView {
5275 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5276 let WhiteboardStyle {
5277 bg,
5278 grid,
5279 text,
5280 ink,
5281 panel,
5282 ..
5283 } = (self.style)();
5284 let cam = self.snapshot.spec.camera;
5285 let layers = build_thumbnail_layers(
5286 &self.snapshot.scene,
5287 &self.font,
5288 cam,
5289 ink,
5290 text,
5291 grid,
5292 panel,
5293 None,
5294 None,
5295 None,
5296 );
5297 let board_layer = canvas(
5298 |_, _, _| {},
5299 move |bounds, _, window, _| paint_board(bounds, cam, bg, grid, window),
5300 )
5301 .absolute()
5302 .size_full();
5303 let element_layers: Vec<gpui::AnyElement> = layers
5304 .into_iter()
5305 .map(|l| match l {
5306 Layer::Band(es) => band_canvas(es, cam).into_any_element(),
5307 Layer::Overlay(el) => el,
5308 })
5309 .collect();
5310 div()
5311 .size_full()
5312 .relative()
5313 .overflow_hidden()
5314 .child(board_layer)
5315 .children(element_layers)
5316 }
5317}
5318
5319#[derive(Clone, Copy)]
5322enum ZOrder {
5323 ToFront,
5324 Forward,
5325 Backward,
5326 ToBack,
5327}
5328
5329fn caret_left(content: &str, at: usize) -> usize {
5333 content[..at.min(content.len())]
5334 .chars()
5335 .next_back()
5336 .map_or(0, |c| at - c.len_utf8())
5337}
5338
5339fn caret_right(content: &str, at: usize) -> usize {
5341 content[at.min(content.len())..]
5342 .chars()
5343 .next()
5344 .map_or(content.len(), |c| at + c.len_utf8())
5345}
5346
5347fn line_start(content: &str, at: usize) -> usize {
5349 content[..at.min(content.len())]
5350 .rfind('\n')
5351 .map_or(0, |i| i + 1)
5352}
5353
5354fn line_end(content: &str, at: usize) -> usize {
5356 let at = at.min(content.len());
5357 content[at..].find('\n').map_or(content.len(), |i| at + i)
5358}
5359
5360fn floor_boundary(content: &str, idx: usize) -> usize {
5363 let mut i = idx.min(content.len());
5364 while i > 0 && !content.is_char_boundary(i) {
5365 i -= 1;
5366 }
5367 i
5368}
5369
5370fn word_range(content: &str, at: usize) -> (usize, usize) {
5373 let is_word = |c: char| c.is_alphanumeric() || c == '_';
5374 let at = floor_boundary(content, at);
5375 let start = content[..at]
5376 .char_indices()
5377 .rev()
5378 .take_while(|(_, c)| is_word(*c))
5379 .last()
5380 .map_or(at, |(i, _)| i);
5381 let end = content[at..]
5382 .char_indices()
5383 .take_while(|(_, c)| is_word(*c))
5384 .last()
5385 .map_or(at, |(i, c)| at + i + c.len_utf8());
5386 (start, end)
5387}
5388
5389struct EditTarget {
5394 content: String,
5395 size: f32,
5396 wrap: Option<f32>,
5397 x: f32,
5398 y: f32,
5399 rotation: f32,
5400 pivot: [f32; 2],
5402}
5403
5404struct LabelBlock {
5407 x: f32,
5408 y: f32,
5409 size: f32,
5410 wrap: f32,
5411}
5412
5413fn glyph_style(s: RunStyle) -> font::GlyphStyle {
5415 font::GlyphStyle {
5416 bold: s.bold,
5417 italic: s.italic,
5418 underline: s.underline,
5419 strike: s.strike,
5420 highlight: s.highlight,
5421 }
5422}
5423
5424fn shape_label_block(
5429 font: &Font,
5430 kind: &ElementKind,
5431 bx: f32,
5432 by: f32,
5433 bw: f32,
5434 bh: f32,
5435 label: &str,
5436) -> LabelBlock {
5437 let (wf, hf, bottom) = match kind {
5444 ElementKind::Ellipse(_) => (
5445 std::f32::consts::FRAC_1_SQRT_2,
5446 std::f32::consts::FRAC_1_SQRT_2,
5447 false,
5448 ),
5449 ElementKind::Diamond(_) => (0.5, 0.5, false),
5450 ElementKind::Triangle(_) => (0.5, 0.5, true),
5451 ElementKind::Star(_) => (0.5, 0.4, false),
5452 ElementKind::Hexagon(_) => (0.8, 0.5, false),
5453 _ => (1.0, 1.0, false),
5454 };
5455 let wrap = (bw * wf - 2.0 * LABEL_PAD).max(1.0);
5456 let ih = (bh * hf - 2.0 * LABEL_PAD).max(1.0);
5457 let size = font.fit_size(label, wrap, ih, TEXT_SIZE);
5458 let (w, h) = font.measure_wrapped(label, size, Some(wrap));
5459 let x = bx + (bw - w) / 2.0;
5462 let y = if bottom {
5463 by + bh - LABEL_PAD - h
5464 } else {
5465 by + (bh - h) / 2.0
5466 };
5467 LabelBlock { x, y, size, wrap }
5468}
5469
5470impl WhiteboardView {
5471 fn render_read_only(&mut self, window: &Window, cx: &mut Context<Self>) -> AnyElement {
5472 let WhiteboardStyle {
5473 bg,
5474 grid,
5475 text,
5476 ink,
5477 panel,
5478 ..
5479 } = (self.style)();
5480 let camera = self.scene.camera;
5481 let bounds_cell = self.bounds.clone();
5482 let render_viewport = self.render_viewport(Some(window.viewport_size()));
5483 let visible_element_ids = self
5484 .scene
5485 .elements
5486 .iter()
5487 .filter(|element| {
5488 render_viewport.is_none_or(|viewport| viewport.intersects(bbox(&element.kind)))
5489 })
5490 .map(|element| element.id)
5491 .collect::<HashSet<_>>();
5492 self.text_layout_cache
5493 .retain(|element_id, _| visible_element_ids.contains(element_id));
5494 self.label_layout_cache
5495 .retain(|element_id, _| visible_element_ids.contains(element_id));
5496 let layers = build_thumbnail_layers(
5497 &self.scene,
5498 &self.font,
5499 camera,
5500 ink,
5501 text,
5502 grid,
5503 panel,
5504 render_viewport,
5505 Some(&mut self.text_layout_cache),
5506 Some(&mut self.label_layout_cache),
5507 );
5508 let board_layer = canvas(
5509 move |bounds, _, _| bounds_cell.set(bounds),
5510 move |bounds, _, window, _| paint_board(bounds, camera, bg, grid, window),
5511 )
5512 .absolute()
5513 .size_full();
5514 let element_layers = layers.into_iter().map(|layer| match layer {
5515 Layer::Band(elements) => band_canvas(elements, camera).into_any_element(),
5516 Layer::Overlay(element) => element,
5517 });
5518
5519 div()
5520 .size_full()
5521 .relative()
5522 .overflow_hidden()
5523 .cursor(if self.panning {
5524 CursorStyle::ClosedHand
5525 } else {
5526 CursorStyle::OpenHand
5527 })
5528 .child(board_layer)
5529 .children(element_layers)
5530 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_left_down))
5531 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_left_up))
5532 .on_mouse_down(MouseButton::Middle, cx.listener(Self::on_middle_down))
5533 .on_mouse_up(MouseButton::Middle, cx.listener(Self::on_middle_up))
5534 .on_mouse_move(cx.listener(Self::on_move))
5535 .on_pinch(cx.listener(Self::on_pinch))
5536 .child(
5537 div()
5538 .absolute()
5539 .left(px(10.0))
5540 .bottom(px(8.0))
5541 .text_size(px(11.0))
5542 .text_color(text)
5543 .child(SharedString::from(format!("{:.0}%", camera.zoom * 100.0))),
5544 )
5545 .into_any_element()
5546 }
5547}
5548
5549impl Render for WhiteboardView {
5550 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5551 if self.read_only {
5552 return self.render_read_only(window, cx);
5553 }
5554 let WhiteboardStyle {
5555 bg,
5556 grid,
5557 text,
5558 ink,
5559 panel,
5560 panel_strong,
5561 accent,
5562 selection,
5563 swatches,
5564 } = (self.style)();
5565 let cam = self.scene.camera;
5566 let zoom = cam.zoom.max(MIN_ZOOM);
5567 let bounds_cell = self.bounds.clone();
5568 let board_bounds = self.bounds.get();
5569 let render_viewport = self.render_viewport(Some(window.viewport_size()));
5570 let visible_element_ids = self
5571 .scene
5572 .elements
5573 .iter()
5574 .filter(|element| {
5575 render_viewport.is_none_or(|viewport| viewport.intersects(bbox(&element.kind)))
5576 })
5577 .map(|element| element.id)
5578 .collect::<HashSet<_>>();
5579 self.text_layout_cache
5580 .retain(|element_id, _| visible_element_ids.contains(element_id));
5581 self.label_layout_cache
5582 .retain(|element_id, _| visible_element_ids.contains(element_id));
5583
5584 let img_sources: HashMap<u64, gpui::ImageSource> = {
5593 let items: Vec<(u64, String, f32)> = self
5594 .scene
5595 .elements
5596 .iter()
5597 .filter(|element| visible_element_ids.contains(&element.id))
5598 .filter_map(|e| match &e.kind {
5599 ElementKind::Image(im) => {
5600 Some((e.id, im.src.clone(), snap_quarter(im.rotation)))
5601 }
5602 _ => None,
5603 })
5604 .collect();
5605 let mut map = HashMap::new();
5606 if let Some(f) = self.on_image.clone() {
5607 for (id, src, rot) in items {
5608 if let Some(s) = f(&src, rot, window, cx) {
5609 map.insert(id, s);
5610 }
5611 }
5612 }
5613 map
5614 };
5615
5616 let font = self.font.clone();
5625 let editing = self.editing;
5626 let (caret_at, sel_anchor) = (self.caret, self.sel_anchor);
5627 let sel_fill = gpui::hsla(selection.h, selection.s, selection.l, 0.30);
5629 let mindmap_connector_styles: HashMap<u64, MindMapConnectorStyle> = self
5630 .scene
5631 .elements
5632 .iter()
5633 .filter(|element| visible_element_ids.contains(&element.id))
5634 .filter_map(|element| {
5635 self.mindmap_connector_style_for_element(&element.kind)
5636 .map(|style| (element.id, style))
5637 })
5638 .collect();
5639 let text_layout_cache = &mut self.text_layout_cache;
5640 let label_layout_cache = &mut self.label_layout_cache;
5641 let mut layers: Vec<Layer> = Vec::new();
5642 let mut band: Vec<ElemPaint> = Vec::new();
5643 for e in self.scene.elements.iter_mut() {
5644 if !visible_element_ids.contains(&e.id) {
5645 continue;
5646 }
5647 let id = e.id;
5648 let stroke = e.stroke.map_or(ink, u32_to_hsla);
5649 let fill = e.fill.map(u32_to_hsla);
5650 let label = e.label.as_deref();
5653 let label_color = e.label_color;
5654 let styles = e.styles.as_slice();
5655 match &mut e.kind {
5656 ElementKind::Embed(em) => {
5659 if !band.is_empty() {
5660 layers.push(Layer::Band(std::mem::take(&mut band)));
5661 }
5662 layers.push(Layer::Overlay(
5663 div()
5664 .absolute()
5665 .left(px((em.x - cam.x) * zoom))
5666 .top(px((em.y - cam.y) * zoom))
5667 .w(px(em.w * zoom))
5668 .h(px(em.h * zoom))
5669 .bg(panel)
5670 .border_1()
5671 .border_color(grid)
5672 .rounded(px(8.0))
5673 .overflow_hidden()
5674 .p(px(10.0 * zoom))
5675 .flex()
5676 .flex_col()
5677 .gap(px(3.0 * zoom))
5678 .child(
5679 div()
5680 .flex()
5681 .items_center()
5682 .gap(px(6.0 * zoom))
5683 .text_size(px(14.0 * zoom))
5684 .text_color(ink)
5685 .child(div().text_color(accent).child("▤"))
5686 .child(SharedString::from(em.title.clone())),
5687 )
5688 .child(
5689 div()
5690 .text_size(px(11.0 * zoom))
5691 .text_color(text)
5692 .child("Double-click to open"),
5693 )
5694 .into_any_element(),
5695 ));
5696 }
5697 ElementKind::Image(im) => {
5701 if !band.is_empty() {
5702 layers.push(Layer::Band(std::mem::take(&mut band)));
5703 }
5704 let rot = snap_quarter(im.rotation);
5705 let (bx, by, bw, bh) = if rot.abs() < ROT_EPS {
5706 (im.x, im.y, im.w, im.h)
5707 } else {
5708 let c = box_padded_corners(im.x, im.y, im.w, im.h, rot, 0.0);
5709 let (x0, y0, x1, y1) = aabb(&c);
5710 (x0, y0, x1 - x0, y1 - y0)
5711 };
5712 let frame = div()
5713 .absolute()
5714 .left(px((bx - cam.x) * zoom))
5715 .top(px((by - cam.y) * zoom))
5716 .w(px(bw * zoom))
5717 .h(px(bh * zoom))
5718 .overflow_hidden()
5719 .rounded(px(2.0));
5720 let el = match img_sources.get(&id) {
5721 Some(src) => frame.child(
5728 gpui::img(src.clone())
5729 .w(px(bw * zoom))
5730 .object_fit(ObjectFit::Contain),
5731 ),
5732 None => frame
5733 .bg(panel)
5734 .border_1()
5735 .border_color(grid)
5736 .flex()
5737 .items_center()
5738 .justify_center()
5739 .child(
5740 div()
5741 .text_size(px(11.0 * zoom))
5742 .text_color(text)
5743 .child("Loading…"),
5744 ),
5745 };
5746 layers.push(Layer::Overlay(el.into_any_element()));
5747 }
5748 kind => {
5750 let text = if let ElementKind::Text(t) = kind {
5751 let layout = cached_text_layout(
5752 text_layout_cache,
5753 &font,
5754 id,
5755 &t.content,
5756 t.size,
5757 None,
5758 styles,
5759 );
5760 t.measured_w = layout.width;
5761 t.measured_h = layout.height;
5762 let active = editing == Some(id);
5765 let caret = active.then(|| font.caret_pos(&t.content, t.size, caret_at));
5766 let (s, e) = (caret_at.min(sel_anchor), caret_at.max(sel_anchor));
5767 let selection = if active {
5768 font.selection_rects(&t.content, t.size, s, e)
5769 } else {
5770 Vec::new()
5771 };
5772 Some(TextOutline {
5773 segs: layout.segs.clone(),
5774 bold_segs: layout.bold_segs.clone(),
5775 bold_width: layout.bold_width,
5776 color: stroke,
5777 x: t.x,
5778 y: t.y,
5779 rotation: t.rotation,
5780 pivot: [t.x + layout.width / 2.0, t.y + layout.height / 2.0],
5781 line_height: layout.line_height,
5782 caret,
5783 selection,
5784 sel_color: sel_fill,
5785 decorations: layout.decorations.clone(),
5786 })
5787 } else if is_closed_shape(kind)
5788 && let Some((bx, by, bw, bh, rot)) = box_like(kind)
5789 && (editing == Some(id) || label.is_some_and(|s| !s.trim().is_empty()))
5790 {
5791 let active = editing == Some(id);
5798 let text = label.map_or("", str::trim);
5799 let label_layout = cached_label_layout(
5800 label_layout_cache,
5801 &font,
5802 id,
5803 kind,
5804 bx,
5805 by,
5806 bw,
5807 bh,
5808 text,
5809 styles,
5810 );
5811 let caret = active.then(|| {
5812 font.caret_pos_wrapped(
5813 text,
5814 label_layout.size,
5815 Some(label_layout.wrap),
5816 caret_at,
5817 )
5818 });
5819 let (s, e) = (caret_at.min(sel_anchor), caret_at.max(sel_anchor));
5820 let selection = if active {
5821 font.selection_rects_wrapped(
5822 text,
5823 label_layout.size,
5824 Some(label_layout.wrap),
5825 s,
5826 e,
5827 )
5828 } else {
5829 Vec::new()
5830 };
5831 Some(TextOutline {
5832 segs: label_layout.text.segs.clone(),
5833 bold_segs: label_layout.text.bold_segs.clone(),
5834 bold_width: label_layout.text.bold_width,
5835 color: label_color.map_or(stroke, u32_to_hsla),
5836 x: bx + label_layout.offset_x,
5837 y: by + label_layout.offset_y,
5838 rotation: rot,
5839 pivot: [bx + bw / 2.0, by + bh / 2.0],
5840 line_height: label_layout.text.line_height,
5841 caret,
5842 selection,
5843 sel_color: sel_fill,
5844 decorations: label_layout.text.decorations.clone(),
5845 })
5846 } else {
5847 None
5848 };
5849 band.push(ElemPaint {
5850 kind: kind.clone(),
5851 stroke,
5852 fill,
5853 text,
5854 mindmap_connector_style: mindmap_connector_styles.get(&id).copied(),
5855 });
5856 }
5857 }
5858 }
5859 if !band.is_empty() {
5860 layers.push(Layer::Band(band));
5861 }
5862
5863 let pending_ink = self.active_stroke.map_or(ink, u32_to_hsla);
5865 let pending_fill = self.active_fill.map(u32_to_hsla);
5866 let pending = self.pending.as_ref().map(|p| p.kind.clone());
5867 let single_sel = self
5873 .selected_single()
5874 .filter(|id| Some(*id) != self.editing)
5875 .and_then(|id| self.scene.elements.iter().find(|e| e.id == id))
5876 .map(|e| e.kind.clone());
5877 let group_sel = (self.selected.len() > 1)
5878 .then(|| self.selection_bbox())
5879 .flatten()
5880 .map(|bb| (bb, self.group_rotatable()));
5881 let marquee = self.marquee;
5882 let alignment_guides = self.alignment_guides;
5883 let snap_target = self.connecting.and_then(|connection| {
5884 self.hovered_connector
5885 .filter(|target| target.id != connection.from.id)
5886 .and_then(|target| {
5887 self.scene
5888 .elements
5889 .iter()
5890 .find(|element| element.id == target.id)
5891 .map(|element| (element.kind.clone(), target.index))
5892 })
5893 });
5894 const CONNECTOR_ICONS: [(&str, &[u8]); 4] = [
5895 ("wb-connector-up", include_bytes!("../assets/icons/up.svg")),
5896 (
5897 "wb-connector-right",
5898 include_bytes!("../assets/icons/right.svg"),
5899 ),
5900 (
5901 "wb-connector-down",
5902 include_bytes!("../assets/icons/down.svg"),
5903 ),
5904 (
5905 "wb-connector-left",
5906 include_bytes!("../assets/icons/left.svg"),
5907 ),
5908 ];
5909 let connector_buttons: Vec<gpui::AnyElement> = single_sel
5910 .as_ref()
5911 .filter(|_| self.connecting.is_none() && self.pending.is_none())
5912 .filter(|kind| connector_capable(kind))
5913 .map(|kind| {
5914 connector_button_centers(kind, cam, board_bounds.origin)
5915 .into_iter()
5916 .enumerate()
5917 .map(|(index, center)| {
5918 let (key, bytes) = CONNECTOR_ICONS[index];
5919 div()
5920 .id(("wb-connector-button", index))
5921 .absolute()
5922 .left(
5923 center.x - board_bounds.origin.x - px(CONNECTOR_BUTTON_SIZE / 2.0),
5924 )
5925 .top(center.y - board_bounds.origin.y - px(CONNECTOR_BUTTON_SIZE / 2.0))
5926 .size(px(CONNECTOR_BUTTON_SIZE))
5927 .flex()
5928 .items_center()
5929 .justify_center()
5930 .rounded_full()
5931 .bg(panel_strong)
5932 .text_color(selection)
5933 .shadow_sm()
5934 .cursor_pointer()
5935 .child(svg_icon(key, bytes, selection, CONNECTOR_BUTTON_SIZE))
5936 .into_any_element()
5937 })
5938 .collect()
5939 })
5940 .unwrap_or_default();
5941 const ROTATE_ICON: &[u8] = include_bytes!("../assets/icons/refresh.svg");
5942 let rotate_position = single_sel
5943 .as_ref()
5944 .filter(|kind| rotatable(kind))
5945 .map(|kind| rotate_handle_screen(kind, cam, board_bounds.origin))
5946 .or_else(|| {
5947 group_sel
5948 .filter(|(_, can_rotate)| *can_rotate)
5949 .map(|(bounds, _)| rotate_handle_for_bbox(bounds, cam, board_bounds.origin))
5950 });
5951 let rotate_button = rotate_position.map(|(x, y)| {
5952 div()
5953 .id("wb-rotate-button")
5954 .absolute()
5955 .left(px(x) - board_bounds.origin.x - px(CONNECTOR_BUTTON_SIZE / 2.0))
5956 .top(px(y) - board_bounds.origin.y - px(CONNECTOR_BUTTON_SIZE / 2.0))
5957 .size(px(CONNECTOR_BUTTON_SIZE))
5958 .flex()
5959 .items_center()
5960 .justify_center()
5961 .rounded_full()
5962 .bg(panel_strong)
5963 .shadow_sm()
5964 .cursor_pointer()
5965 .child(svg_icon(
5966 "wb-icon-refresh",
5967 ROTATE_ICON,
5968 selection,
5969 CONNECTOR_BUTTON_SIZE - 4.0,
5970 ))
5971 });
5972 let selected_mindmap_root = self.selected_mindmap_root();
5973
5974 let active = self.tool;
5979 let open_group = self.open_group;
5980
5981 let tool_btn = |t: Tool| {
5985 let icon: gpui::AnyElement = match t.icon() {
5986 Some((key, bytes)) => svg_icon(key, bytes, ink, 16.0).into_any_element(),
5987 None => t.glyph().into_any_element(),
5988 };
5989 let mut b = div()
5990 .id(("wb-tool", t as usize))
5991 .size(px(30.0))
5992 .flex()
5993 .items_center()
5994 .justify_center()
5995 .rounded(px(6.0))
5996 .text_size(px(15.0))
5997 .text_color(ink)
5998 .child(icon);
5999 if t == active {
6003 b = b.bg(accent);
6004 } else {
6005 b = b.hover(|s| s.bg(grid));
6006 }
6007 b
6008 };
6009
6010 let cat_btn = |g: ToolGroup| {
6014 let shown = if g.contains(active) {
6015 active
6016 } else {
6017 g.representative()
6018 };
6019 let icon: gpui::AnyElement = match shown.icon() {
6020 Some((key, bytes)) => svg_icon(key, bytes, ink, 16.0).into_any_element(),
6021 None => shown.glyph().into_any_element(),
6022 };
6023 let mut b = div()
6024 .id(("wb-group", g as usize))
6025 .h(px(30.0))
6026 .px(px(6.0))
6027 .flex()
6028 .items_center()
6029 .justify_center()
6030 .gap(px(1.0))
6031 .rounded(px(6.0))
6032 .text_color(ink)
6033 .child(icon)
6034 .child(div().text_size(px(8.0)).text_color(text).child("▾"));
6035 if open_group == Some(g) || g.contains(active) {
6036 b = b.bg(accent);
6037 } else {
6038 b = b.hover(|s| s.bg(grid));
6039 }
6040 b
6041 };
6042
6043 let mut cats: Vec<gpui::AnyElement> = Vec::with_capacity(ToolGroup::ALL.len() + 1);
6046 for &g in ToolGroup::ALL.iter() {
6047 cats.push(
6048 cat_btn(g)
6049 .tooltip(self.tip(g.label()))
6050 .on_click(cx.listener(move |this, _ev, window, cx| {
6051 this.focus.focus(window, cx);
6052 this.toggle_group(g, cx);
6053 }))
6054 .into_any_element(),
6055 );
6056 if g == ToolGroup::Lines {
6057 cats.push(
6058 tool_btn(Tool::Text)
6059 .tooltip(self.tip(Tool::Text.label()))
6060 .on_click(cx.listener(|this, _ev, _w, cx| this.set_tool(Tool::Text, cx)))
6061 .into_any_element(),
6062 );
6063 }
6064 }
6065
6066 const UNDO_ICON: &[u8] = include_bytes!("../assets/icons/undo.svg");
6067 const REDO_ICON: &[u8] = include_bytes!("../assets/icons/redo.svg");
6068 const DELETE_ICON: &[u8] = include_bytes!("../assets/icons/delete.svg");
6069 let act = |id: usize, key: &'static str, bytes: &'static [u8]| {
6070 div()
6071 .id(("wb-act", id))
6072 .size(px(30.0))
6073 .flex()
6074 .items_center()
6075 .justify_center()
6076 .rounded(px(6.0))
6077 .hover(|s| s.bg(grid))
6078 .child(svg_icon(key, bytes, ink, 16.0))
6079 };
6080 let cur_swatch = self.active_stroke.map_or(ink, u32_to_hsla);
6082 let mut color_btn = div()
6083 .id("wb-color")
6084 .size(px(30.0))
6085 .flex()
6086 .items_center()
6087 .justify_center()
6088 .rounded(px(6.0));
6089 if self.picker.is_some() {
6090 color_btn = color_btn.bg(accent);
6091 } else {
6092 color_btn = color_btn.hover(|s| s.bg(grid));
6093 }
6094 let color_btn = color_btn
6095 .child(
6096 div()
6097 .size(px(16.0))
6098 .rounded(px(4.0))
6099 .bg(cur_swatch)
6100 .border_1()
6101 .border_color(grid),
6102 )
6103 .tooltip(self.tip("Color"))
6104 .on_click(cx.listener(|this, _ev, window, cx| {
6105 this.focus.focus(window, cx);
6106 this.toggle_picker(cx);
6107 }));
6108 let mut width_btn = div()
6111 .id("wb-width")
6112 .size(px(30.0))
6113 .flex()
6114 .items_center()
6115 .justify_center()
6116 .rounded(px(6.0));
6117 if self.width_open {
6118 width_btn = width_btn.bg(accent);
6119 } else {
6120 width_btn = width_btn.hover(|s| s.bg(grid));
6121 }
6122 let width_btn = width_btn
6123 .child(
6124 div()
6125 .w(px(16.0))
6126 .h(px(self.active_width.clamp(1.0, 8.0)))
6127 .rounded_full()
6128 .bg(cur_swatch),
6129 )
6130 .tooltip(self.tip("Thickness"))
6131 .on_click(cx.listener(|this, _ev, window, cx| {
6132 this.focus.focus(window, cx);
6133 this.toggle_width(cx);
6134 }));
6135 let font_btn = self.on_pick_font.is_some().then(|| {
6138 let mut b = div()
6139 .id("wb-font")
6140 .size(px(30.0))
6141 .flex()
6142 .items_center()
6143 .justify_center()
6144 .rounded(px(6.0));
6145 if self.font_open {
6146 b = b.bg(accent);
6147 } else {
6148 b = b.hover(|s| s.bg(grid));
6149 }
6150 b.child(div().text_size(px(15.0)).text_color(ink).child("Aa"))
6151 .tooltip(self.tip("Font"))
6152 .on_click(cx.listener(|this, _ev, window, cx| {
6153 this.focus.focus(window, cx);
6154 this.toggle_font(cx);
6155 }))
6156 });
6157 const TEMPLATES_ICON: &[u8] = include_bytes!("../assets/icons/templates.svg");
6160 let mut templates_btn = div()
6161 .id("wb-templates")
6162 .size(px(30.0))
6163 .flex()
6164 .items_center()
6165 .justify_center()
6166 .rounded(px(6.0))
6167 .child(svg_icon("wb-icon-templates", TEMPLATES_ICON, ink, 16.0));
6168 if self.templates_open {
6169 templates_btn = templates_btn.bg(accent);
6170 } else {
6171 templates_btn = templates_btn.hover(|s| s.bg(grid));
6172 }
6173 let templates_btn = templates_btn
6174 .tooltip(self.tip("Templates"))
6175 .on_click(cx.listener(|this, _ev, window, cx| {
6176 this.focus.focus(window, cx);
6177 this.toggle_templates(cx);
6178 }));
6179 let grip_cell = self.toolbar_grip_bounds.clone();
6184 let pill_cell = self.toolbar_bounds.clone();
6185 let vertical = self.toolbar_vertical;
6186 let dot_row = move || {
6187 div()
6188 .flex()
6189 .gap(px(3.0))
6190 .child(div().size(px(2.5)).rounded_full().bg(text))
6191 .child(div().size(px(2.5)).rounded_full().bg(text))
6192 };
6193 let grip = div()
6194 .id("wb-grip")
6195 .relative()
6196 .flex()
6197 .flex_col()
6198 .justify_center()
6199 .gap(px(3.0))
6200 .px(px(4.0))
6201 .h(px(30.0))
6202 .cursor(CursorStyle::OpenHand)
6203 .tooltip(self.tip("Drag to move · Tap R to flip · double-click to reset"))
6204 .child(
6205 canvas(move |b, _, _| grip_cell.set(b), |_, _, _, _| {})
6206 .absolute()
6207 .size_full(),
6208 )
6209 .child(dot_row())
6210 .child(dot_row())
6211 .child(dot_row());
6212 let format_btn = self.editing.is_some().then(|| {
6215 let mut b = div()
6216 .id("wb-format-btn")
6217 .size(px(30.0))
6218 .flex()
6219 .items_center()
6220 .justify_center()
6221 .rounded(px(6.0))
6222 .text_size(px(14.0))
6223 .text_color(ink)
6224 .tooltip(self.tip("Text formatting"))
6225 .child("A");
6226 if self.format_flyout {
6227 b = b.bg(accent);
6228 } else {
6229 b = b.hover(|s| s.bg(grid));
6230 }
6231 b.on_click(cx.listener(|this, _ev, _w, cx| {
6232 this.format_flyout = !this.format_flyout;
6233 cx.notify();
6234 }))
6235 });
6236 let mut pill = div()
6237 .relative()
6238 .flex()
6239 .items_center()
6240 .gap(px(2.0))
6241 .p(px(3.0))
6242 .rounded(px(9.0))
6243 .bg(panel);
6244 if vertical {
6245 pill = pill.flex_col();
6246 }
6247 let mut pill = pill
6248 .child(
6249 canvas(move |b, _, _| pill_cell.set(b), |_, _, _, _| {})
6250 .absolute()
6251 .size_full(),
6252 )
6253 .child(grip)
6254 .child(toolbar_divider(grid, vertical))
6255 .child(
6257 tool_btn(Tool::Pan)
6258 .tooltip(self.tip(Tool::Pan.label()))
6259 .on_click(cx.listener(|this, _ev, _w, cx| this.set_tool(Tool::Pan, cx))),
6260 )
6261 .child(
6262 tool_btn(Tool::Select)
6263 .tooltip(self.tip(Tool::Select.label()))
6264 .on_click(cx.listener(|this, _ev, _w, cx| this.set_tool(Tool::Select, cx))),
6265 );
6266 if let Some(root_id) = selected_mindmap_root {
6267 let direction = self.mindmap_root_direction(root_id);
6268 let connector_style = self.mindmap_connector_style_for_root(root_id);
6269 let chip = |id: &'static str, active: bool, icon: gpui::AnyElement| {
6270 let mut d = div()
6271 .id(id)
6272 .px(px(8.0))
6273 .h(px(24.0))
6274 .flex()
6275 .items_center()
6276 .justify_center()
6277 .rounded(px(6.0))
6278 .text_size(px(11.0))
6279 .text_color(ink);
6280 if active {
6281 d = d.bg(accent);
6282 } else {
6283 d = d.hover(|s| s.bg(grid));
6284 }
6285 d.child(icon)
6286 };
6287 let icon_color = ink;
6288 let draw_mm_icon = move |_id: &'static str, kind: &'static str| -> gpui::AnyElement {
6289 canvas(
6290 |_, _, _| {},
6291 move |bounds, _, window, _| {
6292 let w = f32::from(bounds.size.width);
6293 let h = f32::from(bounds.size.height);
6294 let ox = f32::from(bounds.origin.x);
6295 let oy = f32::from(bounds.origin.y);
6296 let p = |x: f32, y: f32| point(px(ox + x), px(oy + y));
6297 let mut stroke = |segments: &[([f32; 2], [f32; 2])]| {
6298 let mut pb = PathBuilder::stroke(px(1.75));
6299 for &([x1, y1], [x2, y2]) in segments {
6300 pb.move_to(p(x1, y1));
6301 pb.line_to(p(x2, y2));
6302 }
6303 if let Ok(path) = pb.build() {
6304 window.paint_path(path, icon_color);
6305 }
6306 };
6307 match kind {
6308 "dir-both" => stroke(&[
6309 ([3.0, h / 2.0], [w - 3.0, h / 2.0]),
6310 ([3.0, h / 2.0], [6.0, h / 2.0 - 3.0]),
6311 ([3.0, h / 2.0], [6.0, h / 2.0 + 3.0]),
6312 ([w - 3.0, h / 2.0], [w - 6.0, h / 2.0 - 3.0]),
6313 ([w - 3.0, h / 2.0], [w - 6.0, h / 2.0 + 3.0]),
6314 ]),
6315 "dir-right" => stroke(&[
6316 ([3.0, h / 2.0], [w - 3.0, h / 2.0]),
6317 ([w - 3.0, h / 2.0], [w - 6.0, h / 2.0 - 3.0]),
6318 ([w - 3.0, h / 2.0], [w - 6.0, h / 2.0 + 3.0]),
6319 ]),
6320 "dir-left" => stroke(&[
6321 ([3.0, h / 2.0], [w - 3.0, h / 2.0]),
6322 ([3.0, h / 2.0], [6.0, h / 2.0 - 3.0]),
6323 ([3.0, h / 2.0], [6.0, h / 2.0 + 3.0]),
6324 ]),
6325 "line-straight" => stroke(&[([3.0, h / 2.0], [w - 3.0, h / 2.0])]),
6326 "line-bezier" => {
6327 let mut pb = PathBuilder::stroke(px(1.75));
6328 pb.move_to(p(2.5, h - 4.0));
6329 pb.cubic_bezier_to(
6330 p(w - 2.5, 4.0),
6331 p(w * 0.35, h - 4.0),
6332 p(w * 0.65, 4.0),
6333 );
6334 if let Ok(path) = pb.build() {
6335 window.paint_path(path, icon_color);
6336 }
6337 }
6338 "line-orthogonal" => stroke(&[
6339 ([3.0, h - 4.0], [w * 0.45, h - 4.0]),
6340 ([w * 0.45, h - 4.0], [w * 0.45, 4.0]),
6341 ([w * 0.45, 4.0], [w - 3.0, 4.0]),
6342 ]),
6343 _ => {}
6344 }
6345 },
6346 )
6347 .w(px(14.0))
6348 .h(px(14.0))
6349 .into_any_element()
6350 };
6351 pill = pill
6352 .child(toolbar_divider(grid, vertical))
6353 .child(
6354 div()
6355 .px(px(4.0))
6356 .text_size(px(11.0))
6357 .text_color(text)
6358 .child("Direction"),
6359 )
6360 .child(
6361 chip(
6362 "wb-mm-dir-both",
6363 direction == MindMapRootDirection::Both,
6364 draw_mm_icon("wb-mm-dir-both-icon", "dir-both"),
6365 )
6366 .on_click(cx.listener(move |this, _ev, _window, cx| {
6367 this.set_mindmap_root_direction(root_id, MindMapRootDirection::Both, cx);
6368 })),
6369 )
6370 .child(
6371 chip(
6372 "wb-mm-dir-right",
6373 direction == MindMapRootDirection::Right,
6374 draw_mm_icon("wb-mm-dir-right-icon", "dir-right"),
6375 )
6376 .on_click(cx.listener(move |this, _ev, _window, cx| {
6377 this.set_mindmap_root_direction(root_id, MindMapRootDirection::Right, cx);
6378 })),
6379 )
6380 .child(
6381 chip(
6382 "wb-mm-dir-left",
6383 direction == MindMapRootDirection::Left,
6384 draw_mm_icon("wb-mm-dir-left-icon", "dir-left"),
6385 )
6386 .on_click(cx.listener(move |this, _ev, _window, cx| {
6387 this.set_mindmap_root_direction(root_id, MindMapRootDirection::Left, cx);
6388 })),
6389 )
6390 .child(toolbar_divider(grid, vertical))
6391 .child(
6392 div()
6393 .px(px(4.0))
6394 .text_size(px(11.0))
6395 .text_color(text)
6396 .child("Connector"),
6397 )
6398 .child(
6399 chip(
6400 "wb-mm-line-straight",
6401 connector_style == MindMapConnectorStyle::Straight,
6402 draw_mm_icon("wb-mm-line-straight-icon", "line-straight"),
6403 )
6404 .on_click(cx.listener(move |this, _ev, _window, cx| {
6405 this.set_mindmap_connector_style(
6406 root_id,
6407 MindMapConnectorStyle::Straight,
6408 cx,
6409 );
6410 })),
6411 )
6412 .child(
6413 chip(
6414 "wb-mm-line-bezier",
6415 connector_style == MindMapConnectorStyle::Bezier,
6416 draw_mm_icon("wb-mm-line-bezier-icon", "line-bezier"),
6417 )
6418 .on_click(cx.listener(move |this, _ev, _window, cx| {
6419 this.set_mindmap_connector_style(
6420 root_id,
6421 MindMapConnectorStyle::Bezier,
6422 cx,
6423 );
6424 })),
6425 )
6426 .child(
6427 chip(
6428 "wb-mm-line-orthogonal",
6429 connector_style == MindMapConnectorStyle::Orthogonal,
6430 draw_mm_icon("wb-mm-line-orthogonal-icon", "line-orthogonal"),
6431 )
6432 .on_click(cx.listener(move |this, _ev, _window, cx| {
6433 this.set_mindmap_connector_style(
6434 root_id,
6435 MindMapConnectorStyle::Orthogonal,
6436 cx,
6437 );
6438 })),
6439 );
6440 } else {
6441 pill = pill
6442 .child(color_btn)
6443 .child(width_btn)
6444 .children(font_btn)
6445 .children(format_btn)
6446 .child(toolbar_divider(grid, vertical))
6447 .children(cats)
6449 .child(templates_btn);
6450 }
6451 let pill = pill
6452 .child(toolbar_divider(grid, vertical))
6453 .child(
6455 act(0, "wb-icon-undo", UNDO_ICON)
6456 .tooltip(self.tip("Undo (⌘Z)"))
6457 .on_click(cx.listener(|this, _ev, window, cx| this.undo(window, cx))),
6458 )
6459 .child(
6460 act(1, "wb-icon-redo", REDO_ICON)
6461 .tooltip(self.tip("Redo (⌘⇧Z)"))
6462 .on_click(cx.listener(|this, _ev, window, cx| this.redo(window, cx))),
6463 )
6464 .child(
6465 act(2, "wb-icon-delete", DELETE_ICON)
6466 .tooltip(self.tip("Delete selection (⌫)"))
6467 .on_click(
6468 cx.listener(|this, _ev, window, cx| this.delete_selected(window, cx)),
6469 ),
6470 );
6471 let tb_pos = self.toolbar_pos.map(|(x, y)| self.clamp_toolbar(x, y));
6475 let toolbar = match tb_pos {
6476 Some((x, y)) => div().absolute().left(px(x)).top(px(y)).child(pill),
6477 None => div()
6478 .absolute()
6479 .top(px(10.0))
6480 .left_0()
6481 .right_0()
6482 .flex()
6483 .justify_center()
6484 .child(pill),
6485 };
6486 let pill_b = self.toolbar_bounds.get();
6491 let board_o = self.bounds.get().origin;
6492 let pill_top = f32::from(pill_b.origin.y) - f32::from(board_o.y);
6493 let pill_right =
6494 f32::from(pill_b.origin.x) - f32::from(board_o.x) + f32::from(pill_b.size.width);
6495 let has_bounds = f32::from(pill_b.size.width) > 1.0;
6496 let popover_anchor = move || -> Div {
6497 if vertical && has_bounds {
6498 div()
6499 .absolute()
6500 .left(px(pill_right + 6.0))
6501 .top(px(pill_top))
6502 } else {
6503 match tb_pos {
6504 Some((x, y)) => div().absolute().left(px(x)).top(px(y + 42.0)),
6505 None => div()
6506 .absolute()
6507 .top(px(52.0))
6508 .left_0()
6509 .right_0()
6510 .flex()
6511 .justify_center(),
6512 }
6513 }
6514 };
6515
6516 let flyout =
6521 open_group.map(|g| {
6522 let mut row = div()
6523 .flex()
6524 .items_center()
6525 .gap(px(2.0))
6526 .p(px(3.0))
6527 .rounded(px(9.0))
6528 .bg(panel_strong)
6529 .shadow_lg()
6530 .occlude();
6531 for &t in g.tools() {
6532 row = row.child(tool_btn(t).tooltip(self.tip(t.label())).on_click(
6533 cx.listener(move |this, _ev, window, cx| {
6534 this.focus.focus(window, cx);
6535 this.set_tool(t, cx);
6536 }),
6537 ));
6538 }
6539 popover_anchor().child(row)
6540 });
6541
6542 let format_panel = (self.editing.is_some() && self.format_flyout).then(|| {
6545 popover_anchor().child(
6546 self.format_menu(ink, text, grid, panel_strong, cx)
6547 .occlude(),
6548 )
6549 });
6550
6551 let width_cell = self.width_bounds.clone();
6557 let width_panel_cell = self.width_panel_bounds.clone();
6558 let width_frac =
6559 ((self.active_width - WIDTH_MIN) / (WIDTH_MAX - WIDTH_MIN)).clamp(0.0, 1.0);
6560 let width_flyout = self.width_open.then(|| {
6561 let mut presets = div().flex().items_center().gap(px(2.0));
6562 for (i, w) in WIDTH_PRESETS.into_iter().enumerate() {
6563 let active = (self.active_width - w).abs() < 0.01;
6564 let mut opt = div()
6565 .id(("wb-width-opt", i))
6566 .size(px(30.0))
6567 .flex()
6568 .items_center()
6569 .justify_center()
6570 .rounded(px(6.0));
6571 if active {
6572 opt = opt.bg(accent);
6573 } else {
6574 opt = opt.hover(|s| s.bg(grid));
6575 }
6576 presets = presets.child(
6577 opt.child(
6578 div()
6579 .w(px(18.0))
6580 .h(px(w.clamp(1.0, 9.0)))
6581 .rounded_full()
6582 .bg(cur_swatch),
6583 )
6584 .on_click(
6585 cx.listener(move |this, _ev, window, cx| this.set_width(w, window, cx)),
6586 ),
6587 );
6588 }
6589 let slider = div()
6592 .relative()
6593 .w(px(WIDTH_SLIDER_W))
6594 .h(px(WIDTH_MAX + 6.0))
6595 .flex()
6596 .items_center()
6597 .child(
6598 canvas(move |b, _, _| width_cell.set(b), |_, _, _, _| {})
6599 .absolute()
6600 .size_full(),
6601 )
6602 .child(
6603 div()
6604 .w_full()
6605 .h(px(self.active_width.clamp(1.0, WIDTH_MAX)))
6606 .rounded_full()
6607 .bg(cur_swatch),
6608 )
6609 .child(
6610 div()
6611 .absolute()
6612 .top_0()
6613 .bottom_0()
6614 .left(px(width_frac * WIDTH_SLIDER_W - 1.5))
6615 .w(px(3.0))
6616 .rounded(px(2.0))
6617 .bg(hsla(0.0, 0.0, 1.0, 1.0))
6618 .border_1()
6619 .border_color(hsla(0.0, 0.0, 0.0, 0.45)),
6620 );
6621 let panel = div()
6622 .relative()
6623 .flex()
6624 .flex_col()
6625 .items_center()
6626 .gap(px(6.0))
6627 .p(px(6.0))
6628 .rounded(px(9.0))
6629 .bg(panel_strong)
6630 .shadow_lg()
6631 .child(
6632 canvas(move |b, _, _| width_panel_cell.set(b), |_, _, _, _| {})
6633 .absolute()
6634 .size_full(),
6635 )
6636 .child(presets)
6637 .child(slider);
6638 popover_anchor().child(panel)
6639 });
6640
6641 let font_flyout = (self.font_open && self.on_pick_font.is_some()).then(|| {
6645 let row = |id: &'static str, label: &'static str| {
6646 div()
6647 .id(id)
6648 .px(px(12.0))
6649 .py(px(6.0))
6650 .mx(px(4.0))
6651 .rounded(px(6.0))
6652 .text_size(px(12.0))
6653 .text_color(ink)
6654 .hover(|s| s.bg(grid))
6655 .child(label)
6656 };
6657 let panel = div()
6658 .occlude()
6659 .py(px(4.0))
6660 .min_w(px(168.0))
6661 .rounded(px(9.0))
6662 .bg(panel_strong)
6663 .shadow_lg()
6664 .border_1()
6665 .border_color(grid)
6666 .flex()
6667 .flex_col()
6668 .child(row("wb-font-upload", "Upload font…").on_click(cx.listener(
6669 |this, _ev, window, cx| {
6670 this.font_open = false;
6671 if let Some(f) = this.on_pick_font.clone() {
6672 f(FontPick::Upload, window, cx);
6673 }
6674 cx.notify();
6675 },
6676 )))
6677 .child(row("wb-font-default", "Use default").on_click(cx.listener(
6678 |this, _ev, window, cx| {
6679 this.font_open = false;
6680 if let Some(f) = this.on_pick_font.clone() {
6681 f(FontPick::Default, window, cx);
6682 }
6683 cx.notify();
6684 },
6685 )));
6686 popover_anchor().child(panel)
6687 });
6688
6689 let menu =
6693 self.context_menu.map(|pos| {
6694 let row = |id: &'static str, label: &'static str, shortcut: &'static str| {
6696 div()
6697 .id(id)
6698 .flex()
6699 .items_center()
6700 .justify_between()
6701 .gap(px(16.0))
6702 .px(px(10.0))
6703 .py(px(5.0))
6704 .mx(px(4.0))
6705 .rounded(px(6.0))
6706 .text_size(px(12.0))
6707 .text_color(ink)
6708 .hover(|s| s.bg(grid))
6709 .child(label)
6710 .child(div().text_size(px(11.0)).text_color(text).child(shortcut))
6711 };
6712 let divider = || div().my(px(4.0)).mx(px(8.0)).h(px(1.0)).bg(grid);
6713 let has_sel = !self.selected.is_empty();
6714 let mut panel = div()
6715 .absolute()
6716 .left(pos.x)
6717 .top(pos.y)
6718 .occlude()
6719 .min_w(px(176.0))
6720 .py(px(4.0))
6721 .rounded(px(8.0))
6722 .bg(panel_strong)
6723 .shadow_lg()
6724 .border_1()
6725 .border_color(grid)
6726 .flex()
6727 .flex_col();
6728 if self.editing.is_some() {
6730 panel = panel
6731 .child(row("wb-ctx-text", "Text", "▸").on_click(cx.listener(
6732 |this, _ev, _w, cx| {
6733 this.ctx_text_sub = !this.ctx_text_sub;
6734 cx.notify();
6735 },
6736 )))
6737 .child(divider());
6738 }
6739 if has_sel {
6741 panel =
6742 panel
6743 .child(row("wb-ctx-front", "Bring to Front", "⌘⇧]").on_click(
6744 cx.listener(|this, _ev, window, cx| {
6745 this.context_menu = None;
6746 this.reorder_selection(ZOrder::ToFront, window, cx);
6747 }),
6748 ))
6749 .child(row("wb-ctx-forward", "Bring Forward", "⌘]").on_click(
6750 cx.listener(|this, _ev, window, cx| {
6751 this.context_menu = None;
6752 this.reorder_selection(ZOrder::Forward, window, cx);
6753 }),
6754 ))
6755 .child(row("wb-ctx-backward", "Send Backward", "⌘[").on_click(
6756 cx.listener(|this, _ev, window, cx| {
6757 this.context_menu = None;
6758 this.reorder_selection(ZOrder::Backward, window, cx);
6759 }),
6760 ))
6761 .child(
6762 row("wb-ctx-back", "Send to Back", "⌘⇧[").on_click(cx.listener(
6763 |this, _ev, window, cx| {
6764 this.context_menu = None;
6765 this.reorder_selection(ZOrder::ToBack, window, cx);
6766 },
6767 )),
6768 )
6769 .child(divider())
6770 .child(row("wb-ctx-copy", "Copy", "⌘C").on_click(cx.listener(
6771 |this, _ev, window, cx| {
6772 this.context_menu = None;
6773 this.copy_selection(window, cx);
6774 },
6775 )))
6776 .child(row("wb-ctx-cut", "Cut", "⌘X").on_click(cx.listener(
6777 |this, _ev, window, cx| {
6778 this.context_menu = None;
6779 if this.copy_selection(window, cx) {
6780 this.delete_selected(window, cx);
6781 }
6782 },
6783 )));
6784 }
6785 if self.on_paste.is_some() {
6787 panel = panel.child(row("wb-ctx-paste", "Paste", "⌘V").on_click(
6788 cx.listener(|this, _ev, window, cx| this.paste_from_menu(window, cx)),
6789 ));
6790 }
6791 if has_sel && self.on_save_template.is_some() {
6793 panel = panel.child(divider()).child(
6794 row("wb-ctx-save-template", "Save as template", "").on_click(cx.listener(
6795 |this, _ev, window, cx| {
6796 this.context_menu = None;
6797 this.save_selection_as_template(window, cx);
6798 },
6799 )),
6800 );
6801 }
6802 panel
6803 });
6804
6805 let text_submenu = self
6809 .context_menu
6810 .filter(|_| self.ctx_text_sub && self.editing.is_some())
6811 .map(|pos| {
6812 self.format_menu(ink, text, grid, panel_strong, cx)
6813 .absolute()
6814 .left(pos.x + px(184.0))
6815 .top(pos.y)
6816 .occlude()
6817 });
6818
6819 let templates_modal = self.templates_open.then(|| {
6824 let body = if self.templates.is_empty() {
6825 div()
6826 .flex_1()
6827 .flex()
6828 .items_center()
6829 .justify_center()
6830 .p(px(28.0))
6831 .child(
6832 div()
6833 .max_w(px(320.0))
6834 .text_size(px(12.0))
6835 .text_color(text)
6836 .child(
6837 "No templates yet. Select shapes on the canvas, right-click, \
6838 and choose “Save as template”.",
6839 ),
6840 )
6841 .into_any_element()
6842 } else {
6843 let mut grid_el = div().flex().flex_wrap().gap(px(8.0)).justify_center();
6844 for i in 0..self.templates.len() {
6845 grid_el = grid_el.child(self.template_card(i, ink, text, grid, bg, cx));
6846 }
6847 div()
6848 .id("wb-tmpl-scroll")
6849 .flex_1()
6850 .min_h_0()
6851 .overflow_y_scroll()
6852 .p(px(12.0))
6853 .child(grid_el)
6854 .into_any_element()
6855 };
6856 let panel = div()
6857 .w(px(540.0))
6858 .max_h(px(460.0))
6859 .flex()
6860 .flex_col()
6861 .rounded(px(12.0))
6862 .bg(panel_strong)
6863 .shadow_lg()
6864 .border_1()
6865 .border_color(grid)
6866 .occlude()
6867 .child(
6869 div()
6870 .flex()
6871 .items_center()
6872 .justify_between()
6873 .px(px(14.0))
6874 .py(px(10.0))
6875 .border_b_1()
6876 .border_color(grid)
6877 .child(div().text_size(px(14.0)).text_color(ink).child("Templates"))
6878 .child(
6879 div()
6880 .id("wb-tmpl-close")
6881 .size(px(22.0))
6882 .flex()
6883 .items_center()
6884 .justify_center()
6885 .rounded(px(6.0))
6886 .text_size(px(15.0))
6887 .text_color(text)
6888 .hover(|s| s.bg(grid))
6889 .child("✕")
6890 .on_click(cx.listener(|this, _ev, _w, cx| {
6891 this.templates_open = false;
6892 cx.notify();
6893 })),
6894 ),
6895 )
6896 .child(body)
6897 .child(
6899 div()
6900 .px(px(14.0))
6901 .py(px(8.0))
6902 .border_t_1()
6903 .border_color(grid)
6904 .text_size(px(10.0))
6905 .text_color(text)
6906 .child("Click to add · right-click to delete"),
6907 );
6908 div()
6909 .absolute()
6910 .size_full()
6911 .flex()
6912 .items_center()
6913 .justify_center()
6914 .bg(hsla(0.0, 0.0, 0.0, 0.35))
6915 .occlude()
6916 .on_mouse_down(
6917 MouseButton::Left,
6918 cx.listener(|this, _ev, _w, cx| {
6919 this.templates_open = false;
6920 cx.notify();
6921 }),
6922 )
6923 .child(panel)
6924 });
6925
6926 let sv_cell = self.sv_bounds.clone();
6931 let hue_cell = self.hue_bounds.clone();
6932 let alpha_cell = self.alpha_bounds.clone();
6933 let panel_cell = self.picker_bounds.clone();
6934 let swatch_list = swatches;
6935 let white = hsla(0.0, 0.0, 1.0, 1.0);
6936 let stroke_disp = self
6939 .selected_single()
6940 .and_then(|id| self.scene.elements.iter().find(|e| e.id == id))
6941 .and_then(|e| e.stroke)
6942 .or(self.active_stroke);
6943 let fill_disp = self
6944 .selected_single()
6945 .and_then(|id| self.scene.elements.iter().find(|e| e.id == id))
6946 .and_then(|e| e.fill)
6947 .or(self.active_fill);
6948 let text_disp = self
6949 .selected_single()
6950 .and_then(|id| self.scene.elements.iter().find(|e| e.id == id))
6951 .and_then(|e| e.label_color)
6952 .or(self.active_text);
6953 let picker_panel = self.picker.map(|p| {
6954 let cur = hsva_to_u32(p.h, p.s, p.v, p.a);
6955 let hex = format!("#{:06X}", cur >> 8);
6956 let clear = hsla(0.0, 0.0, 0.0, 0.0);
6957
6958 let tab = |active: bool, sw: Hsla, label: &'static str, id: &'static str| {
6961 let mut d = div()
6962 .id(id)
6963 .flex()
6964 .items_center()
6965 .gap(px(6.0))
6966 .px(px(8.0))
6967 .py(px(4.0))
6968 .rounded(px(6.0))
6969 .text_size(px(12.0))
6970 .text_color(ink);
6971 if active {
6972 d = d.bg(accent);
6973 }
6974 d.child(
6975 div()
6976 .size(px(12.0))
6977 .rounded(px(3.0))
6978 .bg(sw)
6979 .border_1()
6980 .border_color(grid),
6981 )
6982 .child(label)
6983 };
6984 let tabs = div()
6985 .flex()
6986 .gap(px(6.0))
6987 .child(
6988 tab(
6989 p.target == PickerTarget::Stroke,
6990 stroke_disp.map_or(ink, u32_to_hsla),
6991 "Stroke",
6992 "wb-tab-stroke",
6993 )
6994 .on_click(cx.listener(|this, _ev, _w, cx| {
6995 this.set_picker_target(PickerTarget::Stroke, cx)
6996 })),
6997 )
6998 .child(
6999 tab(
7000 p.target == PickerTarget::Fill,
7001 fill_disp.map_or(clear, u32_to_hsla),
7002 "Fill",
7003 "wb-tab-fill",
7004 )
7005 .on_click(cx.listener(|this, _ev, _w, cx| {
7006 this.set_picker_target(PickerTarget::Fill, cx)
7007 })),
7008 )
7009 .child(
7010 tab(
7011 p.target == PickerTarget::Text,
7012 text_disp.map_or(ink, u32_to_hsla),
7013 "Text",
7014 "wb-tab-text",
7015 )
7016 .on_click(cx.listener(|this, _ev, _w, cx| {
7017 this.set_picker_target(PickerTarget::Text, cx)
7018 })),
7019 );
7020
7021 let sv_square = div()
7022 .relative()
7023 .w(px(SV_W))
7024 .h(px(SV_H))
7025 .rounded(px(5.0))
7026 .overflow_hidden()
7027 .bg(hsla(p.h, 1.0, 0.5, 1.0))
7028 .child(div().absolute().size_full().bg(linear_gradient(
7029 90.0,
7030 linear_color_stop(white, 0.0),
7031 linear_color_stop(hsla(0.0, 0.0, 1.0, 0.0), 1.0),
7032 )))
7033 .child(div().absolute().size_full().bg(linear_gradient(
7034 180.0,
7035 linear_color_stop(hsla(0.0, 0.0, 0.0, 0.0), 0.0),
7036 linear_color_stop(hsla(0.0, 0.0, 0.0, 1.0), 1.0),
7037 )))
7038 .child(
7039 canvas(move |b, _, _| sv_cell.set(b), |_, _, _, _| {})
7040 .absolute()
7041 .size_full(),
7042 )
7043 .child(
7044 div()
7045 .absolute()
7046 .left(px(p.s * SV_W - 7.0))
7047 .top(px((1.0 - p.v) * SV_H - 7.0))
7048 .size(px(14.0))
7049 .rounded_full()
7050 .border_2()
7051 .border_color(white),
7052 );
7053
7054 let seg = |from: f32, to: f32| {
7055 div().flex_1().h_full().bg(linear_gradient(
7056 90.0,
7057 linear_color_stop(hsla(from, 1.0, 0.5, 1.0), 0.0),
7058 linear_color_stop(hsla(to, 1.0, 0.5, 1.0), 1.0),
7059 ))
7060 };
7061 let hue_strip = div()
7062 .relative()
7063 .w(px(SV_W))
7064 .h(px(HUE_H))
7065 .rounded(px(4.0))
7066 .overflow_hidden()
7067 .flex()
7068 .child(seg(0.0, 1.0 / 6.0))
7069 .child(seg(1.0 / 6.0, 2.0 / 6.0))
7070 .child(seg(2.0 / 6.0, 3.0 / 6.0))
7071 .child(seg(3.0 / 6.0, 4.0 / 6.0))
7072 .child(seg(4.0 / 6.0, 5.0 / 6.0))
7073 .child(seg(5.0 / 6.0, 1.0))
7074 .child(
7075 canvas(move |b, _, _| hue_cell.set(b), |_, _, _, _| {})
7076 .absolute()
7077 .size_full(),
7078 )
7079 .child(
7080 div()
7081 .absolute()
7082 .left(px(p.h * SV_W - 1.5))
7083 .top(px(-2.0))
7084 .w(px(3.0))
7085 .h(px(HUE_H + 4.0))
7086 .rounded(px(2.0))
7087 .bg(white)
7088 .border_1()
7089 .border_color(hsla(0.0, 0.0, 0.0, 0.5)),
7090 );
7091
7092 let alpha_strip = div()
7094 .relative()
7095 .w(px(SV_W))
7096 .h(px(HUE_H))
7097 .rounded(px(4.0))
7098 .overflow_hidden()
7099 .bg(linear_gradient(
7100 90.0,
7101 linear_color_stop(clear, 0.0),
7102 linear_color_stop(u32_to_hsla(hsv_to_u32(p.h, p.s, p.v)), 1.0),
7103 ))
7104 .child(
7105 canvas(move |b, _, _| alpha_cell.set(b), |_, _, _, _| {})
7106 .absolute()
7107 .size_full(),
7108 )
7109 .child(
7110 div()
7111 .absolute()
7112 .left(px(p.a * SV_W - 1.5))
7113 .top(px(-2.0))
7114 .w(px(3.0))
7115 .h(px(HUE_H + 4.0))
7116 .rounded(px(2.0))
7117 .bg(white)
7118 .border_1()
7119 .border_color(hsla(0.0, 0.0, 0.0, 0.5)),
7120 );
7121
7122 let reset_label = if p.target == PickerTarget::Fill {
7124 "None"
7125 } else {
7126 "Auto"
7127 };
7128 let info_row = div()
7129 .flex()
7130 .items_center()
7131 .gap(px(8.0))
7132 .child(
7133 div()
7134 .size(px(22.0))
7135 .rounded(px(4.0))
7136 .bg(u32_to_hsla(cur))
7137 .border_1()
7138 .border_color(grid),
7139 )
7140 .child(
7141 div()
7142 .flex_1()
7143 .text_size(px(12.0))
7144 .text_color(text)
7145 .child(SharedString::from(hex)),
7146 )
7147 .child(
7148 div()
7149 .id("wb-color-auto")
7150 .px(px(8.0))
7151 .py(px(3.0))
7152 .rounded(px(5.0))
7153 .border_1()
7154 .border_color(grid)
7155 .text_size(px(12.0))
7156 .text_color(ink)
7157 .child(reset_label)
7158 .on_click(
7159 cx.listener(|this, _ev, window, cx| this.pick_color(None, window, cx)),
7160 ),
7161 );
7162
7163 let mut swatch_views = Vec::with_capacity(swatch_list.len());
7164 for (i, c) in swatch_list.iter().enumerate() {
7165 let col = *c;
7166 swatch_views.push(
7167 div()
7168 .id(("wb-swatch", i))
7169 .size(px(20.0))
7170 .rounded(px(4.0))
7171 .bg(col)
7172 .border_1()
7173 .border_color(grid)
7174 .on_click(cx.listener(move |this, _ev, window, cx| {
7175 this.pick_color(Some(hsla_to_u32(col)), window, cx)
7176 })),
7177 );
7178 }
7179 let theme_row_w = (swatch_views.len() as f32 * 26.0 - 6.0).max(0.0);
7184 let saved_col_w = (theme_row_w - SV_W - 12.0).max(64.0);
7185 let swatch_grid = div().flex().flex_wrap().gap(px(6.0)).children(swatch_views);
7186
7187 let controls_col = div()
7189 .flex()
7190 .flex_col()
7191 .gap(px(10.0))
7192 .child(tabs)
7193 .child(sv_square)
7194 .child(hue_strip)
7195 .child(alpha_strip)
7196 .child(info_row);
7197
7198 let mut saved_grid = div().flex().flex_wrap().gap(px(6.0));
7202 if self.saved_colors.is_empty() {
7203 saved_grid = saved_grid.child(
7204 div()
7205 .w_full()
7206 .text_size(px(11.0))
7207 .text_color(text)
7208 .child("Tap + to save a color"),
7209 );
7210 } else {
7211 for (i, &c) in self.saved_colors.iter().enumerate() {
7212 saved_grid = saved_grid.child(
7213 div()
7214 .id(("wb-saved", i))
7215 .size(px(20.0))
7216 .rounded(px(4.0))
7217 .bg(u32_to_hsla(c))
7218 .border_1()
7219 .border_color(grid)
7220 .tooltip(self.tip("Click to use · right-click to remove"))
7221 .on_click(cx.listener(move |this, _ev, window, cx| {
7222 this.pick_color(Some(c), window, cx)
7223 }))
7224 .on_mouse_down(
7225 MouseButton::Right,
7226 cx.listener(move |this, _ev, window, cx| {
7227 this.remove_saved_color(c, window, cx)
7228 }),
7229 ),
7230 );
7231 }
7232 }
7233 let saved_col = div()
7237 .flex()
7238 .flex_col()
7239 .flex_none()
7240 .w(px(saved_col_w))
7241 .gap(px(8.0))
7242 .child(
7243 div()
7244 .flex()
7245 .items_center()
7246 .justify_between()
7247 .child(div().text_size(px(11.0)).text_color(text).child("Saved"))
7248 .child(
7249 div()
7250 .id("wb-save-color")
7251 .size(px(20.0))
7252 .flex()
7253 .items_center()
7254 .justify_center()
7255 .rounded(px(4.0))
7256 .border_1()
7257 .border_color(grid)
7258 .text_size(px(14.0))
7259 .text_color(ink)
7260 .hover(|s| s.bg(grid))
7261 .child("+")
7262 .tooltip(self.tip("Save current color"))
7263 .on_click(cx.listener(|this, _ev, window, cx| {
7264 this.save_current_color(window, cx)
7265 })),
7266 ),
7267 )
7268 .child(saved_grid);
7269
7270 let top_row = div()
7273 .flex()
7274 .flex_row()
7275 .items_start()
7276 .gap(px(12.0))
7277 .child(controls_col)
7278 .child(saved_col);
7279
7280 popover_anchor().child(
7281 div()
7282 .relative()
7283 .flex()
7284 .flex_col()
7285 .gap(px(10.0))
7286 .p(px(10.0))
7287 .rounded(px(10.0))
7288 .bg(panel_strong)
7289 .shadow_lg()
7290 .border_1()
7291 .border_color(grid)
7292 .child(
7293 canvas(move |b, _, _| panel_cell.set(b), |_, _, _, _| {})
7294 .absolute()
7295 .size_full(),
7296 )
7297 .child(top_row)
7298 .child(swatch_grid),
7299 )
7300 });
7301
7302 let board_cursor = if self.panning {
7305 CursorStyle::ClosedHand
7306 } else if self.tool == Tool::Pan {
7307 CursorStyle::OpenHand
7308 } else {
7309 CursorStyle::Arrow
7310 };
7311
7312 let board_layer = canvas(
7318 move |bounds, _, _| bounds_cell.set(bounds),
7319 move |bounds, _, window, _| paint_board(bounds, cam, bg, grid, window),
7320 )
7321 .absolute()
7322 .size_full();
7323 let element_layers: Vec<gpui::AnyElement> = layers
7324 .into_iter()
7325 .map(|l| match l {
7326 Layer::Band(es) => band_canvas(es, cam).into_any_element(),
7327 Layer::Overlay(el) => el,
7328 })
7329 .collect();
7330 let chrome_layer = canvas(
7331 |_, _, _| {},
7332 move |bounds, _, window, _| {
7333 if let Some(k) = &pending {
7334 paint_element(
7335 k,
7336 None,
7337 cam,
7338 bounds.origin,
7339 pending_ink,
7340 pending_fill,
7341 window,
7342 );
7343 }
7344 if let Some(k) = &single_sel {
7345 paint_selection(k, cam, bounds.origin, selection, window);
7346 }
7347 if let Some((kind, active)) = &snap_target {
7348 paint_snap_points(kind, *active, cam, bounds.origin, selection, window);
7349 }
7350 if let Some((bb, _can_rotate)) = group_sel {
7353 let tl = to_screen(bb.0, bb.1, cam, bounds.origin);
7354 let br = to_screen(bb.2, bb.3, cam, bounds.origin);
7355 let m = 0.0;
7356 let (x0, y0) = (f32::from(tl.x) - m, f32::from(tl.y) - m);
7357 let (x1, y1) = (f32::from(br.x) + m, f32::from(br.y) + m);
7358 let (mx, my) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
7361 for (hx, hy) in [
7362 (x0, y0),
7363 (x1, y0),
7364 (x0, y1),
7365 (x1, y1),
7366 (mx, y0),
7367 (mx, y1),
7368 (x0, my),
7369 (x1, my),
7370 ] {
7371 draw_handle(hx, hy, selection, window);
7372 }
7373 }
7374 if let Some((a, b)) = marquee {
7375 paint_marquee(a, b, cam, bounds.origin, selection, window);
7376 }
7377 paint_alignment_guides(alignment_guides, bounds, cam, selection, window);
7378 },
7379 )
7380 .absolute()
7381 .size_full();
7382
7383 let root = div()
7384 .track_focus(&self.focus)
7385 .size_full()
7386 .relative()
7387 .overflow_hidden()
7388 .cursor(board_cursor)
7389 .child(board_layer)
7390 .children(connector_buttons)
7391 .children(rotate_button)
7392 .child(
7393 div()
7394 .absolute()
7395 .size_full()
7396 .child(WhiteboardInputElement::new(cx.entity())),
7397 )
7398 .on_mouse_down(MouseButton::Left, cx.listener(Self::on_left_down))
7399 .on_mouse_up(MouseButton::Left, cx.listener(Self::on_left_up))
7400 .on_mouse_down(MouseButton::Right, cx.listener(Self::on_right_down))
7401 .on_mouse_down(MouseButton::Middle, cx.listener(Self::on_middle_down))
7402 .on_mouse_up(MouseButton::Middle, cx.listener(Self::on_middle_up))
7403 .on_mouse_move(cx.listener(Self::on_move));
7404 let root = if accepts_wheel_input(self.read_only) {
7405 root.on_scroll_wheel(cx.listener(Self::on_scroll))
7406 } else {
7407 root
7408 };
7409 root.on_pinch(cx.listener(Self::on_pinch))
7410 .on_key_down(cx.listener(Self::on_key))
7411 .on_drop::<gpui::ExternalPaths>(cx.listener(
7414 |this, paths: &gpui::ExternalPaths, window, cx| {
7415 if let Some(f) = this.on_drop_files.clone() {
7416 let w = this.event_to_world(window.mouse_position());
7417 f(paths.paths().to_vec(), w[0], w[1], window, cx);
7418 }
7419 },
7420 ))
7421 .children(element_layers)
7422 .child(chrome_layer)
7423 .child(toolbar)
7424 .children(flyout)
7425 .children(format_panel)
7426 .children(width_flyout)
7427 .children(font_flyout)
7428 .children(menu)
7429 .children(text_submenu)
7430 .children(picker_panel)
7431 .children(templates_modal)
7432 .child(
7433 div()
7434 .absolute()
7435 .left(px(10.0))
7436 .bottom(px(8.0))
7437 .text_size(px(11.0))
7438 .text_color(text)
7439 .child(SharedString::from(format!("{:.0}%", cam.zoom * 100.0))),
7440 )
7441 .into_any_element()
7442 }
7443}
7444
7445#[cfg(test)]
7446mod tests {
7447 use super::*;
7448
7449 #[test]
7450 fn text_layout_cache_reuses_outlines_until_content_changes() {
7451 let font = Font::default();
7452 let mut cache = HashMap::new();
7453 let first = cached_text_layout(&mut cache, &font, 9, "hello", 16.0, None, &[]);
7454 let reused = cached_text_layout(&mut cache, &font, 9, "hello", 16.0, None, &[]);
7455 let changed = cached_text_layout(&mut cache, &font, 9, "hello!", 16.0, None, &[]);
7456
7457 assert!(Arc::ptr_eq(&first.segs, &reused.segs));
7458 assert!(!Arc::ptr_eq(&first.segs, &changed.segs));
7459 }
7460
7461 #[test]
7462 fn shape_label_cache_survives_movement_and_invalidates_on_resize() {
7463 let font = Font::default();
7464 let mut cache = HashMap::new();
7465 let kind = ElementKind::RoundRect(BoxGeom {
7466 x: 10.0,
7467 y: 20.0,
7468 w: 180.0,
7469 h: 60.0,
7470 width: 2.0,
7471 rotation: 0.0,
7472 });
7473 let first = cached_label_layout(
7474 &mut cache,
7475 &font,
7476 3,
7477 &kind,
7478 10.0,
7479 20.0,
7480 180.0,
7481 60.0,
7482 "Node",
7483 &[],
7484 );
7485 let moved = cached_label_layout(
7486 &mut cache,
7487 &font,
7488 3,
7489 &kind,
7490 80.0,
7491 90.0,
7492 180.0,
7493 60.0,
7494 "Node",
7495 &[],
7496 );
7497 let resized = cached_label_layout(
7498 &mut cache,
7499 &font,
7500 3,
7501 &kind,
7502 80.0,
7503 90.0,
7504 240.0,
7505 80.0,
7506 "Node",
7507 &[],
7508 );
7509
7510 assert!(Arc::ptr_eq(&first.text.segs, &moved.text.segs));
7511 assert!(!Arc::ptr_eq(&first.text.segs, &resized.text.segs));
7512 }
7513
7514 #[test]
7515 fn read_only_embed_does_not_accept_wheel_input() {
7516 assert!(!accepts_wheel_input(true));
7517 assert!(accepts_wheel_input(false));
7518 }
7519
7520 #[test]
7521 fn empty_or_garbage_loads_a_blank_board() {
7522 for s in ["", " ", "not json", "{}", r#"{"camera":{"zoom":0}}"#] {
7523 let scene = Scene::from_json(s);
7524 assert_eq!(scene.camera.zoom, 1.0, "input {s:?}");
7525 assert!(scene.elements.is_empty(), "input {s:?}");
7526 }
7527 }
7528
7529 #[test]
7530 fn ime_offsets_bridge_utf16_and_utf8() {
7531 let text = "A中😀B";
7534 let utf8_boundaries = [0, 1, 4, 8, 9];
7535 let utf16_boundaries = [0, 1, 2, 4, 5];
7536 for (utf8, utf16) in utf8_boundaries.into_iter().zip(utf16_boundaries) {
7537 assert_eq!(WhiteboardView::utf8_to_utf16_in(text, utf8), utf16);
7538 assert_eq!(WhiteboardView::utf16_to_utf8_in(text, utf16), utf8);
7539 }
7540
7541 assert_eq!(WhiteboardView::utf8_to_utf16_in(text, 3), 2);
7544 assert_eq!(WhiteboardView::utf16_to_utf8_in(text, 3), 8);
7545 }
7546
7547 #[test]
7548 fn camera_round_trips_through_json() {
7549 let scene = Scene {
7550 camera: Camera {
7551 x: 12.5,
7552 y: -4.0,
7553 zoom: 2.0,
7554 },
7555 ..Default::default()
7556 };
7557 let restored = Scene::from_json(&scene.to_json());
7558 assert_eq!(restored.camera.x, 12.5);
7559 assert_eq!(restored.camera.zoom, 2.0);
7560 }
7561
7562 #[test]
7563 fn all_content_thumbnail_snapshot_uses_scene_bounds_without_mounting_view() {
7564 let scene = Scene {
7565 elements: vec![Element {
7566 id: 1,
7567 kind: ElementKind::Rect(BoxGeom {
7568 x: 10.0,
7569 y: 20.0,
7570 w: 100.0,
7571 h: 60.0,
7572 width: 2.0,
7573 rotation: 0.0,
7574 }),
7575 stroke: None,
7576 fill: None,
7577 label: None,
7578 label_color: None,
7579 styles: Vec::new(),
7580 mindmap: None,
7581 }],
7582 ..Scene::default()
7583 };
7584
7585 let snapshot = LocalThumbnailSnapshot::for_scene_all_content(scene, 320.0, 180.0);
7586
7587 assert_eq!(snapshot.spec.scene_bounds, Some([10.0, 20.0, 110.0, 80.0]));
7588 assert_eq!(snapshot.spec.focus_bounds, [10.0, 20.0, 110.0, 80.0]);
7589 assert!(snapshot.spec.camera.zoom > 0.0);
7590 }
7591
7592 #[test]
7593 fn empty_scene_still_builds_a_renderable_thumbnail_snapshot() {
7594 let snapshot =
7595 LocalThumbnailSnapshot::for_scene_all_content(Scene::default(), 320.0, 180.0);
7596
7597 assert_eq!(snapshot.spec.scene_bounds, None);
7598 assert_eq!(snapshot.spec.focus_bounds, [0.0, 0.0, 320.0, 180.0]);
7599 assert_eq!(snapshot.spec.camera.zoom, 1.0);
7600 }
7601
7602 #[test]
7603 fn every_element_kind_round_trips_through_json() {
7604 let scene = Scene {
7605 camera: Camera::default(),
7606 elements: vec![
7607 Element {
7608 id: 1,
7609 kind: ElementKind::Draw(Stroke {
7610 points: vec![[0.0, 0.0], [10.0, 5.0]],
7611 width: 3.0,
7612 }),
7613 stroke: None,
7614 fill: None,
7615 label: None,
7616 label_color: None,
7617 styles: Vec::new(),
7618 mindmap: None,
7619 },
7620 Element {
7621 id: 2,
7622 kind: ElementKind::Rect(BoxGeom {
7623 x: 1.0,
7624 y: 2.0,
7625 w: 30.0,
7626 h: 40.0,
7627 width: 2.0,
7628 rotation: 0.0,
7629 }),
7630 stroke: Some(0xff0000ff),
7631 fill: Some(0x00ff0080),
7632 label: Some("hi".into()),
7633 label_color: Some(0x112233ff),
7634 styles: Vec::new(),
7635 mindmap: None,
7636 },
7637 Element {
7638 id: 3,
7639 kind: ElementKind::Arrow(SegGeom {
7640 x1: 1.0,
7641 y1: 1.0,
7642 x2: 2.0,
7643 y2: 8.0,
7644 width: 2.5,
7645 style: SegmentStyle::Solid,
7646 start_anchor: None,
7647 end_anchor: None,
7648 }),
7649 stroke: None,
7650 fill: None,
7651 label: None,
7652 label_color: None,
7653 styles: Vec::new(),
7654 mindmap: None,
7655 },
7656 ],
7657 };
7658 let restored = Scene::from_json(&scene.to_json());
7659 assert_eq!(restored.elements.len(), 3);
7660 match &restored.elements[2].kind {
7661 ElementKind::Arrow(s) => assert_eq!(s.y2, 8.0),
7662 other => panic!("expected arrow, got {other:?}"),
7663 }
7664 assert_eq!(restored.elements[1].stroke, Some(0xff0000ff));
7666 assert_eq!(restored.elements[1].fill, Some(0x00ff0080));
7667 assert_eq!(restored.elements[1].label.as_deref(), Some("hi"));
7669 assert_eq!(restored.elements[1].label_color, Some(0x112233ff));
7670 assert_eq!(restored.elements[0].stroke, None);
7671 assert_eq!(restored.elements[0].fill, None);
7672 }
7673
7674 #[test]
7675 fn label_defaults_to_none_for_older_boards() {
7676 let old = r#"{"id":2,"kind":{"rect":{"x":0.0,"y":0.0,"w":1.0,"h":1.0,"width":1.0}}}"#;
7679 let back: Element = serde_json::from_str(old).unwrap();
7680 assert_eq!(back.label, None);
7681 assert!(!serde_json::to_string(&back).unwrap().contains("label"));
7682 }
7683
7684 #[test]
7685 fn shape_label_block_fits_inscribed_region() {
7686 let font = Font::default();
7687 let bg = BoxGeom {
7688 x: 0.0,
7689 y: 0.0,
7690 w: 0.0,
7691 h: 0.0,
7692 width: 0.0,
7693 rotation: 0.0,
7694 };
7695 let (bx, by, bw, bh) = (10.0, 20.0, 200.0, 120.0);
7696
7697 let rect = shape_label_block(&font, &ElementKind::Rect(bg), bx, by, bw, bh, "hello world");
7699 assert!(rect.size <= TEXT_SIZE + 0.01, "size {}", rect.size);
7700 assert!(
7701 (rect.wrap - (bw - 2.0 * LABEL_PAD)).abs() < 0.5,
7702 "rect wraps full width: {}",
7703 rect.wrap
7704 );
7705
7706 let dia = shape_label_block(
7709 &font,
7710 &ElementKind::Diamond(bg),
7711 bx,
7712 by,
7713 bw,
7714 bh,
7715 "hello world",
7716 );
7717 assert!(
7718 (dia.wrap - (bw * 0.5 - 2.0 * LABEL_PAD)).abs() < 0.5,
7719 "diamond half width: {}",
7720 dia.wrap
7721 );
7722 assert!(
7723 dia.size <= rect.size,
7724 "diamond shrinks ≥ rect: {} vs {}",
7725 dia.size,
7726 rect.size
7727 );
7728
7729 let tri = shape_label_block(
7731 &font,
7732 &ElementKind::Triangle(bg),
7733 bx,
7734 by,
7735 bw,
7736 bh,
7737 "hello world",
7738 );
7739 assert!(
7740 tri.y >= by + bh / 2.0 - 0.5,
7741 "triangle label low: y={}",
7742 tri.y
7743 );
7744
7745 let tiny = shape_label_block(
7747 &font,
7748 &ElementKind::Rect(bg),
7749 0.0,
7750 0.0,
7751 44.0,
7752 28.0,
7753 "a long label that must shrink",
7754 );
7755 assert!(tiny.size < TEXT_SIZE, "shrinks: {}", tiny.size);
7756 }
7757
7758 #[test]
7759 fn style_span_toggle_and_layer() {
7760 let bold = RunStyle {
7761 bold: true,
7762 ..Default::default()
7763 };
7764 let s = toggle_format(&[], 0, 4, Format::Bold);
7765 assert_eq!(s.len(), 1);
7766 assert_eq!((s[0].start, s[0].end, s[0].style), (0, 4, bold));
7767 assert!(style_at(&s, 2).bold && !style_at(&s, 4).bold);
7768 assert!(toggle_format(&s, 0, 4, Format::Bold).is_empty());
7770 let s2 = toggle_format(&s, 2, 6, Format::Bold);
7772 assert_eq!((s2.len(), s2[0].start, s2[0].end), (1, 0, 6));
7773 let s3 = toggle_format(&s, 1, 3, Format::Italic);
7775 assert_eq!(s3.len(), 3, "{s3:?}");
7776 assert!(style_at(&s3, 1).bold && style_at(&s3, 1).italic);
7777 assert!(style_at(&s3, 0).bold && !style_at(&s3, 0).italic);
7778 let h = toggle_highlight(&[], 0, 3, 0xffff00ff);
7780 assert_eq!(style_at(&h, 1).highlight, Some(0xffff00ff));
7781 assert!(toggle_highlight(&h, 0, 3, 0xffff00ff).is_empty());
7782 }
7783
7784 #[test]
7785 fn active_style_reports_common_formatting() {
7786 let s = toggle_format(&[], 2, 5, Format::Bold); assert!(active_style(&s, 2, 5).bold, "whole selection bold");
7788 assert!(
7789 !active_style(&s, 0, 5).bold,
7790 "selection spills onto plain text"
7791 );
7792 assert!(
7794 active_style(&s, 5, 5).bold,
7795 "just after the run inherits bold"
7796 );
7797 assert!(!active_style(&s, 2, 2).bold, "just before it is plain");
7798 }
7799
7800 #[test]
7801 fn splice_keeps_runs_aligned() {
7802 let plain = RunStyle::default();
7803 let s = toggle_format(&[], 2, 5, Format::Bold);
7804 let a = splice_styles(&s, 0, 0, 2, plain);
7806 assert_eq!((a[0].start, a[0].end), (4, 7));
7807 let b = splice_styles(&s, 3, 4, 0, plain);
7809 assert_eq!((b[0].start, b[0].end), (2, 4), "{b:?}");
7810 let full = toggle_format(&[], 0, 6, Format::Bold);
7812 let c = splice_styles(&full, 2, 4, 2, plain);
7813 assert_eq!(c.len(), 2, "{c:?}");
7814 assert_eq!(
7815 ((c[0].start, c[0].end), (c[1].start, c[1].end)),
7816 ((0, 2), (4, 6))
7817 );
7818 }
7819
7820 #[test]
7821 fn styles_round_trip_and_back_compat() {
7822 let el = Element {
7823 id: 1,
7824 kind: ElementKind::Text(TextGeom {
7825 x: 0.0,
7826 y: 0.0,
7827 content: "hello world".into(),
7828 size: 12.0,
7829 rotation: 0.0,
7830 measured_w: 0.0,
7831 measured_h: 0.0,
7832 }),
7833 stroke: None,
7834 fill: None,
7835 label: None,
7836 label_color: None,
7837 styles: vec![StyleSpan {
7838 start: 0,
7839 end: 5,
7840 style: RunStyle {
7841 bold: true,
7842 highlight: Some(0xffff00ff),
7843 ..Default::default()
7844 },
7845 }],
7846 mindmap: None,
7847 };
7848 let back: Element = serde_json::from_str(&serde_json::to_string(&el).unwrap()).unwrap();
7849 assert_eq!(back.styles.len(), 1);
7850 assert!(back.styles[0].style.bold);
7851 assert_eq!(back.styles[0].style.highlight, Some(0xffff00ff));
7852 let old = r#"{"id":2,"kind":{"rect":{"x":0.0,"y":0.0,"w":1.0,"h":1.0,"width":1.0}}}"#;
7854 assert!(
7855 serde_json::from_str::<Element>(old)
7856 .unwrap()
7857 .styles
7858 .is_empty()
7859 );
7860 }
7861
7862 #[test]
7863 fn pan_and_zoom_math() {
7864 let mut c = Camera::default();
7865 c.pan_by(50.0, -20.0);
7866 assert_eq!((c.x, c.y), (-50.0, 20.0));
7867
7868 let mut c = Camera {
7869 x: 10.0,
7870 y: 5.0,
7871 zoom: 1.0,
7872 };
7873 let before = c.screen_to_world(300.0, 200.0);
7874 c.zoom_about(300.0, 200.0, 2.5);
7875 let after = c.screen_to_world(300.0, 200.0);
7876 assert!((before.0 - after.0).abs() < 1e-3);
7877 assert!((before.1 - after.1).abs() < 1e-3);
7878 assert_eq!(c.zoom, 2.5);
7879 }
7880
7881 #[test]
7882 fn bbox_translate_and_hit_test() {
7883 let mut k = ElementKind::Line(SegGeom {
7884 x1: 0.0,
7885 y1: 0.0,
7886 x2: 10.0,
7887 y2: 4.0,
7888 width: 1.0,
7889 style: SegmentStyle::Solid,
7890 start_anchor: None,
7891 end_anchor: None,
7892 });
7893 assert_eq!(bbox(&k), (0.0, 0.0, 10.0, 4.0));
7894 translate(&mut k, 5.0, -2.0);
7895 assert_eq!(bbox(&k), (5.0, -2.0, 15.0, 2.0));
7896 assert!(hit_test(&k, 5.0, -2.0, 1.0));
7898 assert!(hit_test(&k, 4.5, -2.5, 1.0)); assert!(!hit_test(&k, 100.0, 100.0, 1.0));
7900 }
7901
7902 #[test]
7903 fn diagonal_scale_projects_the_cursor_onto_the_diagonal() {
7904 let s = diagonal_scale([0.0, 0.0], [10.0, 10.0], [20.0, 20.0]);
7906 assert!((s - 2.0).abs() < 1e-4, "{s}");
7907 let s = diagonal_scale([0.0, 0.0], [10.0, 10.0], [20.0, 0.0]);
7909 assert!((s - 1.0).abs() < 1e-4, "{s}");
7910 }
7911
7912 #[test]
7913 fn snap_45_locks_angle_and_keeps_length() {
7914 let (x, y) = snap_45(0.0, 0.0, 10.0, 9.0);
7916 assert!((x - y).abs() < 1e-3, "{x} vs {y}");
7917 let (x, y) = snap_45(0.0, 0.0, 10.0, 1.0);
7919 assert!(y.abs() < 1e-3);
7920 assert!((x - 101.0f32.sqrt()).abs() < 1e-2);
7921 }
7922
7923 #[test]
7924 fn snap_grid_rounds_to_nearest_line() {
7925 assert_eq!(snap_grid(0.0), 0.0);
7927 assert_eq!(snap_grid(11.0), 0.0);
7928 assert_eq!(snap_grid(13.0), GRID);
7929 assert_eq!(snap_grid(GRID), GRID);
7930 assert_eq!(snap_grid(-13.0), -GRID);
7931 assert_eq!(snap_grid(1.5 * GRID), 2.0 * GRID);
7932 }
7933
7934 #[test]
7935 fn move_target_drives_an_absolute_snapped_target() {
7936 let origin = [100.0, 100.0];
7938 let anchor = [0.0, 0.0];
7939
7940 assert_eq!(
7942 move_target(origin, anchor, [37.0, -11.0], false),
7943 [137.0, 89.0]
7944 );
7945
7946 assert_eq!(
7950 move_target(origin, anchor, [50.0, 50.0], true),
7951 [144.0, 144.0]
7952 );
7953
7954 let mut cursor = [0.0, 0.0];
7958 for _ in 0..12 {
7959 cursor = [cursor[0] + 4.0, cursor[1] + 4.0];
7960 }
7961 assert_eq!(move_target(origin, anchor, cursor, true), [144.0, 144.0]);
7963 }
7964
7965 #[test]
7966 fn resize_scales_geometry_about_the_anchor() {
7967 let mut k = ElementKind::Rect(BoxGeom {
7970 x: 10.0,
7971 y: 10.0,
7972 w: 20.0,
7973 h: 20.0,
7974 width: 1.0,
7975 rotation: 0.0,
7976 });
7977 resize_about(&mut k, 10.0, 10.0, 2.0, 2.0);
7978 match k {
7979 ElementKind::Rect(b) => {
7980 assert_eq!((b.x, b.y), (10.0, 10.0));
7981 assert_eq!((b.w, b.h), (40.0, 40.0));
7982 }
7983 other => panic!("expected rect, got {other:?}"),
7984 }
7985 }
7986
7987 #[test]
7988 fn axis_scale_measures_one_axis_about_the_anchor() {
7989 assert!((axis_scale(0.0, 10.0, 20.0) - 2.0).abs() < 1e-4);
7991 assert!((axis_scale(0.0, 10.0, 5.0) - 0.5).abs() < 1e-4);
7993 assert!((axis_scale(7.0, 7.0, 99.0) - 1.0).abs() < 1e-6);
7995 }
7996
7997 #[test]
7998 fn per_axis_resize_stretches_one_axis_and_keeps_text_uniform() {
7999 let mut k = ElementKind::Rect(BoxGeom {
8001 x: 10.0,
8002 y: 10.0,
8003 w: 20.0,
8004 h: 20.0,
8005 width: 1.0,
8006 rotation: 0.0,
8007 });
8008 resize_about(&mut k, 10.0, 10.0, 2.0, 1.0);
8009 match k {
8010 ElementKind::Rect(b) => {
8011 assert_eq!((b.x, b.y), (10.0, 10.0));
8012 assert_eq!((b.w, b.h), (40.0, 20.0));
8013 }
8014 other => panic!("expected rect, got {other:?}"),
8015 }
8016 let mut t = ElementKind::Text(TextGeom {
8019 x: 0.0,
8020 y: 0.0,
8021 content: "hi".into(),
8022 size: 10.0,
8023 rotation: 0.0,
8024 measured_w: 0.0,
8025 measured_h: 0.0,
8026 });
8027 resize_about(&mut t, 0.0, 0.0, 4.0, 1.0);
8028 match t {
8029 ElementKind::Text(t) => assert!((t.size - 20.0).abs() < 1e-3, "{}", t.size),
8030 other => panic!("expected text, got {other:?}"),
8031 }
8032 }
8033
8034 #[test]
8035 fn color_round_trips_through_hsv_and_packed_ints() {
8036 for c in [0xff0000ff, 0x00ff00ff, 0x0000ffff, 0x808080ff, 0xffffffff] {
8038 let (h, s, v) = u32_to_hsv(c);
8039 assert_eq!(hsv_to_u32(h, s, v), c, "{c:#010x}");
8040 }
8041 assert_eq!(hsv_to_u32(0.0, 1.0, 1.0), 0xff0000ff);
8043 assert_eq!(hsv_to_u32(1.0, 1.0, 1.0), 0xff0000ff);
8044 assert_eq!(hsv_to_u32(2.0 / 3.0, 1.0, 1.0), 0x0000ffff);
8046 assert_eq!(pack_rgba(1.5, -0.2, 0.5, 1.0), 0xff0080ff);
8048 }
8049
8050 #[test]
8051 fn rotation_accumulates_on_boxes_and_bakes_into_segments() {
8052 use std::f32::consts::FRAC_PI_2;
8053 let mut k = ElementKind::Rect(BoxGeom {
8055 x: -10.0,
8056 y: -10.0,
8057 w: 20.0,
8058 h: 20.0,
8059 width: 1.0,
8060 rotation: 0.0,
8061 });
8062 rotate_element(&mut k, 0.0, 0.0, FRAC_PI_2);
8063 match &k {
8064 ElementKind::Rect(b) => assert!((b.rotation - FRAC_PI_2).abs() < 1e-5),
8065 other => panic!("expected rect, got {other:?}"),
8066 }
8067 let bb = bbox(&k);
8069 assert!(
8070 (bb.0 + 10.0).abs() < 1e-3 && (bb.2 - 10.0).abs() < 1e-3,
8071 "{bb:?}"
8072 );
8073
8074 let mut seg = ElementKind::Line(SegGeom {
8077 x1: 0.0,
8078 y1: 0.0,
8079 x2: 10.0,
8080 y2: 0.0,
8081 width: 1.0,
8082 style: SegmentStyle::Solid,
8083 start_anchor: None,
8084 end_anchor: None,
8085 });
8086 rotate_element(&mut seg, 0.0, 0.0, FRAC_PI_2);
8087 match seg {
8088 ElementKind::Line(s) => {
8089 assert!(s.x2.abs() < 1e-3 && (s.y2 - 10.0).abs() < 1e-3, "{s:?}");
8090 }
8091 other => panic!("expected line, got {other:?}"),
8092 }
8093
8094 let mut txt = ElementKind::Text(TextGeom {
8097 x: -20.0,
8098 y: -8.0,
8099 content: "hi".into(),
8100 size: 16.0,
8101 rotation: 0.0,
8102 measured_w: 40.0,
8103 measured_h: 16.0,
8104 });
8105 rotate_element(&mut txt, 0.0, 0.0, FRAC_PI_2);
8106 match txt {
8107 ElementKind::Text(t) => {
8108 assert!((t.rotation - FRAC_PI_2).abs() < 1e-5);
8109 assert!(
8110 (t.x + 20.0).abs() < 1e-3 && (t.y + 8.0).abs() < 1e-3,
8111 "{t:?}"
8112 );
8113 }
8114 other => panic!("expected text, got {other:?}"),
8115 }
8116
8117 let mut orb = ElementKind::Rect(BoxGeom {
8120 x: 0.5,
8121 y: -0.5,
8122 w: 1.0,
8123 h: 1.0,
8124 width: 1.0,
8125 rotation: 0.0,
8126 });
8127 rotate_element(&mut orb, 0.0, 0.0, FRAC_PI_2);
8128 match orb {
8129 ElementKind::Rect(b) => {
8130 let (ccx, ccy) = (b.x + 0.5, b.y + 0.5);
8131 assert!(ccx.abs() < 1e-3 && (ccy - 1.0).abs() < 1e-3, "{b:?}");
8132 }
8133 other => panic!("expected rect, got {other:?}"),
8134 }
8135 }
8136
8137 #[test]
8138 fn rotation_snaps_to_horizontal_and_vertical() {
8139 use std::f32::consts::{FRAC_PI_2, FRAC_PI_4};
8140 let step = std::f32::consts::PI / 12.0;
8141 assert!((snap_angle(FRAC_PI_2 - 0.05, false) - FRAC_PI_2).abs() < 1e-6);
8143 assert!(snap_angle(0.04, false).abs() < 1e-6);
8144 assert!((snap_angle(-FRAC_PI_2 + 0.03, false) + FRAC_PI_2).abs() < 1e-6);
8145 assert!((snap_angle(FRAC_PI_2 - 0.2, false) - (FRAC_PI_2 - 0.2)).abs() < 1e-6);
8147 assert!((snap_angle(FRAC_PI_4, false) - FRAC_PI_4).abs() < 1e-6);
8148 assert!((snap_angle(0.30, true) - step).abs() < 1e-4);
8150 }
8151
8152 #[test]
8153 fn caret_navigation_walks_chars_and_lines() {
8154 let s = "aébc";
8156 assert_eq!(caret_right(s, 0), 1); assert_eq!(caret_right(s, 1), 3); assert_eq!(caret_left(s, 3), 1); assert_eq!(caret_left(s, 0), 0); assert_eq!(caret_right(s, s.len()), s.len()); let m = "ab\ncde";
8163 assert_eq!(line_start(m, 5), 3); assert_eq!(line_end(m, 0), 2); assert_eq!(line_start(m, 1), 0);
8166 assert_eq!(line_end(m, 4), m.len());
8167 assert_eq!(floor_boundary(s, 2), 1); assert_eq!(floor_boundary(s, 99), s.len());
8170 }
8171
8172 #[test]
8173 fn word_range_selects_the_word_under_the_caret() {
8174 let s = "foo bar_baz qux";
8175 assert_eq!(word_range(s, 1), (0, 3)); assert_eq!(word_range(s, 7), (4, 11)); assert_eq!(word_range(s, 3), (0, 3));
8179 assert_eq!(word_range("a b", 2), (2, 2));
8181 }
8182
8183 #[test]
8184 fn text_bbox_anchors_at_origin_and_grows() {
8185 let t = TextGeom {
8186 x: 5.0,
8187 y: 6.0,
8188 content: "ab\ncde".into(),
8189 size: 10.0,
8190 rotation: 0.0,
8191 measured_w: 0.0,
8192 measured_h: 0.0,
8193 };
8194 let bb = bbox(&ElementKind::Text(t));
8195 assert_eq!((bb.0, bb.1), (5.0, 6.0));
8196 assert!(bb.2 > bb.0 && bb.3 > bb.1);
8197 }
8198
8199 #[test]
8200 fn tiny_drags_are_not_committed() {
8201 assert!(!committable(&ElementKind::Draw(Stroke {
8202 points: vec![[0.0, 0.0]],
8203 width: 1.0,
8204 })));
8205 assert!(committable(&ElementKind::Rect(BoxGeom {
8206 x: 0.0,
8207 y: 0.0,
8208 w: 20.0,
8209 h: 5.0,
8210 width: 1.0,
8211 rotation: 0.0,
8212 })));
8213 }
8214
8215 #[test]
8216 fn image_round_trips_and_behaves_like_a_box() {
8217 let kind = ElementKind::Image(ImageGeom {
8218 src: "images/x.png".into(),
8219 x: 10.0,
8220 y: 20.0,
8221 w: 100.0,
8222 h: 60.0,
8223 rotation: 0.0,
8224 });
8225 assert_eq!(bbox(&kind), (10.0, 20.0, 110.0, 80.0));
8227 assert!(!is_closed_shape(&kind));
8228 let elem = Element {
8230 id: 1,
8231 kind,
8232 stroke: None,
8233 fill: None,
8234 label: None,
8235 label_color: None,
8236 styles: Vec::new(),
8237 mindmap: None,
8238 };
8239 let json = serde_json::to_string(&elem).unwrap();
8240 assert!(json.contains("\"image\""), "{json}");
8241 assert!(json.contains("images/x.png"));
8242 let mut back = serde_json::from_str::<Element>(&json).unwrap().kind;
8243 assert_eq!(bbox(&back), (10.0, 20.0, 110.0, 80.0));
8244 translate(&mut back, 5.0, -3.0);
8246 assert_eq!(bbox(&back), (15.0, 17.0, 115.0, 77.0));
8247 }
8248
8249 #[test]
8250 fn new_box_shapes_share_box_behavior_and_round_trip() {
8251 let b = BoxGeom {
8252 x: 1.0,
8253 y: 2.0,
8254 w: 30.0,
8255 h: 40.0,
8256 width: 2.0,
8257 rotation: 0.5,
8258 };
8259 let cases = [
8261 ("diamond", ElementKind::Diamond(b)),
8262 ("triangle", ElementKind::Triangle(b)),
8263 ("round_rect", ElementKind::RoundRect(b)),
8264 ("star", ElementKind::Star(b)),
8265 ("hexagon", ElementKind::Hexagon(b)),
8266 ];
8267 for (tag, kind) in cases {
8268 assert!(is_closed_shape(&kind), "{tag} should be fillable");
8272 assert!(committable(&kind), "{tag} should commit");
8273 assert_eq!(
8274 box_like(&kind),
8275 Some((1.0, 2.0, 30.0, 40.0, 0.5)),
8276 "{tag} box_like"
8277 );
8278 let elem = Element {
8280 id: 7,
8281 kind,
8282 stroke: None,
8283 fill: None,
8284 label: None,
8285 label_color: None,
8286 styles: Vec::new(),
8287 mindmap: None,
8288 };
8289 let json = serde_json::to_string(&elem).unwrap();
8290 assert!(json.contains(tag), "{tag} not in json: {json}");
8291 let back: Element = serde_json::from_str(&json).unwrap();
8292 assert_eq!(box_like(&back.kind), Some((1.0, 2.0, 30.0, 40.0, 0.5)));
8293 }
8294 }
8295}