1use std::borrow::Cow;
12
13use blockworx_doc::{
14 id::BlockId,
15 rev::Rev,
16 values::{PinDir, Role as AccentRole},
17};
18use blockworx_editor::{
19 content_path,
20 edit::lower::accent_from_role,
21 path::{BlockPath, Scope, child_blocks, tree_root},
22 shape::{ShapeId, ShapeRef},
23 widget::drawing::Drawing,
24};
25use blockworx_geom::{Rect, grid::GridCell};
26use blockworx_paint::{
27 Color, Zoom,
28 theme::{Role, accent_role},
29};
30use blockworx_store::{
31 doc::{Attachment, Doc, Renaming, Viewing, Writability},
32 history::Row,
33 record::WallTime,
34};
35use blockworx_tools::{
36 commands::{Act, CommandId, CommandSet, Effect},
37 names::{ToolName, displayed_tool, instruction},
38 tool::{RoleTarget, ToolTrait},
39};
40
41use crate::session::{Consequences, Session, viewed};
42
43#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
46pub struct TopBar {
47 pub name: String,
49 pub scope: ScopePath,
50 pub steps: Consequences,
51 pub lens: Lens,
52 pub liveness: Liveness,
53 pub renaming: Renaming,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
61pub struct ScopePath {
62 pub path: BlockPath,
63 pub names: Vec<String>,
64}
65
66const MAX_SEGMENTS: usize = 4;
68const TAIL_SEGMENTS: usize = 2;
70
71#[derive(Clone, PartialEq, Eq, Debug)]
73pub enum Crumb {
74 Root,
75 Level(usize),
77 Elided(Vec<usize>),
79}
80
81impl ScopePath {
82 pub fn here(&self) -> usize {
84 self.names.len()
85 }
86
87 pub fn collapsed(&self) -> Vec<Crumb> {
93 let here = self.here();
94 let depths = 1..=here;
95 if here < MAX_SEGMENTS {
96 return std::iter::once(Crumb::Root)
97 .chain(depths.map(Crumb::Level))
98 .collect();
99 }
100 let tail = here.saturating_sub(TAIL_SEGMENTS - 1);
101 vec![
102 Crumb::Root,
103 Crumb::Elided((1..tail).collect()),
104 Crumb::Level(tail),
105 Crumb::Level(here),
106 ]
107 }
108
109 pub fn up_to(&self, depth: usize) -> blockworx_tools::tool::Action {
113 if depth + 1 == self.path.segments().len() {
114 return blockworx_tools::tool::Action::GoUp;
115 }
116 let mut to = BlockPath::empty();
117 for &id in &self.path.segments()[..depth] {
118 to.push(id);
119 }
120 blockworx_tools::tool::Action::GoToPath(to)
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
126pub struct Lens {
127 pub viewing: Viewing,
128 pub head: Rev,
130 pub age: String,
134}
135
136#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
144pub enum Liveness {
145 Recorded,
147 ReadOnly(Locked),
149 Scratch,
151}
152
153#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
155pub enum Locked {
156 Lens,
158 Container,
160}
161
162impl Liveness {
163 pub fn of(viewing: Viewing, writability: Writability, attachment: Attachment) -> Self {
174 if matches!(viewing, Viewing::Past(_)) {
175 return Liveness::ReadOnly(Locked::Lens);
176 }
177 if writability == Writability::ReadOnly {
178 return Liveness::ReadOnly(Locked::Container);
179 }
180 match attachment {
181 Attachment::Scratch => Liveness::Scratch,
182 Attachment::Attached => Liveness::Recorded,
183 }
184 }
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
189pub struct Reading {
190 pub tool: Option<Cow<'static, str>>,
192 pub selection: Option<String>,
194 pub zoom: Zoom,
195 pub cursor: Option<GridCell>,
197 pub title: TitleBlock,
198}
199
200#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
208pub struct TitleBlock {
209 pub author: String,
211 pub rev: Rev,
212 pub written: String,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
222pub struct NavTree {
223 pub hung: Hung,
227 pub nodes: Vec<NavNode>,
228 pub path: BlockPath,
229 pub selected: Vec<BlockId>,
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
234pub enum Hung {
235 #[default]
236 FromRoot,
237 FromTop {
238 id: BlockId,
239 name: String,
241 },
242}
243
244impl Hung {
245 pub fn scope(&self) -> Scope {
246 match self {
247 Hung::FromRoot => Scope::Root,
248 Hung::FromTop { id, .. } => Scope::Block(*id),
249 }
250 }
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
256pub struct NavNode {
257 pub id: BlockId,
258 pub name: String,
261 pub label: String,
264 pub accent: Option<u8>,
266 pub children: Vec<NavNode>,
267}
268
269impl NavNode {
270 pub fn opens_a_scope(&self) -> bool {
273 !self.children.is_empty()
274 }
275
276 pub fn leaves(&self) -> usize {
279 if self.children.is_empty() {
280 return 1;
281 }
282 self.children.iter().map(NavNode::leaves).sum()
283 }
284}
285
286impl NavTree {
287 pub fn of(
290 document: &blockworx_doc::document::IndexedDocument<'_>,
291 path: BlockPath,
292 selected: Vec<BlockId>,
293 ) -> Self {
294 fn nodes(
295 document: &blockworx_doc::document::IndexedDocument<'_>,
296 scope: Scope,
297 ) -> Vec<NavNode> {
298 child_blocks(document, scope)
299 .into_iter()
300 .map(|id| NavNode {
301 id,
302 name: content_path::name_of(document, id),
303 label: block_label(document, id),
304 accent: document
305 .doc
306 .block(&id)
307 .and_then(|block| accent_from_role(block.role)),
308 children: nodes(document, Scope::Block(id)),
309 })
310 .collect()
311 }
312 let root = tree_root(document);
313 NavTree {
314 hung: match root {
315 Scope::Root => Hung::FromRoot,
316 Scope::Block(id) => Hung::FromTop {
317 id,
318 name: content_path::name_of(document, id),
319 },
320 },
321 nodes: nodes(document, root),
322 path,
323 selected,
324 }
325 }
326
327 pub fn path_of(&self, text: &str) -> Option<BlockPath> {
332 let mut names = text
333 .split(content_path::SEPARATOR)
334 .map(str::trim)
335 .filter(|name| !name.is_empty())
336 .peekable();
337 let mut path = BlockPath::empty();
338 if let Hung::FromTop { id, name } = &self.hung {
339 let first = names.next()?;
340 if !name.eq_ignore_ascii_case(first) {
341 return None;
342 }
343 path.push(*id);
344 }
345 let mut level = self.nodes.as_slice();
346 for name in names {
347 let node = level
348 .iter()
349 .find(|node| node.name.eq_ignore_ascii_case(name))?;
350 path.push(node.id);
351 level = &node.children;
352 }
353 Some(path)
354 }
355
356 pub fn all_blocks(&self) -> Vec<(BlockId, String)> {
359 self.walk()
360 .map(|(node, _)| (node.id, node.label.clone()))
361 .collect()
362 }
363
364 pub fn level_blocks(&self) -> Vec<(BlockId, String)> {
368 self.children_of(self.path.scope())
369 .iter()
370 .map(|node| (node.id, node.label.clone()))
371 .collect()
372 }
373
374 pub fn walk(&self) -> impl Iterator<Item = (&NavNode, Vec<BlockId>)> {
377 fn descend<'a>(
378 nodes: &'a [NavNode],
379 above: &mut Vec<BlockId>,
380 out: &mut Vec<(&'a NavNode, Vec<BlockId>)>,
381 ) {
382 for node in nodes {
383 out.push((node, above.clone()));
384 above.push(node.id);
385 descend(&node.children, above, out);
386 above.pop();
387 }
388 }
389 let mut out = Vec::new();
390 descend(&self.nodes, &mut Vec::new(), &mut out);
391 out.into_iter()
392 }
393
394 pub fn node(&self, id: BlockId) -> Option<&NavNode> {
396 self.walk().map(|(node, _)| node).find(|node| node.id == id)
397 }
398
399 pub fn children_of(&self, scope: Scope) -> &[NavNode] {
404 if scope == Scope::Root || scope == self.hung.scope() {
405 return &self.nodes;
406 }
407 match scope {
408 Scope::Root => &[],
409 Scope::Block(id) => self.node(id).map_or(&[], |node| node.children.as_slice()),
410 }
411 }
412}
413
414pub fn block_label(document: &blockworx_doc::document::IndexedDocument<'_>, id: BlockId) -> String {
419 let Some(block) = document.doc.block(&id) else {
420 return id.to_string();
421 };
422 let name = block.title.name.trim();
423 let head = if name.is_empty() {
424 id.to_string()
425 } else {
426 name.to_owned()
427 };
428 let kind = block.type_label.name.trim();
429 if kind.is_empty() {
430 head
431 } else {
432 format!("{head}/{kind}")
433 }
434}
435
436#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
441pub struct Overlay {
442 pub selection: Selection,
443 pub swatch: Option<Color>,
446 pub accent: Option<u8>,
450 pub pin_dir: Option<PinDir>,
453}
454
455#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
458pub struct Selection {
459 pub screen: Rect,
460 pub count: usize,
461}
462
463impl Eq for Selection {}
466
467impl Overlay {
468 pub fn of(
472 drawing: &Drawing<'_>,
473 theme: &blockworx_paint::theme::Theme,
474 selection: Selection,
475 commands: &CommandSet,
476 ) -> Self {
477 let accent = commands
481 .iter_drawn()
482 .find_map(|cmd| match (cmd.id, &cmd.act) {
483 (CommandId::Accent, Act::Effect(Effect::Accent(target))) => Some(*target),
484 _ => None,
485 });
486 let pins = commands
487 .iter_drawn()
488 .find_map(|cmd| match (cmd.id, &cmd.act) {
489 (CommandId::PinType, Act::Effect(Effect::PinType(pins))) => Some(pins.as_slice()),
490 _ => None,
491 });
492 let current = accent.map(|target| (target, current_accent(drawing, target)));
493 Overlay {
494 selection,
495 swatch: current
496 .map(|(target, index)| theme.resolve(accent_display_role(target, index))),
497 accent: current.and_then(|(_, index)| index),
498 pin_dir: pins.and_then(|pins| shared_dir(drawing, pins)),
499 }
500 }
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
512pub enum Notice {
513 Failure(String),
516 Standing(String),
518}
519
520#[derive(Default, Debug)]
524pub struct Notices(Vec<String>);
525
526impl Notices {
527 pub fn opening(failure: Option<String>) -> Self {
530 Self(failure.into_iter().collect())
531 }
532
533 pub fn report(&mut self, failure: String) {
537 tracing::error!("{failure}");
538 self.0.push(failure);
539 }
540
541 pub fn forget(&mut self) {
543 self.0.clear();
544 }
545
546 pub fn acknowledge(&mut self, Acknowledged(failure): Acknowledged) {
547 if failure < self.0.len() {
548 self.0.remove(failure);
549 }
550 }
551
552 pub fn failures(&self) -> &[String] {
553 &self.0
554 }
555}
556
557#[derive(Clone, Copy, Debug, PartialEq, Eq)]
559pub struct Acknowledged(pub usize);
560
561impl Session {
562 pub fn top_bar(&mut self) -> TopBar {
565 let viewing = self.viewing();
566 let age = match viewing {
569 Viewing::Head => None,
570 Viewing::Past(at) => self
571 .history_rows()
572 .iter()
573 .find(|row| row.rev == at)
574 .map(|row| row.since(blockworx_store::history::now())),
575 };
576 TopBar {
577 name: self.sheet.name.clone(),
578 scope: ScopePath {
579 path: self.path.clone(),
580 names: self.scope_names(),
581 },
582 steps: self.consequences(),
583 lens: Lens {
584 viewing,
585 head: self.doc.repo().rev(),
586 age: age.unwrap_or_default(),
587 },
588 liveness: Liveness::of(viewing, self.doc.writability(), self.doc.attachment()),
589 renaming: self.doc.renaming(),
590 }
591 }
592
593 pub fn ground(&self) -> Ground {
597 Ground {
598 background: self.chrome_color(Role::CanvasBackground),
599 grid: self.chrome_color(Role::GridLine),
600 }
601 }
602
603 pub fn landed(&self, stood: Rev) -> Option<String> {
608 let now = self.doc.repo().rev();
609 if now == stood {
610 return None;
611 }
612 let take_back = self.doc.trail().next_undo();
613 Some(match crate::session::step_label(&self.doc, take_back) {
614 Some(label) => format!("{label} \u{2014} rev {}", now.get()),
615 None => format!("Rev {}", now.get()),
616 })
617 }
618
619 pub fn history_rows(&self) -> Vec<Row> {
621 blockworx_store::history::rows(self.doc.journal(), self.doc.tags())
622 }
623
624 pub fn displayed_tool(&self) -> ToolName {
626 displayed_tool(self.tool.name())
627 }
628
629 pub fn reading(&mut self) -> Reading {
631 let cursor = self
632 .pointer_screen()
633 .and(self.hovered_world())
634 .map(GridCell::at);
635 Reading {
636 tool: instruction(self.displayed_tool()).map(Cow::Borrowed),
637 selection: self.selection_path(),
638 zoom: self.vantage().zoom,
639 cursor,
640 title: self.title_block(),
641 }
642 }
643
644 fn selection_path(&mut self) -> Option<String> {
650 let count = self.tool.selection().map_or(0, |sel| sel.count());
651 if count == 0 {
652 return None;
653 }
654 let named = (count == 1)
655 .then(|| {
656 let shape = self.tool.selection()?.shapes()?.into_iter().next()?;
657 let title = self.drawing().shape(shape)?.title()?.name.to_owned();
658 let mut path = self.scope_names();
659 path.push(title);
660 Some(path.join(&format!(" {SELECTION_SEPARATOR} ")))
661 })
662 .flatten();
663 Some(named.unwrap_or_else(|| format!("{count} selected")))
664 }
665
666 fn title_block(&self) -> TitleBlock {
671 let rev = self.viewed_repo().rev();
672 TitleBlock {
673 author: self.identity.name.clone(),
674 rev,
675 written: self
676 .written_at(rev)
677 .map(blockworx_store::history::written_at)
678 .unwrap_or_default(),
679 }
680 }
681
682 pub fn written_at(&self, rev: Rev) -> Option<WallTime> {
686 match &self.doc {
687 Doc::Scratch { .. } => None,
688 Doc::Attached { store, .. } => store.row(rev).map(|row| row.wall_time),
689 }
690 }
691
692 pub fn nav_tree(&mut self) -> NavTree {
694 let selected = self
695 .tool
696 .selection()
697 .and_then(|d| d.shapes())
698 .unwrap_or_default()
699 .into_iter()
700 .filter_map(ShapeId::block)
701 .collect();
702 let document = self
703 .doc_index
704 .view(viewed(&self.doc, self.time_machine.as_ref()).document());
705 NavTree::of(&document, self.path.clone(), selected)
706 }
707
708 pub fn overlay(&mut self, bounds: Option<Rect>, commands: &CommandSet) -> Option<Overlay> {
713 let count = self.tool.selection()?.count();
714 let screen = bounds?;
715 let Session {
716 doc,
717 time_machine,
718 doc_index,
719 presentation,
720 gesture,
721 path,
722 theme,
723 ..
724 } = self;
725 let document = viewed(doc, time_machine.as_ref()).document();
726 let drawing = Drawing::new(doc_index.view(document), path, presentation, gesture);
727 Some(Overlay::of(
728 &drawing,
729 theme,
730 Selection { screen, count },
731 commands,
732 ))
733 }
734
735 pub fn notices(&self) -> Vec<Notice> {
738 self.failures
739 .failures()
740 .iter()
741 .map(|failure| Notice::Failure(failure.clone()))
742 .chain(self.file_notices().into_iter().map(Notice::Standing))
743 .collect()
744 }
745
746 fn file_notices(&self) -> Vec<String> {
749 let mut notices = Vec::new();
750 if let Some(reason) = self.doc.read_only_reason() {
751 notices.push(format!("Read-only \u{2014} {reason}"));
752 }
753 notices
754 }
755}
756
757const SELECTION_SEPARATOR: &str = "/";
760
761#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
763pub struct Ground {
764 pub background: Color,
765 pub grid: Color,
766}
767
768fn current_accent(data: &Drawing<'_>, target: RoleTarget) -> Option<u8> {
770 let accent = |role: AccentRole| accent_from_role(role);
771 match target {
772 RoleTarget::Block(rid) => data.block(rid).and_then(|b| accent(b.role)),
773 RoleTarget::Port(pid) => match data.shape(ShapeId::Port(pid)) {
774 Some(ShapeRef::Port(port)) => accent(port.pin.port_accent),
775 _ => None,
776 },
777 RoleTarget::Route(rid) => data.auto_route(rid).and_then(|w| accent(w.route.role)),
778 RoleTarget::Area(cid) => match data.shape(ShapeId::Area(cid)) {
779 Some(ShapeRef::Area(area)) => accent(area.role),
780 _ => None,
781 },
782 RoleTarget::Text(tid) => match data.shape(ShapeId::Text(tid)) {
783 Some(ShapeRef::Text(text)) => accent(text.text.role),
784 _ => None,
785 },
786 }
787}
788
789pub fn accent_display_role(target: RoleTarget, current: Option<u8>) -> Role {
793 let default_role = match target {
794 RoleTarget::Area(_) => Role::AreaStroke,
795 RoleTarget::Text(_) => Role::TextBoxStroke,
796 RoleTarget::Block(_) | RoleTarget::Port(_) | RoleTarget::Route(_) => Role::AccentDefault,
797 };
798 accent_role(current).unwrap_or(default_role)
799}
800
801fn shared_dir(data: &Drawing<'_>, pins: &[blockworx_doc::id::PinId]) -> Option<PinDir> {
804 let mut dirs = pins
805 .iter()
806 .map(|&pin| data.pin_on_shape(pin).map(|(_, pin)| pin.dir));
807 let first = dirs.next()??;
808 dirs.all(|dir| dir == Some(first)).then_some(first)
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use blockworx_doc::fixtures::block_id;
815 use blockworx_editor::widget::test_fixtures::{self as fx, Scene};
816 use blockworx_geom::{Rect, pos2};
817
818 fn levels_deep(levels: usize) -> ScopePath {
819 let path = (1..=levels).fold(BlockPath::empty(), |mut path, at| {
820 path.push(block_id(at as u32));
821 path
822 });
823 ScopePath {
824 names: (1..=levels).map(|at| format!("Level {at}")).collect(),
825 path,
826 }
827 }
828
829 #[test]
833 fn a_deep_path_collapses_from_the_middle_and_keeps_its_ends() {
834 assert_eq!(
835 levels_deep(2).collapsed(),
836 vec![Crumb::Root, Crumb::Level(1), Crumb::Level(2)],
837 "a path that fits was collapsed anyway",
838 );
839 let deep = levels_deep(5).collapsed();
840 assert_eq!(
841 deep,
842 vec![
843 Crumb::Root,
844 Crumb::Elided(vec![1, 2, 3]),
845 Crumb::Level(4),
846 Crumb::Level(5),
847 ],
848 );
849 assert_eq!(deep.first(), Some(&Crumb::Root), "the root was hidden");
850 assert_eq!(
851 deep.last(),
852 Some(&Crumb::Level(5)),
853 "the current level was hidden",
854 );
855 }
856
857 #[test]
861 fn a_click_on_the_parent_rises_and_a_click_further_out_names_the_path() {
862 let scope = levels_deep(3);
863 assert!(matches!(
864 scope.up_to(2),
865 blockworx_tools::tool::Action::GoUp
866 ));
867 let blockworx_tools::tool::Action::GoToPath(to) = scope.up_to(1) else {
868 panic!("a jump past the parent is a whole path");
869 };
870 assert_eq!(to.segments(), &[block_id(1)]);
871 let blockworx_tools::tool::Action::GoToPath(root) = scope.up_to(0) else {
872 panic!("the document's own segment is the root");
873 };
874 assert!(root.segments().is_empty());
875 }
876
877 fn body() -> Rect {
878 Rect::from_min_max(pos2(0.0, 0.0), pos2(60.0, 60.0))
879 }
880
881 fn nested() -> (Scene, Vec<BlockId>) {
883 let scene = Scene::new(vec![
884 fx::block_in(1, Scope::Root, body()),
885 fx::titled(1, "top"),
886 fx::block_in(2, Scope::Block(block_id(1)), body()),
887 fx::titled(2, "Thing 1"),
888 fx::block_in(3, Scope::Block(block_id(2)), body()),
889 fx::titled(3, "Core"),
890 ]);
891 (scene, vec![block_id(1), block_id(2), block_id(3)])
892 }
893
894 fn path_of(ids: &[BlockId]) -> BlockPath {
895 let mut path = BlockPath::empty();
896 for &id in ids {
897 path.push(id);
898 }
899 path
900 }
901
902 fn tree_of(scene: &mut Scene) -> NavTree {
903 NavTree::of(&scene.indexed(), BlockPath::empty(), Vec::new())
904 }
905
906 #[test]
909 fn a_displayed_path_parses_back_to_itself() {
910 let (mut scene, ids) = nested();
911 let text = content_path::to_string(&scene.indexed(), &path_of(&ids));
912 let tree = tree_of(&mut scene);
913 let path = path_of(&ids);
914 assert_eq!(tree.path_of(&text), Some(path.clone()));
915 assert_eq!(tree.path_of(" top / thing 1 / core "), Some(path));
917 assert_eq!(tree.path_of("top"), Some(path_of(&ids[..1])));
918 assert_eq!(tree.path_of("top/Thing 1/Nope"), None);
919 assert_eq!(tree.path_of("Core"), None, "names resolve level by level");
920 }
921
922 #[test]
925 fn an_untitled_block_falls_back_to_its_id() {
926 let id = block_id(1);
927 let mut scene = Scene::new(vec![
928 fx::block_in(1, Scope::Root, body()),
929 fx::titled(1, ""),
930 ]);
931 let tree = tree_of(&mut scene);
932 assert_eq!(tree.path_of(&id.to_string()), Some(path_of(&[id])));
933 }
934
935 #[test]
938 fn a_path_passes_through_the_designated_top() {
939 let (mut scene, ids) = nested();
940 scene.apply(vec![fx::top(1)]);
941 let tree = tree_of(&mut scene);
942 assert!(matches!(tree.hung, Hung::FromTop { .. }));
943 assert_eq!(tree.path_of("top/Thing 1"), Some(path_of(&ids[..2])));
944 assert_eq!(tree.path_of("Thing 1"), None, "the top is not skipped");
945 assert_eq!(
946 tree.children_of(Scope::Block(ids[0]))
947 .iter()
948 .map(|node| node.id)
949 .collect::<Vec<_>>(),
950 vec![ids[1]],
951 );
952 }
953}