1use crate::model::{MaskShape, ShapeKind};
15use crate::theme::Palette;
16use eframe::egui;
17use egui::{Align2, Color32, CornerRadius, FontId, Key, Modifiers, Sense, Stroke, StrokeKind};
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
20pub enum Tool {
21 #[default]
22 Select,
23 Text,
24 Shape(ShapeKind),
25 Draw,
26 Mask(MaskShape),
27 Zoom,
28 Cut,
30 Marker,
32 Stretch,
34 Spacer,
36}
37
38pub struct ToolsState {
39 pub tool: Tool,
40 pub fill: [u8; 4],
42 pub stroke: [u8; 4],
43 pub stroke_width: f32,
44 pub sides: u32,
45 pub corner: f32,
46 pub brush: [u8; 4],
48 pub brush_width: f32,
49 pub draw_rate: f32,
50 pub page: [u8; 4],
51 pub recording: bool,
54}
55
56impl Default for ToolsState {
57 fn default() -> Self {
58 Self {
59 tool: Tool::Select,
60 fill: [255, 255, 255, 255],
61 stroke: [0, 0, 0, 0],
62 stroke_width: 4.0,
63 sides: 5,
64 corner: 0.0,
65 brush: [255, 80, 80, 255],
66 brush_width: 6.0,
67 draw_rate: 1.0,
68 page: [0, 0, 0, 0],
69 recording: false,
70 }
71 }
72}
73
74#[derive(Clone, Copy, PartialEq, Eq, Debug)]
76pub(crate) enum Dir {
77 Up,
78 Down,
79 Left,
80 Right,
81}
82
83impl Dir {
84 fn unit(self) -> egui::Vec2 {
86 match self {
87 Dir::Up => egui::vec2(0.0, -1.0),
88 Dir::Down => egui::vec2(0.0, 1.0),
89 Dir::Left => egui::vec2(-1.0, 0.0),
90 Dir::Right => egui::vec2(1.0, 0.0),
91 }
92 }
93}
94
95#[derive(Clone, Copy, PartialEq, Eq, Debug)]
101pub(crate) enum Glyph {
102 Cursor,
103 Letter(char),
104 Rect,
105 Ellipse,
106 Poly(u32),
107 Star,
108 Line,
109 Arrow,
110 Pencil,
111 Mask,
112 Zoom,
113 Razor,
114 Flag,
115 Hourglass,
116 Eye,
117 EyeOff,
118 Diamond,
119 Record,
120 Mic,
121 Headphone,
122 SpeakerOn,
123 SpeakerOff,
124 Camera,
125 FilmStrip,
126 Nodes,
128 Layers,
130 Target,
132 Magnet,
134 Spacer,
136 MusicNote,
138 Folder,
140 Container,
142 Cross,
144 Dot,
146 Copy,
148 Paste,
150 Tri(Dir),
152 Skip(Dir),
154 Jump(Dir),
156 Play,
157 Pause,
158 Stop,
159 Fullscreen,
161 PopOut,
163 Indent(bool),
165 Sequence,
167 Template,
169 Snowflake,
171 FilmReel,
173 Bolt,
175 ImportArrow,
177 ExportArrow,
179 Transition,
181 Subtitles,
183 Gear,
185 Sliders,
187 Wrench,
189 Clapperboard,
191 Waveform,
193 CurveIcon,
195 Clock,
197 Notepad,
199 Bookmark,
201 UndoArrow(Dir),
203 FloppyDisk,
205 Terminal,
207}
208
209impl Glyph {
210 pub const ALL: &'static [Glyph] = &[
213 Glyph::Cursor,
214 Glyph::Letter('T'),
215 Glyph::Rect,
216 Glyph::Ellipse,
217 Glyph::Poly(5),
218 Glyph::Star,
219 Glyph::Line,
220 Glyph::Arrow,
221 Glyph::Pencil,
222 Glyph::Mask,
223 Glyph::Zoom,
224 Glyph::Razor,
225 Glyph::Flag,
226 Glyph::Hourglass,
227 Glyph::Eye,
228 Glyph::EyeOff,
229 Glyph::Diamond,
230 Glyph::Record,
231 Glyph::Mic,
232 Glyph::Headphone,
233 Glyph::SpeakerOn,
234 Glyph::SpeakerOff,
235 Glyph::Camera,
236 Glyph::FilmStrip,
237 Glyph::Nodes,
238 Glyph::Layers,
239 Glyph::Target,
240 Glyph::Magnet,
241 Glyph::Spacer,
242 Glyph::MusicNote,
243 Glyph::Folder,
244 Glyph::Container,
245 Glyph::Cross,
246 Glyph::Dot,
247 Glyph::Copy,
248 Glyph::Paste,
249 Glyph::Tri(Dir::Up),
250 Glyph::Tri(Dir::Down),
251 Glyph::Tri(Dir::Left),
252 Glyph::Tri(Dir::Right),
253 Glyph::Skip(Dir::Left),
254 Glyph::Skip(Dir::Right),
255 Glyph::Jump(Dir::Left),
256 Glyph::Jump(Dir::Right),
257 Glyph::Play,
258 Glyph::Pause,
259 Glyph::Stop,
260 Glyph::Fullscreen,
261 Glyph::PopOut,
262 Glyph::Indent(true),
263 Glyph::Indent(false),
264 Glyph::Sequence,
265 Glyph::Template,
266 Glyph::Snowflake,
267 Glyph::FilmReel,
268 Glyph::Bolt,
269 Glyph::ImportArrow,
270 Glyph::ExportArrow,
271 Glyph::Transition,
272 Glyph::Subtitles,
273 Glyph::Gear,
274 Glyph::Sliders,
275 Glyph::Wrench,
276 Glyph::Clapperboard,
277 Glyph::Waveform,
278 Glyph::CurveIcon,
279 Glyph::Clock,
280 Glyph::Notepad,
281 Glyph::Bookmark,
282 Glyph::UndoArrow(Dir::Left),
283 Glyph::UndoArrow(Dir::Right),
284 Glyph::FloppyDisk,
285 Glyph::Terminal,
286 ];
287
288 pub fn name(self) -> &'static str {
291 match self {
292 Glyph::Cursor => "cursor",
293 Glyph::Letter(_) => "letter",
294 Glyph::Rect => "rect",
295 Glyph::Ellipse => "ellipse",
296 Glyph::Poly(_) => "poly",
297 Glyph::Star => "star",
298 Glyph::Line => "line",
299 Glyph::Arrow => "arrow",
300 Glyph::Pencil => "pencil",
301 Glyph::Mask => "mask",
302 Glyph::Zoom => "zoom",
303 Glyph::Razor => "razor",
304 Glyph::Flag => "flag",
305 Glyph::Hourglass => "hourglass",
306 Glyph::Eye => "eye",
307 Glyph::EyeOff => "eye-off",
308 Glyph::Diamond => "diamond",
309 Glyph::Record => "record",
310 Glyph::Mic => "mic",
311 Glyph::Headphone => "headphone",
312 Glyph::SpeakerOn => "speaker-on",
313 Glyph::SpeakerOff => "speaker-off",
314 Glyph::Camera => "camera",
315 Glyph::FilmStrip => "film-strip",
316 Glyph::Nodes => "nodes",
317 Glyph::Layers => "layers",
318 Glyph::Target => "target",
319 Glyph::Magnet => "magnet",
320 Glyph::Spacer => "spacer",
321 Glyph::MusicNote => "music-note",
322 Glyph::Folder => "folder",
323 Glyph::Container => "container",
324 Glyph::Cross => "cross",
325 Glyph::Dot => "dot",
326 Glyph::Copy => "copy",
327 Glyph::Paste => "paste",
328 Glyph::Tri(Dir::Up) => "tri-up",
329 Glyph::Tri(Dir::Down) => "tri-down",
330 Glyph::Tri(Dir::Left) => "tri-left",
331 Glyph::Tri(Dir::Right) => "tri-right",
332 Glyph::Skip(Dir::Up) => "skip-up",
333 Glyph::Skip(Dir::Down) => "skip-down",
334 Glyph::Skip(Dir::Left) => "skip-left",
335 Glyph::Skip(Dir::Right) => "skip-right",
336 Glyph::Jump(Dir::Up) => "jump-up",
337 Glyph::Jump(Dir::Down) => "jump-down",
338 Glyph::Jump(Dir::Left) => "jump-left",
339 Glyph::Jump(Dir::Right) => "jump-right",
340 Glyph::Play => "play",
341 Glyph::Pause => "pause",
342 Glyph::Stop => "stop",
343 Glyph::Fullscreen => "fullscreen",
344 Glyph::PopOut => "pop-out",
345 Glyph::Indent(true) => "indent",
346 Glyph::Indent(false) => "outdent",
347 Glyph::Sequence => "sequence",
348 Glyph::Template => "template",
349 Glyph::Snowflake => "snowflake",
350 Glyph::FilmReel => "film-reel",
351 Glyph::Bolt => "bolt",
352 Glyph::ImportArrow => "import",
353 Glyph::ExportArrow => "export",
354 Glyph::Transition => "transition",
355 Glyph::Subtitles => "subtitles",
356 Glyph::Gear => "gear",
357 Glyph::Sliders => "sliders",
358 Glyph::Wrench => "wrench",
359 Glyph::Clapperboard => "clapperboard",
360 Glyph::Waveform => "waveform",
361 Glyph::CurveIcon => "curve",
362 Glyph::Clock => "clock",
363 Glyph::Notepad => "notepad",
364 Glyph::Bookmark => "bookmark",
365 Glyph::UndoArrow(Dir::Left) => "undo",
366 Glyph::UndoArrow(_) => "redo",
367 Glyph::FloppyDisk => "floppy-disk",
368 Glyph::Terminal => "terminal",
369 }
370 }
371
372 pub fn from_name(s: &str) -> Option<Glyph> {
374 Self::ALL.iter().copied().find(|g| g.name() == s)
375 }
376}
377
378const STRIP: [(Tool, Glyph, &str); 16] = [
379 (Tool::Select, Glyph::Cursor, "Select"),
380 (Tool::Cut, Glyph::Razor, "Cut"),
381 (Tool::Marker, Glyph::Flag, "Marker"),
382 (Tool::Stretch, Glyph::Hourglass, "Stretch"),
383 (Tool::Spacer, Glyph::Spacer, "Spacer"),
384 (Tool::Text, Glyph::Letter('T'), "Text"),
385 (Tool::Shape(ShapeKind::Rect), Glyph::Rect, "Rectangle"),
386 (Tool::Shape(ShapeKind::Ellipse), Glyph::Ellipse, "Ellipse"),
387 (Tool::Shape(ShapeKind::Triangle), Glyph::Poly(3), "Triangle"),
388 (Tool::Shape(ShapeKind::Polygon), Glyph::Poly(5), "Polygon"),
389 (Tool::Shape(ShapeKind::Star), Glyph::Star, "Star"),
390 (Tool::Shape(ShapeKind::Line), Glyph::Line, "Line"),
391 (Tool::Shape(ShapeKind::Arrow), Glyph::Arrow, "Arrow"),
392 (Tool::Draw, Glyph::Pencil, "Draw"),
393 (Tool::Mask(MaskShape::Rect), Glyph::Mask, "Mask"),
394 (Tool::Zoom, Glyph::Zoom, "Zoom"),
395];
396
397pub fn tool_hotkey(tool: Tool) -> Option<Key> {
400 match tool {
401 Tool::Select => Some(Key::V),
402 Tool::Text => Some(Key::T),
403 Tool::Draw => Some(Key::D),
404 Tool::Mask(_) => Some(Key::M),
405 Tool::Cut => Some(Key::C),
406 Tool::Stretch => Some(Key::R),
407 Tool::Shape(_) | Tool::Zoom | Tool::Marker | Tool::Spacer => None,
408 }
409}
410
411pub fn handle_hotkeys(ctx: &egui::Context, state: &mut ToolsState) -> Option<Tool> {
416 if ctx.wants_keyboard_input() {
417 return None;
418 }
419 ctx.input_mut(|i| {
420 if i.consume_key(Modifiers::SHIFT, Key::S) {
426 let next = Tool::Shape(next_shape(state.tool));
427 state.tool = next;
428 return Some(next);
429 }
430 if !i.modifiers.is_none() {
431 return None;
432 }
433 let next = if i.consume_key(Modifiers::NONE, Key::V) {
434 Tool::Select
435 } else if i.consume_key(Modifiers::NONE, Key::T) {
436 Tool::Text
437 } else if i.consume_key(Modifiers::NONE, Key::D) {
438 Tool::Draw
439 } else if i.consume_key(Modifiers::NONE, Key::M) {
440 Tool::Mask(next_mask(state.tool))
441 } else if i.consume_key(Modifiers::NONE, Key::C) {
442 Tool::Cut
443 } else if i.consume_key(Modifiers::NONE, Key::R) {
444 Tool::Stretch
445 } else {
446 return None;
447 };
448 (next != state.tool).then(|| {
449 state.tool = next;
450 next
451 })
452 })
453}
454
455pub fn handle_snap_hotkey(ctx: &egui::Context, snap: &mut bool) -> bool {
459 if ctx.wants_keyboard_input() {
460 return false;
461 }
462 let pressed = ctx.input_mut(|i| i.modifiers.is_none() && i.consume_key(Modifiers::NONE, Key::S));
463 if pressed {
464 *snap = !*snap;
465 }
466 pressed
467}
468
469const SHAPE_CYCLE: [ShapeKind; 7] = [
471 ShapeKind::Rect,
472 ShapeKind::Ellipse,
473 ShapeKind::Triangle,
474 ShapeKind::Polygon,
475 ShapeKind::Star,
476 ShapeKind::Line,
477 ShapeKind::Arrow,
478];
479
480fn next_shape(cur: Tool) -> ShapeKind {
481 match cur {
482 Tool::Shape(k) => match SHAPE_CYCLE.iter().position(|c| *c == k) {
483 Some(i) => SHAPE_CYCLE[(i + 1) % SHAPE_CYCLE.len()],
484 None => SHAPE_CYCLE[0],
485 },
486 _ => SHAPE_CYCLE[0],
487 }
488}
489
490fn next_mask(cur: Tool) -> MaskShape {
491 match cur {
492 Tool::Mask(m) => match MaskShape::ALL.iter().position(|c| *c == m) {
493 Some(i) => MaskShape::ALL[(i + 1) % MaskShape::ALL.len()],
494 None => MaskShape::ALL[0],
495 },
496 _ => MaskShape::ALL[0],
497 }
498}
499
500pub fn show(ui: &mut egui::Ui, state: &mut ToolsState, palette: &Palette, snap: &mut bool) -> bool {
503 let mut changed = handle_hotkeys(ui.ctx(), state).is_some();
504 changed |= handle_snap_hotkey(ui.ctx(), snap);
505 let base = ui.id();
506 ui.horizontal_wrapped(|ui| {
508 ui.spacing_mut().item_spacing.x = 2.0;
509 for (tool, icon, name) in STRIP {
510 let active = same_tool(tool, state.tool);
511 let tip = if matches!(tool, Tool::Shape(_)) {
512 format!("{name} (Shift+S)")
513 } else {
514 match tool_hotkey(tool) {
515 Some(k) => format!("{name} ({})", k.name()),
516 None if tool == Tool::Spacer => format!("{name} — drag the lanes to open or close a gap"),
518 None => name.to_string(),
519 }
520 };
521 if icon_button(ui, palette, base.with(("tool", name)), icon, &tip, active).clicked() && !active {
522 state.tool = tool;
523 changed = true;
524 }
525 }
526 ui.separator();
527 if icon_button(ui, palette, base.with("snap"), Glyph::Magnet, "Snapping (S)", *snap).clicked() {
528 *snap = !*snap;
529 changed = true;
530 }
531 ui.separator();
532 changed |= style_controls(ui, state, palette);
533 });
534 changed
535}
536
537fn same_tool(entry: Tool, cur: Tool) -> bool {
540 matches!((entry, cur), (Tool::Mask(_), Tool::Mask(_))) || entry == cur
541}
542
543fn style_controls(ui: &mut egui::Ui, state: &mut ToolsState, palette: &Palette) -> bool {
545 let mut changed = false;
546 match state.tool {
547 Tool::Shape(kind) => {
548 let outline_only = matches!(kind, ShapeKind::Line | ShapeKind::Arrow);
549 if !outline_only {
550 ui.label("Fill");
551 changed |= ui.color_edit_button_srgba_unmultiplied(&mut state.fill).changed();
552 }
553 ui.label("Stroke");
554 changed |= ui.color_edit_button_srgba_unmultiplied(&mut state.stroke).changed();
555 changed |= ui
556 .add(egui::DragValue::new(&mut state.stroke_width).speed(0.2).range(0.0..=200.0))
557 .on_hover_text("Stroke width (px)")
558 .changed();
559 match kind {
560 ShapeKind::Polygon | ShapeKind::Star => {
561 ui.label("Sides");
562 changed |= ui.add(egui::DragValue::new(&mut state.sides).range(3..=64)).changed();
563 }
564 ShapeKind::Rect => {
565 ui.label("Corner");
566 changed |= ui.add(egui::DragValue::new(&mut state.corner).speed(0.5).range(0.0..=2000.0)).changed();
567 }
568 ShapeKind::Arrow => {
569 ui.label("Head");
570 changed |= ui
571 .add(egui::DragValue::new(&mut state.corner).speed(0.5).range(0.0..=2000.0))
572 .on_hover_text("Arrow head size (px, 0 = follow the stroke width)")
573 .changed();
574 }
575 _ => {}
576 }
577 }
578 Tool::Draw => {
579 let rec = state.recording;
581 let tip = "Play and record: every stroke joins one drawing until the video stops";
582 if icon_button(ui, palette, ui.id().with("draw-rec"), Glyph::Record, tip, rec).clicked() {
583 state.recording = !rec;
584 changed = true;
585 }
586 ui.label("Brush");
587 changed |= ui.color_edit_button_srgba_unmultiplied(&mut state.brush).changed();
588 changed |= ui
589 .add(egui::DragValue::new(&mut state.brush_width).speed(0.2).range(0.5..=200.0))
590 .on_hover_text("Brush width (px)")
591 .changed();
592 ui.label("Speed");
593 for (rate, label) in [(0.5, "0.5x"), (1.0, "1x"), (2.0, "2x"), (0.0, "all")] {
594 let on = (state.draw_rate - rate).abs() < 1e-3;
595 if ui
596 .selectable_label(on, label)
597 .on_hover_text(if rate == 0.0 {
598 "Show the whole sketch at once"
599 } else {
600 "Playback speed of the recorded drawing"
601 })
602 .clicked()
603 && !on
604 {
605 state.draw_rate = rate;
606 changed = true;
607 }
608 }
609 let mut page = state.page[3] > 0;
610 if ui.checkbox(&mut page, "Page").changed() {
611 state.page[3] = if page { 255 } else { 0 };
612 if page && state.page[..3] == [0, 0, 0] {
613 state.page = [255, 255, 255, 255];
614 }
615 changed = true;
616 }
617 if page {
618 changed |= ui.color_edit_button_srgba_unmultiplied(&mut state.page).changed();
619 }
620 }
621 Tool::Mask(shape) => {
622 ui.label("Mask");
623 let mut pick = shape;
624 egui::ComboBox::from_id_salt("tool-mask-shape").selected_text(shape.name()).width(90.0).show_ui(ui, |ui| {
625 for m in MaskShape::ALL {
626 ui.selectable_value(&mut pick, m, m.name());
627 }
628 });
629 if pick != shape {
630 state.tool = Tool::Mask(pick);
631 changed = true;
632 }
633 }
634 Tool::Text | Tool::Select | Tool::Zoom | Tool::Cut | Tool::Marker | Tool::Stretch | Tool::Spacer => {}
635 }
636 changed
637}
638
639pub(crate) fn glyph_text_button(ui: &mut egui::Ui, icon: Glyph, text: &str) -> egui::Response {
643 let font = egui::TextStyle::Button.resolve(ui.style());
644 let galley = ui.painter().layout_no_wrap(text.to_owned(), font, Color32::PLACEHOLDER);
645 let pad = ui.spacing().button_padding;
646 let icon_w = 18.0;
647 let size = egui::vec2(icon_w + galley.size().x + pad.x * 2.0, galley.size().y.max(20.0) + pad.y * 2.0);
648 let (rect, r) = ui.allocate_exact_size(size, Sense::click());
649 let v = ui.style().interact(&r);
650 ui.painter().rect(rect, v.corner_radius, v.weak_bg_fill, v.bg_stroke, StrokeKind::Inside);
651 let icon_x = if text.is_empty() { rect.center().x - icon_w / 2.0 } else { rect.left() + pad.x };
652 let icon_rect = egui::Rect::from_min_size(egui::pos2(icon_x, rect.top()), egui::vec2(icon_w, rect.height()));
653 draw_glyph(ui.painter(), icon_rect, icon, v.text_color());
654 let tp = egui::pos2(icon_rect.right(), rect.center().y - galley.size().y / 2.0);
655 ui.painter().galley(tp, galley, v.text_color());
656 r
657}
658
659pub(crate) fn icon_button(
661 ui: &mut egui::Ui,
662 palette: &Palette,
663 id: egui::Id,
664 icon: Glyph,
665 tip: &str,
666 active: bool,
667) -> egui::Response {
668 let (rect, _) = ui.allocate_exact_size(egui::vec2(24.0, 22.0), Sense::hover());
669 let r = ui.interact(rect, id, Sense::click());
670 let p = ui.painter();
671 if active {
672 p.rect_filled(rect, CornerRadius::same(2), palette.accent);
673 } else if r.hovered() {
674 p.rect_filled(rect, CornerRadius::same(2), palette.header);
675 p.rect_stroke(rect, CornerRadius::same(2), Stroke::new(1.0, palette.border), StrokeKind::Inside);
676 }
677 let fg = if active { on_accent(palette.accent) } else { palette.text };
678 draw_glyph(p, rect, icon, fg);
679 r.on_hover_text(tip)
680}
681
682pub(crate) fn draw_glyph(p: &egui::Painter, rect: egui::Rect, g: Glyph, fg: Color32) {
684 let c = rect.center();
685 let r = 6.0; let stroke = Stroke::new(1.4, fg);
687 let poly = |n: u32, rot: f32, rad: f32| -> Vec<egui::Pos2> {
688 (0..n)
689 .map(|i| {
690 let a = rot + std::f32::consts::TAU * i as f32 / n as f32;
691 c + egui::vec2(a.cos() * rad, a.sin() * rad)
692 })
693 .collect()
694 };
695 let tri = |ctr: egui::Pos2, d: Dir, depth: f32, half: f32| -> Vec<egui::Pos2> {
697 let u = d.unit();
698 let n = egui::vec2(-u.y, u.x);
699 vec![ctr + u * depth, ctr - u * depth + n * half, ctr - u * depth - n * half]
700 };
701 match g {
702 Glyph::Letter(ch) => {
703 p.text(c, Align2::CENTER_CENTER, ch, FontId::proportional(13.0), fg);
704 }
705 Glyph::Cursor => {
707 let pts = vec![
708 c + egui::vec2(-3.5, -6.5),
709 c + egui::vec2(-3.5, 5.5),
710 c + egui::vec2(-0.5, 2.5),
711 c + egui::vec2(1.5, 6.5),
712 c + egui::vec2(3.5, 5.5),
713 c + egui::vec2(1.5, 1.8),
714 c + egui::vec2(5.0, 1.5),
715 ];
716 p.add(egui::Shape::convex_polygon(pts, fg, Stroke::NONE));
717 }
718 Glyph::Rect => {
719 p.rect_stroke(
720 egui::Rect::from_center_size(c, egui::vec2(12.0, 9.0)),
721 CornerRadius::ZERO,
722 stroke,
723 StrokeKind::Inside,
724 );
725 }
726 Glyph::Ellipse => {
727 p.circle_stroke(c, r, stroke);
728 }
729 Glyph::Poly(n) => {
730 let pts = poly(n, -std::f32::consts::FRAC_PI_2, r);
731 p.add(egui::Shape::closed_line(pts, stroke));
732 }
733 Glyph::Star => {
734 let outer = poly(5, -std::f32::consts::FRAC_PI_2, r);
735 let inner = poly(5, -std::f32::consts::FRAC_PI_2 + std::f32::consts::TAU / 10.0, r * 0.45);
736 let mut pts = Vec::with_capacity(10);
737 for i in 0..5 {
738 pts.push(outer[i]);
739 pts.push(inner[i]);
740 }
741 p.add(egui::Shape::closed_line(pts, stroke));
742 }
743 Glyph::Line => {
744 p.line_segment([c + egui::vec2(-6.0, 5.0), c + egui::vec2(6.0, -5.0)], stroke);
745 }
746 Glyph::Arrow => {
747 p.line_segment([c + egui::vec2(-6.0, 0.0), c + egui::vec2(4.0, 0.0)], stroke);
748 let head = vec![c + egui::vec2(6.5, 0.0), c + egui::vec2(2.5, -3.2), c + egui::vec2(2.5, 3.2)];
749 p.add(egui::Shape::convex_polygon(head, fg, Stroke::NONE));
750 }
751 Glyph::Pencil => {
753 let body = vec![
754 c + egui::vec2(1.0, -6.0),
755 c + egui::vec2(5.5, -1.5),
756 c + egui::vec2(-2.0, 6.0),
757 c + egui::vec2(-6.0, 6.5),
758 c + egui::vec2(-5.5, 2.5),
759 ];
760 p.add(egui::Shape::convex_polygon(body, fg, Stroke::NONE));
761 }
762 Glyph::Mask => {
764 p.circle_stroke(c, r, stroke);
765 let half: Vec<egui::Pos2> = (0..=12)
766 .map(|i| {
767 let a = std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * i as f32 / 12.0;
768 c + egui::vec2(a.cos() * r, a.sin() * r)
769 })
770 .collect();
771 p.add(egui::Shape::convex_polygon(half, fg, Stroke::NONE));
772 }
773 Glyph::Zoom => {
774 p.circle_stroke(c + egui::vec2(-1.0, -1.0), 4.5, stroke);
775 p.line_segment([c + egui::vec2(2.2, 2.2), c + egui::vec2(6.0, 6.0)], Stroke::new(1.8, fg));
776 }
777 Glyph::Razor => {
779 let body = egui::Rect::from_center_size(c + egui::vec2(0.0, -2.0), egui::vec2(12.0, 6.0));
780 p.rect_stroke(body, CornerRadius::same(1), stroke, StrokeKind::Inside);
781 p.line_segment([c + egui::vec2(-6.0, 1.5), c + egui::vec2(6.0, 1.5)], Stroke::new(2.0, fg));
782 for i in 0..4 {
783 let x = -4.5 + i as f32 * 3.0;
784 p.line_segment([c + egui::vec2(x, 1.5), c + egui::vec2(x, 5.0)], Stroke::new(1.0, fg));
785 }
786 }
787 Glyph::Spacer => {
789 for x in [-6.5, 6.5] {
790 p.line_segment([c + egui::vec2(x, -6.0), c + egui::vec2(x, 6.0)], stroke);
791 }
792 p.line_segment([c + egui::vec2(-4.5, 0.0), c + egui::vec2(4.5, 0.0)], stroke);
793 for (tip, back) in [(-5.5f32, -2.0f32), (5.5, 2.0)] {
794 let head = vec![c + egui::vec2(tip, 0.0), c + egui::vec2(back, -3.0), c + egui::vec2(back, 3.0)];
795 p.add(egui::Shape::convex_polygon(head, fg, Stroke::NONE));
796 }
797 }
798 Glyph::Flag => {
800 p.line_segment([c + egui::vec2(-4.0, -6.5), c + egui::vec2(-4.0, 6.5)], Stroke::new(1.6, fg));
801 let cloth = vec![c + egui::vec2(-4.0, -6.0), c + egui::vec2(5.5, -3.0), c + egui::vec2(-4.0, 0.0)];
802 p.add(egui::Shape::convex_polygon(cloth, fg, Stroke::NONE));
803 }
804 Glyph::Hourglass => {
806 p.line_segment([c + egui::vec2(-5.0, -6.5), c + egui::vec2(5.0, -6.5)], stroke);
807 p.line_segment([c + egui::vec2(-5.0, 6.5), c + egui::vec2(5.0, 6.5)], stroke);
808 p.add(egui::Shape::convex_polygon(
809 vec![c + egui::vec2(-4.5, -6.0), c + egui::vec2(4.5, -6.0), c],
810 fg,
811 Stroke::NONE,
812 ));
813 p.add(egui::Shape::convex_polygon(
814 vec![c + egui::vec2(-4.5, 6.0), c + egui::vec2(4.5, 6.0), c],
815 fg,
816 Stroke::NONE,
817 ));
818 }
819 Glyph::Eye | Glyph::EyeOff => {
821 let lid = |up: f32| -> Vec<egui::Pos2> {
822 (0..=16)
823 .map(|i| {
824 let x = -7.0 + i as f32 * 14.0 / 16.0;
825 let k: f32 = 1.0 - (x / 7.0) * (x / 7.0);
826 c + egui::vec2(x, up * 4.2 * k.max(0.0))
827 })
828 .collect()
829 };
830 p.add(egui::Shape::line(lid(-1.0), stroke));
831 p.add(egui::Shape::line(lid(1.0), stroke));
832 p.circle_filled(c, 2.2, fg);
833 if g == Glyph::EyeOff {
834 p.line_segment([c + egui::vec2(-6.5, -6.0), c + egui::vec2(6.5, 6.0)], Stroke::new(1.8, fg));
835 }
836 }
837 Glyph::Diamond => {
838 let pts = vec![
839 c + egui::vec2(0.0, -5.5),
840 c + egui::vec2(4.5, 0.0),
841 c + egui::vec2(0.0, 5.5),
842 c + egui::vec2(-4.5, 0.0),
843 ];
844 p.add(egui::Shape::convex_polygon(pts, fg, Stroke::NONE));
845 }
846 Glyph::Record => {
847 p.circle_filled(c, 5.5, Color32::from_rgb(220, 60, 60));
848 }
849 Glyph::Mic => {
851 p.rect_filled(
852 egui::Rect::from_center_size(c + egui::vec2(0.0, -2.5), egui::vec2(5.0, 8.0)),
853 CornerRadius::same(2),
854 fg,
855 );
856 p.line_segment([c + egui::vec2(0.0, 3.0), c + egui::vec2(0.0, 6.0)], Stroke::new(1.6, fg));
857 p.line_segment([c + egui::vec2(-3.5, 6.0), c + egui::vec2(3.5, 6.0)], Stroke::new(1.6, fg));
858 }
859 Glyph::Headphone => {
861 let band: Vec<egui::Pos2> = (0..=16)
862 .map(|i| {
863 let a = std::f32::consts::PI + std::f32::consts::PI * i as f32 / 16.0;
864 c + egui::vec2(a.cos() * 6.0, a.sin() * 6.0 + 1.0)
865 })
866 .collect();
867 p.add(egui::Shape::line(band, stroke));
868 for x in [-6.0, 6.0] {
869 p.rect_filled(
870 egui::Rect::from_center_size(c + egui::vec2(x, 3.0), egui::vec2(3.5, 6.0)),
871 CornerRadius::same(1),
872 fg,
873 );
874 }
875 }
876 Glyph::SpeakerOn | Glyph::SpeakerOff => {
878 let cone = vec![
879 c + egui::vec2(-6.0, -2.0),
880 c + egui::vec2(-3.0, -2.0),
881 c + egui::vec2(0.5, -5.5),
882 c + egui::vec2(0.5, 5.5),
883 c + egui::vec2(-3.0, 2.0),
884 c + egui::vec2(-6.0, 2.0),
885 ];
886 p.add(egui::Shape::convex_polygon(cone, fg, Stroke::NONE));
887 if g == Glyph::SpeakerOn {
888 for (i, rad) in [3.0_f32, 5.5].iter().enumerate() {
889 let arc: Vec<egui::Pos2> = (0..=10)
890 .map(|k| {
891 let a = -std::f32::consts::FRAC_PI_3 + 2.0 * std::f32::consts::FRAC_PI_3 * k as f32 / 10.0;
892 c + egui::vec2(1.5 + a.cos() * rad, a.sin() * rad)
893 })
894 .collect();
895 p.add(egui::Shape::line(arc, Stroke::new(1.3 - i as f32 * 0.2, fg)));
896 }
897 } else {
898 p.line_segment([c + egui::vec2(2.5, -3.5), c + egui::vec2(6.5, 3.5)], Stroke::new(1.6, fg));
899 p.line_segment([c + egui::vec2(6.5, -3.5), c + egui::vec2(2.5, 3.5)], Stroke::new(1.6, fg));
900 }
901 }
902 Glyph::Camera => {
904 let body = egui::Rect::from_center_size(c + egui::vec2(0.0, 1.0), egui::vec2(13.0, 9.0));
905 p.rect_stroke(body, CornerRadius::same(1), stroke, StrokeKind::Inside);
906 p.rect_filled(
907 egui::Rect::from_center_size(c + egui::vec2(-2.0, -4.5), egui::vec2(5.0, 2.5)),
908 CornerRadius::same(1),
909 fg,
910 );
911 p.circle_stroke(c + egui::vec2(0.0, 1.0), 3.0, stroke);
912 }
913 Glyph::FilmStrip => {
915 let body = egui::Rect::from_center_size(c, egui::vec2(13.0, 11.0));
916 p.rect_stroke(body, CornerRadius::same(1), stroke, StrokeKind::Inside);
917 for i in 0..3 {
918 let y = -3.5 + i as f32 * 3.5;
919 for x in [-4.8_f32, 4.8] {
920 p.rect_filled(
921 egui::Rect::from_center_size(c + egui::vec2(x, y), egui::vec2(2.2, 2.0)),
922 CornerRadius::ZERO,
923 fg,
924 );
925 }
926 }
927 }
928 Glyph::Nodes => {
930 let a = egui::Rect::from_center_size(c + egui::vec2(-3.5, -3.5), egui::vec2(7.0, 5.0));
931 let b = egui::Rect::from_center_size(c + egui::vec2(3.5, 3.5), egui::vec2(7.0, 5.0));
932 p.line_segment([a.right_center(), b.left_center()], stroke);
933 p.rect_stroke(a, CornerRadius::same(1), stroke, StrokeKind::Inside);
934 p.rect_stroke(b, CornerRadius::same(1), stroke, StrokeKind::Inside);
935 }
936 Glyph::Layers => {
938 for dy in [-4.0_f32, 0.0, 4.0] {
939 let sheet = egui::Rect::from_center_size(c + egui::vec2(0.0, dy), egui::vec2(12.0, 3.0));
940 p.rect_stroke(sheet, CornerRadius::same(1), stroke, StrokeKind::Inside);
941 }
942 }
943 Glyph::Magnet => {
945 let arc_c = c + egui::vec2(0.0, -1.0);
946 let arc_r = 5.0;
947 let arc: Vec<egui::Pos2> = (0..=12)
948 .map(|i| {
949 let a = std::f32::consts::PI + std::f32::consts::PI * i as f32 / 12.0;
950 arc_c + egui::vec2(a.cos() * arc_r, a.sin() * arc_r)
951 })
952 .collect();
953 p.add(egui::Shape::line(arc, stroke));
954 p.line_segment([arc_c + egui::vec2(-arc_r, 0.0), arc_c + egui::vec2(-arc_r, 6.5)], stroke);
955 p.line_segment([arc_c + egui::vec2(arc_r, 0.0), arc_c + egui::vec2(arc_r, 6.5)], stroke);
956 for x in [-arc_r, arc_r] {
957 p.rect_filled(
958 egui::Rect::from_center_size(arc_c + egui::vec2(x, 6.5), egui::vec2(3.0, 2.6)),
959 CornerRadius::same(1),
960 fg,
961 );
962 }
963 }
964 Glyph::Target => {
966 p.circle_stroke(c, r, stroke);
967 p.circle_stroke(c, r * 0.4, stroke);
968 for (dx, dy) in [(1.0_f32, 0.0_f32), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] {
969 p.line_segment(
970 [c + egui::vec2(dx * r, dy * r), c + egui::vec2(dx * (r + 2.5), dy * (r + 2.5))],
971 stroke,
972 );
973 }
974 }
975 Glyph::MusicNote => {
977 let head = c + egui::vec2(-2.5, 3.5);
978 p.circle_filled(head, 2.6, fg);
979 let stem_top = c + egui::vec2(2.0, -6.0);
980 p.line_segment([head + egui::vec2(2.4, -0.5), stem_top], Stroke::new(1.6, fg));
981 let flag = vec![stem_top, stem_top + egui::vec2(4.0, 1.5), stem_top + egui::vec2(0.5, 4.5)];
982 p.add(egui::Shape::convex_polygon(flag, fg, Stroke::NONE));
983 }
984 Glyph::Folder => {
985 let body = egui::Rect::from_center_size(c + egui::vec2(0.0, 1.5), egui::vec2(12.0, 8.0));
986 let tab = egui::Rect::from_min_size(body.left_top() - egui::vec2(0.0, 2.5), egui::vec2(5.0, 2.5));
987 p.rect_stroke(tab, CornerRadius::ZERO, stroke, StrokeKind::Inside);
988 p.rect_stroke(body, CornerRadius::ZERO, stroke, StrokeKind::Inside);
989 }
990 Glyph::Cross => {
991 let s = Stroke::new(1.7, fg);
992 p.line_segment([c + egui::vec2(-4.2, -4.2), c + egui::vec2(4.2, 4.2)], s);
993 p.line_segment([c + egui::vec2(4.2, -4.2), c + egui::vec2(-4.2, 4.2)], s);
994 }
995 Glyph::Dot => {
996 p.circle_filled(c, 3.6, fg);
997 }
998 Glyph::Copy => {
1000 p.rect_stroke(
1001 egui::Rect::from_min_size(c + egui::vec2(-6.0, -6.5), egui::vec2(8.0, 10.0)),
1002 CornerRadius::same(1),
1003 stroke,
1004 StrokeKind::Inside,
1005 );
1006 p.rect_filled(
1007 egui::Rect::from_min_size(c + egui::vec2(-2.0, -3.0), egui::vec2(8.0, 10.0)),
1008 CornerRadius::same(1),
1009 fg,
1010 );
1011 }
1012 Glyph::Paste => {
1014 p.rect_stroke(
1015 egui::Rect::from_center_size(c + egui::vec2(0.0, 1.0), egui::vec2(11.0, 12.0)),
1016 CornerRadius::same(1),
1017 stroke,
1018 StrokeKind::Inside,
1019 );
1020 p.rect_filled(
1021 egui::Rect::from_center_size(c + egui::vec2(0.0, -5.0), egui::vec2(6.5, 3.5)),
1022 CornerRadius::same(1),
1023 fg,
1024 );
1025 }
1026 Glyph::Tri(d) => {
1027 p.add(egui::Shape::convex_polygon(tri(c, d, 4.0, 4.2), fg, Stroke::NONE));
1028 }
1029 Glyph::Skip(d) => {
1030 let u = d.unit();
1031 for k in [-3.4, 3.4] {
1032 p.add(egui::Shape::convex_polygon(tri(c + u * k, d, 3.4, 4.6), fg, Stroke::NONE));
1033 }
1034 }
1035 Glyph::Jump(d) => {
1037 let u = d.unit();
1038 let n = egui::vec2(-u.y, u.x);
1039 p.line_segment([c + u * 5.5 - n * 5.0, c + u * 5.5 + n * 5.0], Stroke::new(2.0, fg));
1040 p.add(egui::Shape::convex_polygon(tri(c - u * 1.5, d, 3.8, 5.0), fg, Stroke::NONE));
1041 }
1042 Glyph::Play => {
1043 p.add(egui::Shape::convex_polygon(tri(c + egui::vec2(0.8, 0.0), Dir::Right, 5.5, 6.0), fg, Stroke::NONE));
1044 }
1045 Glyph::Pause => {
1046 for x in [-3.0_f32, 3.0] {
1047 p.rect_filled(
1048 egui::Rect::from_center_size(c + egui::vec2(x, 0.0), egui::vec2(3.0, 11.0)),
1049 CornerRadius::ZERO,
1050 fg,
1051 );
1052 }
1053 }
1054 Glyph::Stop => {
1055 p.rect_filled(egui::Rect::from_center_size(c, egui::vec2(10.0, 10.0)), CornerRadius::same(1), fg);
1056 }
1057 Glyph::Fullscreen => {
1059 for (sx, sy) in [(-1.0_f32, -1.0_f32), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
1060 let corner = c + egui::vec2(sx * 6.5, sy * 5.5);
1061 p.line_segment([corner, corner - egui::vec2(sx * 4.0, 0.0)], stroke);
1062 p.line_segment([corner, corner - egui::vec2(0.0, sy * 3.5)], stroke);
1063 }
1064 }
1065 Glyph::PopOut => {
1067 p.rect_stroke(
1068 egui::Rect::from_center_size(c + egui::vec2(-1.0, 1.5), egui::vec2(11.0, 10.0)),
1069 CornerRadius::same(1),
1070 stroke,
1071 StrokeKind::Inside,
1072 );
1073 p.line_segment([c + egui::vec2(0.0, -1.0), c + egui::vec2(6.0, -6.0)], stroke);
1074 p.add(egui::Shape::convex_polygon(
1075 vec![c + egui::vec2(6.5, -6.5), c + egui::vec2(1.5, -6.0), c + egui::vec2(6.0, -1.5)],
1076 fg,
1077 Stroke::NONE,
1078 ));
1079 }
1080 Glyph::Indent(inward) => {
1082 let d = if inward { Dir::Right } else { Dir::Left };
1083 let u = d.unit();
1084 p.line_segment([c + u * 6.0 + egui::vec2(0.0, -5.5), c + u * 6.0 + egui::vec2(0.0, 5.5)], stroke);
1085 p.line_segment([c - u * 6.0, c + u * 1.0], stroke);
1086 p.add(egui::Shape::convex_polygon(tri(c + u * 2.5, d, 2.5, 3.2), fg, Stroke::NONE));
1087 }
1088 Glyph::Sequence => {
1090 p.line_segment([c + egui::vec2(-6.5, 5.5), c + egui::vec2(6.5, 5.5)], stroke);
1091 for (x, h) in [(-6.0_f32, 9.0_f32), (0.5, 6.0)] {
1092 p.rect_filled(
1093 egui::Rect::from_min_size(c + egui::vec2(x, 4.0 - h), egui::vec2(5.5, h)),
1094 CornerRadius::same(1),
1095 fg,
1096 );
1097 }
1098 }
1099 Glyph::Container => {
1101 let outer = egui::Rect::from_center_size(c, egui::vec2(12.0, 10.0));
1102 let inner = egui::Rect::from_center_size(c, egui::vec2(6.0, 4.5));
1103 p.rect_stroke(outer, CornerRadius::same(1), stroke, StrokeKind::Inside);
1104 p.rect_filled(inner, CornerRadius::ZERO, fg);
1105 }
1106 Glyph::Template => {
1108 let fold = 4.0;
1109 let (l, t, rr, b) = (c.x - 5.0, c.y - 6.0, c.x + 5.0, c.y + 6.0);
1110 p.add(egui::Shape::closed_line(
1111 vec![
1112 egui::pos2(l, t),
1113 egui::pos2(rr - fold, t),
1114 egui::pos2(rr, t + fold),
1115 egui::pos2(rr, b),
1116 egui::pos2(l, b),
1117 ],
1118 stroke,
1119 ));
1120 p.add(egui::Shape::line(
1121 vec![egui::pos2(rr - fold, t), egui::pos2(rr - fold, t + fold), egui::pos2(rr, t + fold)],
1122 stroke,
1123 ));
1124 }
1125 Glyph::Snowflake => {
1127 let thin = Stroke::new(1.0, fg);
1128 for i in 0..3 {
1129 let a = std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * i as f32 / 3.0;
1130 let arm = egui::vec2(a.cos(), a.sin());
1131 p.line_segment([c - arm * 6.5, c + arm * 6.5], thin);
1132 let barb = egui::vec2(-arm.y, arm.x);
1133 for s in [-1.0_f32, 1.0] {
1134 let tip = c + arm * (6.5 * s);
1135 p.line_segment([tip, tip - arm * 2.4 * s + barb * 1.8], thin);
1136 p.line_segment([tip, tip - arm * 2.4 * s - barb * 1.8], thin);
1137 }
1138 }
1139 }
1140 Glyph::FilmReel => {
1142 p.circle_stroke(c, r, stroke);
1143 p.circle_filled(c, 1.2, fg);
1144 for i in 0..4 {
1145 let a = std::f32::consts::FRAC_PI_2 * i as f32;
1146 p.circle_stroke(c + egui::vec2(a.cos(), a.sin()) * (r * 0.55), 1.3, Stroke::new(1.0, fg));
1147 }
1148 p.line_segment([c + egui::vec2(4.2, 4.2), c + egui::vec2(8.0, 6.0)], stroke);
1149 }
1150 Glyph::Bolt => {
1152 let pts = vec![
1153 c + egui::vec2(1.5, -6.5),
1154 c + egui::vec2(-3.5, 1.0),
1155 c + egui::vec2(-0.5, 1.0),
1156 c + egui::vec2(-1.5, 6.5),
1157 c + egui::vec2(3.5, -1.0),
1158 c + egui::vec2(0.5, -1.0),
1159 ];
1160 p.add(egui::Shape::closed_line(pts.clone(), Stroke::new(1.0, fg)));
1161 p.add(egui::Shape::convex_polygon(pts, fg, Stroke::NONE)); }
1163 Glyph::ImportArrow | Glyph::ExportArrow => {
1165 p.add(egui::Shape::line(
1166 vec![
1167 c + egui::vec2(-6.0, 2.0),
1168 c + egui::vec2(-6.0, 6.0),
1169 c + egui::vec2(6.0, 6.0),
1170 c + egui::vec2(6.0, 2.0),
1171 ],
1172 stroke,
1173 ));
1174 let d = if g == Glyph::ImportArrow { Dir::Down } else { Dir::Up };
1175 let tip_y = if g == Glyph::ImportArrow { 3.0 } else { -6.5 };
1176 let tail_y = if g == Glyph::ImportArrow { -6.5 } else { 3.0 };
1177 p.line_segment([c + egui::vec2(0.0, tail_y), c + egui::vec2(0.0, tip_y)], stroke);
1178 p.add(egui::Shape::convex_polygon(tri(c + egui::vec2(0.0, tip_y), d, 2.8, 3.2), fg, Stroke::NONE));
1179 }
1180 Glyph::Transition => {
1182 let a = egui::Rect::from_center_size(c + egui::vec2(-2.5, -2.0), egui::vec2(9.0, 8.0));
1183 let b = egui::Rect::from_center_size(c + egui::vec2(2.5, 2.0), egui::vec2(9.0, 8.0));
1184 p.rect_stroke(a, CornerRadius::ZERO, stroke, StrokeKind::Inside);
1185 p.rect_stroke(b, CornerRadius::ZERO, stroke, StrokeKind::Inside);
1186 p.line_segment([a.left_bottom() + egui::vec2(2.0, 0.0), a.right_top() + egui::vec2(0.0, 2.0)], stroke);
1187 }
1188 Glyph::Subtitles => {
1190 p.rect_stroke(
1191 egui::Rect::from_center_size(c, egui::vec2(13.0, 10.0)),
1192 CornerRadius::same(2),
1193 stroke,
1194 StrokeKind::Inside,
1195 );
1196 p.line_segment([c + egui::vec2(-4.5, 1.0), c + egui::vec2(2.5, 1.0)], Stroke::new(1.6, fg));
1197 p.line_segment([c + egui::vec2(-4.5, 3.2), c + egui::vec2(4.5, 3.2)], Stroke::new(1.6, fg));
1198 }
1199 Glyph::Gear => {
1201 p.circle_stroke(c, 4.2, stroke);
1202 p.circle_filled(c, 1.4, fg);
1203 for i in 0..8 {
1204 let a = std::f32::consts::FRAC_PI_4 * i as f32;
1205 let u = egui::vec2(a.cos(), a.sin());
1206 p.line_segment([c + u * 4.2, c + u * 6.5], stroke);
1207 }
1208 }
1209 Glyph::Sliders => {
1211 for (i, kx) in [(-1.0_f32, -2.5_f32), (0.0, 3.0), (1.0, -0.5)] {
1212 let y = i * 4.5;
1213 p.line_segment([c + egui::vec2(-6.5, y), c + egui::vec2(6.5, y)], stroke);
1214 p.circle_filled(c + egui::vec2(kx, y), 1.8, fg);
1215 }
1216 }
1217 Glyph::Wrench => {
1219 let jaw = c + egui::vec2(-3.5, -3.5);
1220 p.circle_stroke(jaw, 3.0, stroke);
1221 p.line_segment([jaw, jaw + egui::vec2(-3.5, -3.5)], Stroke::new(2.6, fg));
1223 p.line_segment([jaw + egui::vec2(2.0, 2.0), c + egui::vec2(6.0, 6.0)], Stroke::new(2.2, fg));
1224 }
1225 Glyph::Clapperboard => {
1227 let body = egui::Rect::from_center_size(c + egui::vec2(0.0, 2.0), egui::vec2(13.0, 7.5));
1228 p.rect_stroke(body, CornerRadius::same(1), stroke, StrokeKind::Inside);
1229 p.add(egui::Shape::closed_line(
1230 vec![
1231 body.left_top(),
1232 body.left_top() + egui::vec2(1.0, -3.8),
1233 body.right_top() + egui::vec2(0.0, -2.8),
1234 body.right_top(),
1235 ],
1236 stroke,
1237 ));
1238 for x in [-2.5_f32, 2.0] {
1239 p.line_segment([c + egui::vec2(x, -1.7), c + egui::vec2(x + 2.0, -4.8)], Stroke::new(1.0, fg));
1240 }
1241 }
1242 Glyph::Waveform => {
1244 for (i, h) in [3.0_f32, 6.0, 4.0, 6.5, 2.5].iter().enumerate() {
1245 let x = -6.0 + i as f32 * 3.0;
1246 p.line_segment([c + egui::vec2(x, -h / 2.0), c + egui::vec2(x, h / 2.0)], Stroke::new(1.8, fg));
1247 }
1248 }
1249 Glyph::CurveIcon => {
1251 let o = c + egui::vec2(-6.0, 6.0);
1252 p.line_segment([o, o + egui::vec2(0.0, -12.0)], stroke);
1253 p.line_segment([o, o + egui::vec2(12.0, 0.0)], stroke);
1254 p.add(egui::Shape::line(
1255 vec![
1256 o + egui::vec2(1.0, -1.0),
1257 o + egui::vec2(5.0, -2.5),
1258 o + egui::vec2(8.0, -6.5),
1259 o + egui::vec2(11.0, -11.0),
1260 ],
1261 stroke,
1262 ));
1263 for d in [egui::vec2(5.0, -2.5), egui::vec2(8.0, -6.5)] {
1264 p.rect_filled(egui::Rect::from_center_size(o + d, egui::vec2(2.4, 2.4)), CornerRadius::ZERO, fg);
1265 }
1266 }
1267 Glyph::Clock => {
1268 p.circle_stroke(c, r, stroke);
1269 p.line_segment([c, c + egui::vec2(0.0, -4.0)], stroke);
1270 p.line_segment([c, c + egui::vec2(3.0, 1.5)], stroke);
1271 }
1272 Glyph::Notepad => {
1273 p.rect_stroke(
1274 egui::Rect::from_center_size(c, egui::vec2(10.0, 12.0)),
1275 CornerRadius::same(1),
1276 stroke,
1277 StrokeKind::Inside,
1278 );
1279 for dy in [-3.0_f32, 0.0, 3.0] {
1280 p.line_segment([c + egui::vec2(-3.0, dy), c + egui::vec2(3.0, dy)], Stroke::new(1.0, fg));
1281 }
1282 }
1283 Glyph::Bookmark => {
1285 p.add(egui::Shape::closed_line(
1286 vec![
1287 c + egui::vec2(-3.5, -6.5),
1288 c + egui::vec2(3.5, -6.5),
1289 c + egui::vec2(3.5, 6.5),
1290 c + egui::vec2(0.0, 3.0),
1291 c + egui::vec2(-3.5, 6.5),
1292 ],
1293 stroke,
1294 ));
1295 }
1296 Glyph::UndoArrow(d) => {
1298 let sx = if d == Dir::Left { 1.0 } else { -1.0 };
1299 let arc: Vec<egui::Pos2> = (0..=8)
1300 .map(|i| {
1301 let a = std::f32::consts::PI + std::f32::consts::PI * 0.85 * i as f32 / 8.0;
1302 c + egui::vec2(a.cos() * 5.0 * sx, a.sin() * 5.0 + 1.5)
1303 })
1304 .collect();
1305 let tip = arc[0];
1306 p.add(egui::Shape::line(arc, stroke));
1307 p.add(egui::Shape::convex_polygon(
1308 vec![
1309 tip + egui::vec2(-2.5 * sx, 0.5),
1310 tip + egui::vec2(2.0 * sx, -1.5),
1311 tip + egui::vec2(1.5 * sx, 3.0),
1312 ],
1313 fg,
1314 Stroke::NONE,
1315 ));
1316 }
1317 Glyph::FloppyDisk => {
1319 let (l, t, rr, b) = (c.x - 6.0, c.y - 6.0, c.x + 6.0, c.y + 6.0);
1320 p.add(egui::Shape::closed_line(
1321 vec![
1322 egui::pos2(l, t),
1323 egui::pos2(rr - 2.5, t),
1324 egui::pos2(rr, t + 2.5),
1325 egui::pos2(rr, b),
1326 egui::pos2(l, b),
1327 ],
1328 stroke,
1329 ));
1330 p.rect_filled(
1331 egui::Rect::from_min_size(egui::pos2(l + 2.5, t + 0.7), egui::vec2(5.5, 3.0)),
1332 CornerRadius::ZERO,
1333 fg,
1334 );
1335 p.rect_stroke(
1336 egui::Rect::from_min_size(egui::pos2(l + 2.0, b - 4.5), egui::vec2(8.0, 3.8)),
1337 CornerRadius::ZERO,
1338 Stroke::new(1.0, fg),
1339 StrokeKind::Inside,
1340 );
1341 }
1342 Glyph::Terminal => {
1344 p.rect_stroke(
1345 egui::Rect::from_center_size(c, egui::vec2(13.0, 10.0)),
1346 CornerRadius::same(1),
1347 stroke,
1348 StrokeKind::Inside,
1349 );
1350 p.line_segment([c + egui::vec2(-4.5, -2.5), c + egui::vec2(-2.0, 0.0)], stroke);
1351 p.line_segment([c + egui::vec2(-2.0, 0.0), c + egui::vec2(-4.5, 2.5)], stroke);
1352 p.line_segment([c + egui::vec2(0.0, 2.5), c + egui::vec2(3.5, 2.5)], stroke);
1353 }
1354 }
1355}
1356
1357pub(crate) fn action_glyph(a: crate::hotkeys::Action) -> Option<Glyph> {
1360 use crate::hotkeys::Action::*;
1361 Some(match a {
1362 NewProject => Glyph::Clapperboard,
1363 OpenFile | OpenProject => Glyph::Folder,
1364 Save | SaveProjectAs => Glyph::FloppyDisk,
1365 ExportVideo | ExportLossless | ExportXml => Glyph::ExportArrow,
1366 ImportMedia => Glyph::FilmReel,
1367 Settings => Glyph::Gear,
1368 Undo => Glyph::UndoArrow(Dir::Left),
1369 Redo => Glyph::UndoArrow(Dir::Right),
1370 PlayPause => Glyph::Play,
1371 Stop => Glyph::Stop,
1372 Split => Glyph::Razor,
1373 AddText => Glyph::Letter('T'),
1374 AddMarker => Glyph::Flag,
1375 Retime => Glyph::Clock,
1376 Fullscreen => Glyph::Fullscreen,
1377 ScreenCapture => Glyph::Camera,
1378 _ => return None,
1379 })
1380}
1381
1382pub(crate) fn glyph_label(ui: &mut egui::Ui, icon: Glyph, color: Color32) -> egui::Response {
1384 let (rect, r) = ui.allocate_exact_size(egui::vec2(18.0, ui.spacing().interact_size.y), Sense::hover());
1385 draw_glyph(ui.painter(), rect, icon, color);
1386 r
1387}
1388
1389pub(crate) fn color_chip<'a>(color: Color32, selected: bool, palette: &Palette) -> egui::Button<'a> {
1392 let ring = if selected { Stroke::new(2.0, palette.accent) } else { Stroke::new(1.0, palette.border) };
1393 egui::Button::new("").fill(color).stroke(ring).corner_radius(CornerRadius::same(8)).min_size(egui::vec2(15.0, 15.0))
1394}
1395
1396fn on_accent(c: Color32) -> Color32 {
1398 let l = 0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32;
1399 if l > 140.0 {
1400 Color32::BLACK
1401 } else {
1402 Color32::WHITE
1403 }
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408 use super::*;
1409 use egui::{Event, Pos2, Rect, Vec2};
1410
1411 struct Harness {
1412 ctx: egui::Context,
1413 state: ToolsState,
1414 snap: bool,
1415 base: egui::Id,
1416 time: f64,
1417 changed: bool,
1418 }
1419
1420 impl Harness {
1421 fn new() -> Self {
1422 let mut h = Self {
1423 ctx: egui::Context::default(),
1424 state: ToolsState::default(),
1425 snap: false,
1426 base: egui::Id::NULL,
1427 time: 0.0,
1428 changed: false,
1429 };
1430 h.frame(vec![]);
1431 h
1432 }
1433 fn frame(&mut self, events: Vec<Event>) {
1434 self.time += 0.05;
1435 let input = egui::RawInput {
1436 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(900.0, 60.0))),
1437 time: Some(self.time),
1438 events,
1439 ..Default::default()
1440 };
1441 let pal = Palette::new(true, Color32::from_rgb(0, 120, 212));
1442 let Harness { ctx, state, snap, base, changed, .. } = self;
1443 let _ = ctx.run(input, |ctx| {
1444 egui::CentralPanel::default().show(ctx, |ui| {
1445 *base = ui.id();
1446 *changed = show(ui, state, &pal, snap);
1447 });
1448 });
1449 }
1450 fn button(&self, name: &str) -> Pos2 {
1452 self.ctx
1453 .read_response(self.base.with(("tool", name)))
1454 .unwrap_or_else(|| panic!("no button {name}"))
1455 .rect
1456 .center()
1457 }
1458 fn click(&mut self, name: &str) {
1459 let pos = self.button(name);
1460 self.frame(vec![Event::PointerMoved(pos)]);
1461 self.frame(vec![Event::PointerButton {
1462 pos,
1463 button: egui::PointerButton::Primary,
1464 pressed: true,
1465 modifiers: Modifiers::NONE,
1466 }]);
1467 self.frame(vec![Event::PointerButton {
1468 pos,
1469 button: egui::PointerButton::Primary,
1470 pressed: false,
1471 modifiers: Modifiers::NONE,
1472 }]);
1473 }
1474 fn key(&mut self, key: Key) {
1475 self.key_mod(key, Modifiers::NONE);
1476 }
1477 fn key_mod(&mut self, key: Key, modifiers: Modifiers) {
1478 self.frame(vec![
1479 Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers },
1480 Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers },
1481 ]);
1482 }
1483 }
1484
1485 const ALL_GLYPHS: &[Glyph] = Glyph::ALL;
1488
1489 fn painted(paint: impl Fn(&egui::Painter, Rect)) -> usize {
1491 let ctx = egui::Context::default();
1492 let input = || egui::RawInput {
1493 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(200.0, 100.0))),
1494 ..Default::default()
1495 };
1496 let mut run = || {
1497 ctx.run(input(), |ctx| {
1498 egui::CentralPanel::default().show(ctx, |ui| {
1499 let rect = Rect::from_min_size(Pos2::new(20.0, 20.0), Vec2::new(24.0, 22.0));
1500 ui.allocate_space(rect.size());
1501 paint(ui.painter(), rect);
1502 });
1503 })
1504 };
1505 run();
1506 let out = run();
1507 ctx.tessellate(out.shapes, 1.0)
1508 .iter()
1509 .map(|p| match &p.primitive {
1510 egui::epaint::Primitive::Mesh(m) => m.vertices.len(),
1511 _ => 0,
1512 })
1513 .sum()
1514 }
1515
1516 #[test]
1517 fn every_glyph_paints_a_picture() {
1518 let empty = painted(|_, _| {});
1521 for g in ALL_GLYPHS {
1522 let n = painted(|p, rect| draw_glyph(p, rect, *g, Color32::WHITE));
1523 assert!(n > empty, "{g:?} painted nothing ({n} vs {empty} vertices)");
1524 }
1525 }
1526
1527 #[test]
1528 fn strip_lays_out_every_tool() {
1529 let h = Harness::new();
1530 for (_, _, name) in STRIP {
1531 let c = h.button(name);
1532 assert!(c.x > 0.0 && c.x < 900.0, "{name} off-strip at {c:?}");
1533 }
1534 }
1535
1536 #[test]
1537 fn clicking_switches_the_tool() {
1538 let mut h = Harness::new();
1539 assert_eq!(h.state.tool, Tool::Select);
1540 h.click("Ellipse");
1541 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Ellipse));
1542 assert!(h.changed, "a switch is reported as a change");
1543 h.click("Draw");
1544 assert_eq!(h.state.tool, Tool::Draw);
1545 h.click("Zoom");
1546 assert_eq!(h.state.tool, Tool::Zoom);
1547 h.click("Select");
1548 assert_eq!(h.state.tool, Tool::Select);
1549 }
1550
1551 #[test]
1552 fn clicking_the_active_tool_reports_nothing() {
1553 let mut h = Harness::new();
1554 h.click("Star");
1555 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Star));
1556 h.click("Star");
1557 assert!(!h.changed, "re-clicking the active tool is not a change");
1558 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Star));
1559 }
1560
1561 #[test]
1562 fn mask_button_lights_up_for_every_mask_shape() {
1563 assert!(same_tool(Tool::Mask(MaskShape::Rect), Tool::Mask(MaskShape::Path)));
1564 assert!(!same_tool(Tool::Mask(MaskShape::Rect), Tool::Select));
1565 assert!(!same_tool(Tool::Shape(ShapeKind::Rect), Tool::Shape(ShapeKind::Star)));
1566 let mut h = Harness::new();
1567 h.state.tool = Tool::Mask(MaskShape::Path);
1568 h.frame(vec![]);
1569 h.click("Mask");
1570 assert_eq!(h.state.tool, Tool::Mask(MaskShape::Path), "the active mask shape survives a re-click");
1571 h.click("Rectangle");
1572 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Rect));
1573 h.click("Mask");
1574 assert_eq!(h.state.tool, Tool::Mask(MaskShape::Rect));
1575 }
1576
1577 #[test]
1578 fn single_key_shortcuts_switch_tools() {
1579 let mut h = Harness::new();
1580 h.key(Key::T);
1581 assert_eq!(h.state.tool, Tool::Text);
1582 h.key(Key::D);
1583 assert_eq!(h.state.tool, Tool::Draw);
1584 h.key(Key::V);
1585 assert_eq!(h.state.tool, Tool::Select);
1586 h.key_mod(Key::S, Modifiers::SHIFT);
1587 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Rect));
1588 h.key_mod(Key::S, Modifiers::SHIFT);
1589 assert_eq!(h.state.tool, Tool::Shape(ShapeKind::Ellipse), "Shift+S steps through the shapes");
1590 h.key(Key::M);
1591 assert_eq!(h.state.tool, Tool::Mask(MaskShape::Rect));
1592 h.key(Key::M);
1593 assert_eq!(h.state.tool, Tool::Mask(MaskShape::Ellipse));
1594 }
1595
1596 #[test]
1597 fn bare_s_toggles_snapping_not_the_tool() {
1598 let mut h = Harness::new();
1599 h.state.tool = Tool::Draw;
1600 assert!(!h.snap);
1601 h.key(Key::S);
1602 assert!(h.snap, "bare S toggles snapping");
1603 assert_eq!(h.state.tool, Tool::Draw, "bare S must not touch the active tool");
1604 assert!(h.changed);
1605 h.key(Key::S);
1606 assert!(!h.snap, "S toggles back off");
1607 }
1608
1609 #[test]
1610 fn modified_keys_are_left_alone() {
1611 let mut h = Harness::new();
1612 h.frame(vec![Event::Key {
1613 key: Key::V,
1614 physical_key: None,
1615 pressed: true,
1616 repeat: false,
1617 modifiers: Modifiers::CTRL,
1618 }]);
1619 assert_eq!(h.state.tool, Tool::Select);
1620 h.state.tool = Tool::Draw;
1621 h.frame(vec![Event::Key {
1622 key: Key::S,
1623 physical_key: None,
1624 pressed: true,
1625 repeat: false,
1626 modifiers: Modifiers::CTRL,
1627 }]);
1628 assert_eq!(h.state.tool, Tool::Draw, "Ctrl+S must stay the save shortcut");
1629 assert!(!h.snap, "Ctrl+S must not toggle snapping either");
1630 }
1631
1632 #[test]
1633 fn hotkeys_are_consumed_once() {
1634 let mut state = ToolsState::default();
1636 let mut snap = false;
1637 let ctx = egui::Context::default();
1638 let input = egui::RawInput {
1639 screen_rect: Some(Rect::from_min_size(Pos2::ZERO, Vec2::new(900.0, 60.0))),
1640 events: vec![Event::Key {
1641 key: Key::S,
1642 physical_key: None,
1643 pressed: true,
1644 repeat: false,
1645 modifiers: Modifiers::SHIFT,
1646 }],
1647 ..Default::default()
1648 };
1649 let pal = Palette::new(true, Color32::from_rgb(0, 120, 212));
1650 let _ = ctx.run(input, |ctx| {
1651 egui::CentralPanel::default().show(ctx, |ui| {
1652 show(ui, &mut state, &pal, &mut snap);
1653 assert!(handle_hotkeys(ui.ctx(), &mut state).is_none(), "key already consumed");
1654 });
1655 });
1656 assert_eq!(state.tool, Tool::Shape(ShapeKind::Rect));
1657 assert!(!snap, "Shift+S is the shape cycle, not the snap toggle");
1658 }
1659
1660 #[test]
1661 fn tool_hotkey_covers_the_documented_keys() {
1662 assert_eq!(tool_hotkey(Tool::Select), Some(Key::V));
1663 assert_eq!(tool_hotkey(Tool::Text), Some(Key::T));
1664 assert_eq!(tool_hotkey(Tool::Shape(ShapeKind::Star)), None, "shape tools cycle on Shift+S");
1665 assert_eq!(tool_hotkey(Tool::Draw), Some(Key::D));
1666 assert_eq!(tool_hotkey(Tool::Mask(MaskShape::Path)), Some(Key::M));
1667 assert_eq!(tool_hotkey(Tool::Zoom), None);
1668 }
1669
1670 #[test]
1671 fn clicking_the_magnet_toggles_snapping() {
1672 let mut h = Harness::new();
1673 assert!(!h.snap);
1674 let pos = h.ctx.read_response(h.base.with("snap")).unwrap().rect.center();
1675 h.frame(vec![Event::PointerMoved(pos)]);
1676 h.frame(vec![Event::PointerButton {
1677 pos,
1678 button: egui::PointerButton::Primary,
1679 pressed: true,
1680 modifiers: Modifiers::NONE,
1681 }]);
1682 h.frame(vec![Event::PointerButton {
1683 pos,
1684 button: egui::PointerButton::Primary,
1685 pressed: false,
1686 modifiers: Modifiers::NONE,
1687 }]);
1688 assert!(h.snap, "clicking the magnet turns snapping on");
1689 assert!(h.changed);
1690 }
1691
1692 #[test]
1693 fn every_tool_renders_its_style_controls() {
1694 let mut h = Harness::new();
1695 let mut tools: Vec<Tool> = STRIP.iter().map(|(t, ..)| *t).collect();
1696 tools.extend(MaskShape::ALL.map(Tool::Mask));
1697 tools.push(Tool::Shape(ShapeKind::Draw));
1698 for t in tools {
1699 h.state.tool = t;
1700 h.frame(vec![]);
1701 assert_eq!(h.state.tool, t, "{t:?} controls must not change the tool on their own");
1702 assert!(!h.changed, "{t:?} reports no change when nothing is touched");
1703 }
1704 }
1705}