From 4aa470c96854cd06d3960421a8b8dab8ed92a346 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 21 Apr 2026 06:38:35 +1200 Subject: [PATCH 1/7] Explicitly specify `f32` type for `Size::length`. --- crate/input_ir_rt/src/ir_to_taffy_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crate/input_ir_rt/src/ir_to_taffy_builder.rs b/crate/input_ir_rt/src/ir_to_taffy_builder.rs index 8d160c5..1575b4b 100644 --- a/crate/input_ir_rt/src/ir_to_taffy_builder.rs +++ b/crate/input_ir_rt/src/ir_to_taffy_builder.rs @@ -896,7 +896,7 @@ impl IrToTaffyBuilder<'_> { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: Some(AlignItems::Center), - gap: Size::length(4.0), + gap: Size::length(4.0f32), ..label_wrapper_style }; @@ -1095,7 +1095,7 @@ impl IrToTaffyBuilder<'_> { display: Display::Flex, flex_direction: FlexDirection::Row, align_items: Some(AlignItems::Center), - gap: Size::length(4.0), + gap: Size::length(4.0f32), ..Default::default() }; From ad14bb0d3bd82baab263f2b37a89a8da338bb3d4 Mon Sep 17 00:00:00 2001 From: Azriel Hoh Date: Tue, 21 Apr 2026 07:15:03 +1200 Subject: [PATCH 2/7] Add tooltip to node / edge elements if present. --- .../src/svg_elements_to_svg_mapper.rs | 51 +++++++++++++++---- .../svg_edge_infos_builder.rs | 7 +++ .../svg_node_info_builder.rs | 8 +++ crate/input_model/src/input_diagram.rs | 6 +-- crate/svg_model/src/svg_edge_info.rs | 7 +++ crate/svg_model/src/svg_node_info.rs | 9 ++++ 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs index 43da5b9..d85298b 100644 --- a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs +++ b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs @@ -237,6 +237,12 @@ impl SvgElementsToSvgMapper { ) .unwrap(); + // Add tooltip element if present + if !svg_node_info.tooltip.is_empty() { + let tooltip_escaped = Self::escape_xml_content(&svg_node_info.tooltip); + write!(content_buffer, "{tooltip_escaped}").unwrap(); + } + // Add path element with corner radii. // If a circle is present, apply wrapper_tailwind_classes to make the // rect path invisible, and render the circle path separately. @@ -361,24 +367,51 @@ impl SvgElementsToSvgMapper { "" + ) + .unwrap(); + + // Add tooltip element if present + if !svg_edge_info.tooltip.is_empty() { + let tooltip_escaped = Self::escape_xml_content(&svg_edge_info.tooltip); + write!(content_buffer, "{tooltip_escaped}").unwrap(); + } + + write!( + content_buffer, + "\ + \ \ - \ - \ - + \ " ) .unwrap(); }); } + /// Escapes XML text content special characters. + /// + /// Replaces `&`, `<`, and `>` with their XML entity equivalents so that + /// the string can be safely embedded as text content inside an XML element + /// such as ``. + fn escape_xml_content(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + s.chars().for_each(|c| match c { + '&' => result.push_str("&"), + '<' => result.push_str("<"), + '>' => result.push_str(">"), + _ => result.push(c), + }); + result + } + /// Returns the `class=".."` attribute with `&` escaped as `&`. fn class_attr_escaped(tailwind_classes: String) -> String { if tailwind_classes.is_empty() { diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs index bb06a13..5e4e73e 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_edge_infos_builder.rs @@ -259,6 +259,12 @@ impl SvgEdgeInfosBuilder { ArrowHeadBuilder::build_static_arrow_head(&path) }; + let tooltip = ir_diagram + .entity_tooltips + .get(edge_id.as_ref()) + .cloned() + .unwrap_or_default(); + svg_edge_infos.push(SvgEdgeInfo::new( edge_id, edge_group_id.clone(), @@ -266,6 +272,7 @@ impl SvgEdgeInfosBuilder { edge.to.clone(), path_d, arrow_head_path_d, + tooltip, )); }); } diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs index b26ed9c..68d1fc7 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs @@ -138,6 +138,12 @@ impl SvgNodeInfoBuilder { NodeShape::Rect(_node_shape_rect) => None, }; + let tooltip = ir_diagram + .entity_tooltips + .get(node_id.as_ref()) + .cloned() + .unwrap_or_default(); + if let Some(circle) = circle_info { SvgNodeInfo::with_circle( node_id.clone(), @@ -150,6 +156,7 @@ impl SvgNodeInfoBuilder { process_id, text_spans, circle, + tooltip, ) } else { SvgNodeInfo::new( @@ -162,6 +169,7 @@ impl SvgNodeInfoBuilder { path_d_collapsed, process_id, text_spans, + tooltip, ) } } diff --git a/crate/input_model/src/input_diagram.rs b/crate/input_model/src/input_diagram.rs index 4a216b1..68dffb4 100644 --- a/crate/input_model/src/input_diagram.rs +++ b/crate/input_model/src/input_diagram.rs @@ -96,10 +96,10 @@ pub struct InputDiagram<'id> { #[serde(default, skip_serializing_if = "EntityDescs::is_empty")] pub entity_descs: EntityDescs<'id>, - /// Descriptions for entities (nodes, edges, and edge groups). + /// Tooltips for entities (nodes, edges, and edge groups). /// - /// Contains text (typically markdown) that provides additional context - /// about entities in the diagram, such as process steps. + /// Contains plain text that provides additional context about entities in + /// the diagram, such as process steps. #[serde(default, skip_serializing_if = "EntityTooltips::is_empty")] pub entity_tooltips: EntityTooltips<'id>, diff --git a/crate/svg_model/src/svg_edge_info.rs b/crate/svg_model/src/svg_edge_info.rs index d962c08..09ea4f3 100644 --- a/crate/svg_model/src/svg_edge_info.rs +++ b/crate/svg_model/src/svg_edge_info.rs @@ -32,6 +32,11 @@ pub struct SvgEdgeInfo<'id> { /// origin-centred V-shape that is animated along the edge path via CSS /// `offset-path`. pub arrow_head_path_d: String, + /// Tooltip text to display when the edge is hovered. + /// + /// When non-empty, rendered as a `<title>` element inside the edge's `<g>` + /// element. Example value: `"Sends a request to the API server."`. + pub tooltip: String, } impl<'id> SvgEdgeInfo<'id> { @@ -43,6 +48,7 @@ impl<'id> SvgEdgeInfo<'id> { to_node_id: NodeId<'id>, path_d: String, arrow_head_path_d: String, + tooltip: String, ) -> Self { Self { edge_id, @@ -51,6 +57,7 @@ impl<'id> SvgEdgeInfo<'id> { to_node_id, path_d, arrow_head_path_d, + tooltip, } } } diff --git a/crate/svg_model/src/svg_node_info.rs b/crate/svg_model/src/svg_node_info.rs index 7b4131d..ae06e09 100644 --- a/crate/svg_model/src/svg_node_info.rs +++ b/crate/svg_model/src/svg_node_info.rs @@ -58,6 +58,11 @@ pub struct SvgNodeInfo<'id> { /// Typically set to `"[fill-opacity:0.0] [stroke-opacity:0.0]"` to make /// the rectangular background invisible so only the circle is visible. pub wrapper_tailwind_classes: Option<Cow<'static, str>>, + /// Tooltip text to display when the node is hovered. + /// + /// When non-empty, rendered as a `<title>` element inside the node's `<g>` + /// element. Example value: `"Clones the repository to the local machine."`. + pub tooltip: String, } impl<'id> SvgNodeInfo<'id> { @@ -73,6 +78,7 @@ impl<'id> SvgNodeInfo<'id> { path_d_collapsed: String, process_id: Option<NodeId<'id>>, text_spans: Vec<SvgTextSpan>, + tooltip: String, ) -> Self { Self { node_id, @@ -86,6 +92,7 @@ impl<'id> SvgNodeInfo<'id> { text_spans, circle: None, wrapper_tailwind_classes: None, + tooltip, } } @@ -102,6 +109,7 @@ impl<'id> SvgNodeInfo<'id> { process_id: Option<NodeId<'id>>, text_spans: Vec<SvgTextSpan>, circle: SvgNodeInfoCircle, + tooltip: String, ) -> Self { Self { node_id, @@ -117,6 +125,7 @@ impl<'id> SvgNodeInfo<'id> { wrapper_tailwind_classes: Some(Cow::Borrowed( "[fill-opacity:0.0] [stroke-opacity:0.0]", )), + tooltip, } } } From af8d5c7de25add9a27cdab87ad6e171bf1f3f542 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Tue, 21 Apr 2026 07:23:41 +1200 Subject: [PATCH 3/7] Extract `StringXmlEscaper::escape` for escaping text. --- crate/input_ir_rt/src/lib.rs | 3 +- crate/input_ir_rt/src/string_xml_escaper.rs | 47 +++++++++++++++++++ .../src/svg_elements_to_svg_mapper.rs | 22 ++------- .../svg_node_info_builder.rs | 19 ++------ 4 files changed, 57 insertions(+), 34 deletions(-) create mode 100644 crate/input_ir_rt/src/string_xml_escaper.rs diff --git a/crate/input_ir_rt/src/lib.rs b/crate/input_ir_rt/src/lib.rs index 641de2f..2390d73 100644 --- a/crate/input_ir_rt/src/lib.rs +++ b/crate/input_ir_rt/src/lib.rs @@ -7,7 +7,7 @@ pub use crate::{ input_diagram_merger::InputDiagramMerger, input_diagram_theme_sources::InputDiagramThemeSources, input_to_ir_diagram_mapper::InputToIrDiagramMapper, ir_to_taffy_builder::IrToTaffyBuilder, - svg_elements_to_svg_mapper::SvgElementsToSvgMapper, + string_xml_escaper::StringXmlEscaper, svg_elements_to_svg_mapper::SvgElementsToSvgMapper, taffy_to_svg_elements_mapper::TaffyToSvgElementsMapper, theme_value_source::ThemeValueSource, }; @@ -21,6 +21,7 @@ mod input_diagram_theme_sources; mod input_to_ir_diagram_mapper; mod ir_to_taffy_builder; mod node_ranks_calculator; +mod string_xml_escaper; mod svg_elements_to_svg_mapper; mod taffy_to_svg_elements_mapper; mod theme_value_source; diff --git a/crate/input_ir_rt/src/string_xml_escaper.rs b/crate/input_ir_rt/src/string_xml_escaper.rs new file mode 100644 index 0000000..48c21ca --- /dev/null +++ b/crate/input_ir_rt/src/string_xml_escaper.rs @@ -0,0 +1,47 @@ +/// Escapes XML special characters in a string. +pub struct StringXmlEscaper; + +impl StringXmlEscaper { + /// Escapes XML special characters in a string, returning the escaped + /// result. + /// + /// The following characters are replaced with their XML entity equivalents: + /// + /// | Character | Replacement | + /// |-----------|-------------| + /// | `&` | `&` | + /// | `<` | `<` | + /// | `>` | `>` | + /// | `"` | `"` | + /// | `'` | `'` | + /// + /// This makes the output safe for use as XML text content or attribute + /// values. + /// + /// # Examples + /// + /// ```rust + /// use disposition_input_ir_rt::StringXmlEscaper; + /// + /// assert_eq!(StringXmlEscaper::escape("hello"), "hello"); + /// assert_eq!(StringXmlEscaper::escape("a & b"), "a & b"); + /// assert_eq!(StringXmlEscaper::escape("<tag>"), "<tag>"); + /// assert_eq!( + /// StringXmlEscaper::escape(r#"say "hi""#), + /// "say "hi"" + /// ); + /// assert_eq!(StringXmlEscaper::escape("it's"), "it's"); + /// ``` + pub fn escape(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + s.chars().for_each(|c| match c { + '&' => result.push_str("&"), + '<' => result.push_str("<"), + '>' => result.push_str(">"), + '"' => result.push_str("""), + '\'' => result.push_str("'"), + _ => result.push(c), + }); + result + } +} diff --git a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs index d85298b..bf03e15 100644 --- a/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs +++ b/crate/input_ir_rt/src/svg_elements_to_svg_mapper.rs @@ -1,5 +1,7 @@ use std::fmt::Write; +use crate::string_xml_escaper::StringXmlEscaper; + use base64::{prelude::BASE64_STANDARD, Engine}; use disposition_ir_model::entity::EntityTailwindClasses; use disposition_svg_model::{SvgEdgeInfo, SvgElements, SvgNodeInfo}; @@ -239,7 +241,7 @@ impl SvgElementsToSvgMapper { // Add tooltip element if present if !svg_node_info.tooltip.is_empty() { - let tooltip_escaped = Self::escape_xml_content(&svg_node_info.tooltip); + let tooltip_escaped = StringXmlEscaper::escape(&svg_node_info.tooltip); write!(content_buffer, "<title>{tooltip_escaped}").unwrap(); } @@ -373,7 +375,7 @@ impl SvgElementsToSvgMapper { // Add tooltip element if present if !svg_edge_info.tooltip.is_empty() { - let tooltip_escaped = Self::escape_xml_content(&svg_edge_info.tooltip); + let tooltip_escaped = StringXmlEscaper::escape(&svg_edge_info.tooltip); write!(content_buffer, "{tooltip_escaped}").unwrap(); } @@ -396,22 +398,6 @@ impl SvgElementsToSvgMapper { }); } - /// Escapes XML text content special characters. - /// - /// Replaces `&`, `<`, and `>` with their XML entity equivalents so that - /// the string can be safely embedded as text content inside an XML element - /// such as ``. - fn escape_xml_content(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - s.chars().for_each(|c| match c { - '&' => result.push_str("&"), - '<' => result.push_str("<"), - '>' => result.push_str(">"), - _ => result.push(c), - }); - result - } - /// Returns the `class=".."` attribute with `&` escaped as `&`. fn class_attr_escaped(tailwind_classes: String) -> String { if tailwind_classes.is_empty() { diff --git a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs index 68d1fc7..93f8b55 100644 --- a/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs +++ b/crate/input_ir_rt/src/taffy_to_svg_elements_mapper/svg_node_info_builder.rs @@ -1,3 +1,4 @@ +use crate::string_xml_escaper::StringXmlEscaper; use disposition_ir_model::{node::NodeId, IrDiagram}; use disposition_model_common::{entity::EntityType, Map}; use disposition_svg_model::{SvgNodeInfo, SvgNodeInfoCircle, SvgProcessInfo, SvgTextSpan}; @@ -101,7 +102,9 @@ impl SvgNodeInfoBuilder { .map(|spans| { spans .iter() - .map(|span| SvgTextSpan::new(span.x, span.y, Self::escape_xml(&span.text))) + .map(|span| { + SvgTextSpan::new(span.x, span.y, StringXmlEscaper::escape(&span.text)) + }) .collect() }) .unwrap_or_default(); @@ -244,18 +247,4 @@ impl SvgNodeInfoBuilder { None } } - - /// Escape XML special characters in text content. - fn escape_xml(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - s.chars().for_each(|c| match c { - '&' => result.push_str("&"), - '<' => result.push_str("<"), - '>' => result.push_str(">"), - '"' => result.push_str("""), - '\'' => result.push_str("'"), - _ => result.push(c), - }); - result - } } From 7a2094814e408aa5bafba64805ab7c8ebaa36a95 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Tue, 21 Apr 2026 07:28:04 +1200 Subject: [PATCH 4/7] Update `tailwind.css`. --- app/playground/assets/tailwind.css | 54 ------------------------------ 1 file changed, 54 deletions(-) diff --git a/app/playground/assets/tailwind.css b/app/playground/assets/tailwind.css index 1d26be5..80bf2b4 100644 --- a/app/playground/assets/tailwind.css +++ b/app/playground/assets/tailwind.css @@ -35,9 +35,7 @@ --spacing: 0.25rem; --container-sm: 24rem; --container-lg: 32rem; - --container-2xl: 42rem; --container-3xl: 48rem; - --container-4xl: 56rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -46,8 +44,6 @@ --text-lg--line-height: calc(1.75 / 1.125); --text-xl: 1.25rem; --text-xl--line-height: calc(1.75 / 1.25); - --text-2xl: 1.5rem; - --text-2xl--line-height: calc(2 / 1.5); --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-wide: 0.025em; @@ -292,9 +288,6 @@ .mr-1 { margin-right: calc(var(--spacing) * 1); } - .mb-0 { - margin-bottom: calc(var(--spacing) * 0); - } .mb-0\.5 { margin-bottom: calc(var(--spacing) * 0.5); } @@ -331,9 +324,6 @@ .inline-block { display: inline-block; } - .table { - display: table; - } .h-4 { height: calc(var(--spacing) * 4); } @@ -418,9 +408,6 @@ .shrink-0 { flex-shrink: 0; } - .border-collapse { - border-collapse: collapse; - } .rotate-90 { rotate: 90deg; } @@ -439,9 +426,6 @@ .cursor-pointer { cursor: pointer; } - .resize { - resize: both; - } .list-inside { list-style-position: inside; } @@ -591,9 +575,6 @@ .border-b-transparent { border-bottom-color: transparent; } - .bg-black { - background-color: var(--color-black); - } .bg-black\/50 { background-color: color-mix(in srgb, #000000 50%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -636,9 +617,6 @@ .p-4 { padding: calc(var(--spacing) * 4); } - .px-0 { - padding-inline: calc(var(--spacing) * 0); - } .px-0\.5 { padding-inline: calc(var(--spacing) * 0.5); } @@ -654,9 +632,6 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } - .py-0 { - padding-block: calc(var(--spacing) * 0); - } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -747,9 +722,6 @@ .text-nowrap { text-wrap: nowrap; } - .text-wrap { - text-wrap: wrap; - } .whitespace-nowrap { white-space: nowrap; } @@ -804,9 +776,6 @@ .line-through { text-decoration-line: line-through; } - .underline { - text-decoration-line: underline; - } .accent-blue-500 { accent-color: var(--color-blue-500); } @@ -831,10 +800,6 @@ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } .blur { --tw-blur: blur(8px); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); @@ -843,9 +808,6 @@ --tw-invert: invert(100%); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } .transition-all { transition-property: all; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1161,11 +1123,6 @@ flex-direction: row; } } - .\[\&_\*\]\:mb-2 { - & * { - margin-bottom: calc(var(--spacing) * 2); - } - } .\[\&_\*\]\:mb-3 { & * { margin-bottom: calc(var(--spacing) * 3); @@ -1222,11 +1179,6 @@ } } } - .\[\&\>li\]\:mb-2 { - &>li { - margin-bottom: calc(var(--spacing) * 2); - } - } .\[\&\>rect\]\:fill-gray-300 { &>rect { fill: var(--color-gray-300); @@ -1416,11 +1368,6 @@ inherits: false; initial-value: 0 0 #0000; } -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} @property --tw-blur { syntax: "*"; inherits: false; @@ -1504,7 +1451,6 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; - --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; From 82f6256fb8ae83ecd9780af5e4168a5848d1a594 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Tue, 21 Apr 2026 07:32:14 +1200 Subject: [PATCH 5/7] Add tip to `ThingEntityTooltipsPage`. --- app/playground/src/components/editor/things_page.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/playground/src/components/editor/things_page.rs b/app/playground/src/components/editor/things_page.rs index 2bc189d..6e04607 100644 --- a/app/playground/src/components/editor/things_page.rs +++ b/app/playground/src/components/editor/things_page.rs @@ -356,6 +356,12 @@ pub fn ThingEntityTooltipsPage(input_diagram: Signal<InputDiagram<'static>>) -> class: "text-xs text-gray-500 mb-1", "Tooltip text (markdown) shown on hover." } + p { + class: "text-xs text-gray-500 mb-1", + "Note that for edges, you need to add `__{{index}}` to the edge group ID where {{index}} is the n'th edge in the group." + br {} + "For example, for an edge group whose ID is `edge_dep_a_to_b`, the first edge's ID is `edge_dep_a_to_b__0`, the second `edge_dep_a_to_b__1`, and so on." + } ReorderableContainer { data_attr: "data-entry-id".to_owned(), From 172b278a834ac7849771992835541c8501524595 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Tue, 21 Apr 2026 07:51:50 +1200 Subject: [PATCH 6/7] Move `Thing: Tooltips` into `Entity: Tooltips` page. --- .../disposition_editor/editor_page_content.rs | 8 +- .../disposition_editor/editor_tab_bar.rs | 11 +- .../editor_tab_bar/editor_tab_bar_entity.rs | 62 +++++++++ .../editor_tab_bar_entity_pages.rs | 61 +++++++++ .../editor_tab_bar/editor_tab_bar_tabs.rs | 9 +- app/playground/src/components/editor.rs | 8 +- .../src/components/editor/entity_page.rs | 38 ++++++ .../components/editor/entity_tooltips_page.rs | 127 ++++++++++++++++++ .../src/components/editor/things_page.rs | 111 +-------------- app/playground/src/editor_state.rs | 4 +- .../src/editor_state/editor_page.rs | 17 ++- .../src/editor_state/editor_page_entity.rs | 27 ++++ .../src/editor_state/editor_page_thing.rs | 6 +- 13 files changed, 359 insertions(+), 130 deletions(-) create mode 100644 app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity.rs create mode 100644 app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity_pages.rs create mode 100644 app/playground/src/components/editor/entity_page.rs create mode 100644 app/playground/src/components/editor/entity_tooltips_page.rs create mode 100644 app/playground/src/editor_state/editor_page_entity.rs diff --git a/app/playground/src/components/disposition_editor/editor_page_content.rs b/app/playground/src/components/disposition_editor/editor_page_content.rs index 667bbda..c7a30f4 100644 --- a/app/playground/src/components/disposition_editor/editor_page_content.rs +++ b/app/playground/src/components/disposition_editor/editor_page_content.rs @@ -10,11 +10,10 @@ use disposition::input_model::InputDiagram; use crate::{ components::editor::{ - EntityTypesPage, ProcessesPage, RenderOptionsPage, TagsPage, TextPage, ThemeBaseStylesPage, + EntityPage, ProcessesPage, RenderOptionsPage, TagsPage, TextPage, ThemeBaseStylesPage, ThemeDependenciesStylesPage, ThemeProcessStepStylesPage, ThemeStyleAliasesPage, ThemeTagsFocusPage, ThemeTypesStylesPage, ThingCopyTextPage, ThingDependenciesPage, - ThingEntityDescsPage, ThingEntityTooltipsPage, ThingInteractionsPage, ThingLayoutPage, - ThingNamesPage, + ThingEntityDescsPage, ThingInteractionsPage, ThingLayoutPage, ThingNamesPage, }, editor_state::{EditorPage, EditorPageTheme, EditorPageThing}, }; @@ -32,14 +31,13 @@ pub fn EditorPageContent( EditorPageThing::Names => rsx! { ThingNamesPage { input_diagram } }, EditorPageThing::CopyText => rsx! { ThingCopyTextPage { input_diagram } }, EditorPageThing::EntityDescs => rsx! { ThingEntityDescsPage { input_diagram } }, - EditorPageThing::EntityTooltips => rsx! { ThingEntityTooltipsPage { input_diagram } }, }, EditorPage::ThingLayout => rsx! { ThingLayoutPage { input_diagram } }, EditorPage::ThingDependencies => rsx! { ThingDependenciesPage { input_diagram } }, EditorPage::ThingInteractions => rsx! { ThingInteractionsPage { input_diagram } }, EditorPage::Processes => rsx! { ProcessesPage { input_diagram } }, EditorPage::Tags => rsx! { TagsPage { input_diagram } }, - EditorPage::EntityTypes => rsx! { EntityTypesPage { input_diagram } }, + EditorPage::Entity(_) => rsx! { EntityPage { active_page, input_diagram } }, EditorPage::RenderOptions => rsx! { RenderOptionsPage { input_diagram } }, EditorPage::Theme(sub) => match sub { EditorPageTheme::BaseStyles => rsx! { ThemeBaseStylesPage { input_diagram } }, diff --git a/app/playground/src/components/disposition_editor/editor_tab_bar.rs b/app/playground/src/components/disposition_editor/editor_tab_bar.rs index 40af8f2..38d1150 100644 --- a/app/playground/src/components/disposition_editor/editor_tab_bar.rs +++ b/app/playground/src/components/disposition_editor/editor_tab_bar.rs @@ -18,10 +18,13 @@ use dioxus::{ use crate::editor_state::EditorPage; use self::{ - editor_tab_bar_tabs::EditorTabBarTabs, editor_tab_bar_theme::EditorTabBarTheme, - editor_tab_bar_thing::EditorTabBarThing, editor_tab_bar_thing_pages::EditorTabBarThingPages, + editor_tab_bar_entity::EditorTabBarEntity, editor_tab_bar_tabs::EditorTabBarTabs, + editor_tab_bar_theme::EditorTabBarTheme, editor_tab_bar_thing::EditorTabBarThing, + editor_tab_bar_thing_pages::EditorTabBarThingPages, }; +mod editor_tab_bar_entity; +mod editor_tab_bar_entity_pages; mod editor_tab_bar_tabs; mod editor_tab_bar_theme; mod editor_tab_bar_theme_pages; @@ -123,16 +126,16 @@ pub fn EditorTabBar(active_page: Signal<EditorPage>) -> Element { EditorTabBarTabs { active_page } } - // === Things sub-tabs (only visible when a Things page is active) === // + // === Sub-tabs (only visible when a grouped page is active) === // match current_page { EditorPage::Thing(_) => rsx! { EditorTabBarThing { active_page } }, + EditorPage::Entity(_) => rsx! { EditorTabBarEntity { active_page } }, EditorPage::Theme(_) => rsx! { EditorTabBarTheme { active_page } }, EditorPage::ThingLayout | EditorPage::ThingDependencies | EditorPage::ThingInteractions | EditorPage::Processes | EditorPage::Tags | - EditorPage::EntityTypes | EditorPage::RenderOptions | EditorPage::Text => rsx! {}, } diff --git a/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity.rs b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity.rs new file mode 100644 index 0000000..ffa12a5 --- /dev/null +++ b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity.rs @@ -0,0 +1,62 @@ +use dioxus::{ + prelude::{ + component, dioxus_core, dioxus_elements, dioxus_signals, document, rsx, Element, Key, Props, + }, + signals::Signal, +}; + +use crate::{ + components::disposition_editor::editor_tab_bar::editor_tab_bar_entity_pages::EditorTabBarEntityPages, + editor_state::EditorPage, +}; + +#[component] +pub(crate) fn EditorTabBarEntity(active_page: Signal<EditorPage>) -> Element { + rsx! { + div { + class: " + flex + flex-row + flex-wrap + gap-1 + border-b + border-gray-700 + mb-1 + pl-2 + ", + role: "tablist", + + onkeydown: move |evt| { + match evt.key() { + Key::ArrowLeft => { + evt.prevent_default(); + document::eval( + "(() => {\ + let el = document.activeElement;\ + if (!el || el.getAttribute('role') !== 'tab') return;\ + let prev = el.previousElementSibling;\ + if (prev) prev.focus();\ + else { let last = el.parentElement?.lastElementChild; if (last) last.focus(); }\ + })()" + ); + } + Key::ArrowRight => { + evt.prevent_default(); + document::eval( + "(() => {\ + let el = document.activeElement;\ + if (!el || el.getAttribute('role') !== 'tab') return;\ + let next = el.nextElementSibling;\ + if (next) next.focus();\ + else { let first = el.parentElement?.firstElementChild; if (first) first.focus(); }\ + })()" + ); + } + _ => {} + } + }, + + EditorTabBarEntityPages { active_page } + } + } +} diff --git a/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity_pages.rs b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity_pages.rs new file mode 100644 index 0000000..2ba1246 --- /dev/null +++ b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_entity_pages.rs @@ -0,0 +1,61 @@ +use dioxus::{ + prelude::{component, dioxus_core, dioxus_elements, dioxus_signals, rsx, Element, Key, Props}, + signals::{ReadableExt, Signal, WritableExt}, +}; + +use crate::{ + components::disposition_editor::editor_tab_bar::{SUB_TAB_CLASS, TAB_ACTIVE, TAB_INACTIVE}, + editor_state::{EditorPage, EditorPageEntity}, +}; + +#[component] +pub fn EditorTabBarEntityPages(active_page: Signal<EditorPage>) -> Element { + let current_page = active_page.read().clone(); + + rsx! { + for sub in enum_iterator::all::<EditorPageEntity>() { + { + let sub_page = EditorPage::Entity(sub.clone()); + let is_active = current_page == sub_page; + let css = format!( + "{SUB_TAB_CLASS} {}", + if is_active { TAB_ACTIVE } else { TAB_INACTIVE } + ); + let tab_index = if is_active { "0" } else { "-1" }; + let sub_page_click = sub_page.clone(); + let sub_page_key = sub_page.clone(); + + rsx! { + span { + key: "{sub.label()}", + role: "tab", + tabindex: "{tab_index}", + "aria-selected": if is_active { "true" } else { "false" }, + class: "{css}", + onclick: { + let page = sub_page_click.clone(); + move |_| { + active_page.set(page.clone()); + } + }, + onkeydown: { + let page = sub_page_key.clone(); + move |evt| { + let activate = match evt.key() { + Key::Enter => true, + Key::Character(ref c) if c == " " => true, + _ => false, + }; + if activate { + evt.prevent_default(); + active_page.set(page.clone()); + } + } + }, + "{sub.label()}" + } + } + } + } + } +} diff --git a/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_tabs.rs b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_tabs.rs index 657f3cd..64c57c5 100644 --- a/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_tabs.rs +++ b/app/playground/src/components/disposition_editor/editor_tab_bar/editor_tab_bar_tabs.rs @@ -5,7 +5,7 @@ use dioxus::{ use crate::{ components::disposition_editor::editor_tab_bar::{TAB_ACTIVE, TAB_CLASS, TAB_INACTIVE}, - editor_state::{EditorPage, EditorPageTheme, EditorPageThing}, + editor_state::{EditorPage, EditorPageEntity, EditorPageTheme, EditorPageThing}, }; #[component] @@ -78,6 +78,13 @@ fn editor_tab_bar_top_level_activate(mut active_page: Signal<EditorPage>, entry: active_page.set(EditorPage::Thing(EditorPageThing::default())); } } + EditorPage::Entity(_) => { + // If already on an entity page, stay there; + // otherwise default to Entity::EntityTypes. + if !active_page.peek().is_entity() { + active_page.set(EditorPage::Entity(EditorPageEntity::default())); + } + } EditorPage::Theme(_) => { // If already on a theme page, stay there; // otherwise default to Theme::StyleAliases. diff --git a/app/playground/src/components/editor.rs b/app/playground/src/components/editor.rs index 504f87c..babf3aa 100644 --- a/app/playground/src/components/editor.rs +++ b/app/playground/src/components/editor.rs @@ -7,6 +7,8 @@ pub use self::{ datalists::EditorDataLists, + entity_page::EntityPage, + entity_tooltips_page::EntityTooltipsPage, entity_types_page::EntityTypesPage, processes_page::ProcessesPage, render_options_page::RenderOptionsPage, @@ -18,9 +20,7 @@ pub use self::{ }, thing_dependencies_page::{ThingDependenciesPage, ThingInteractionsPage}, thing_layout_page::ThingLayoutPage, - things_page::{ - ThingCopyTextPage, ThingEntityDescsPage, ThingEntityTooltipsPage, ThingNamesPage, - }, + things_page::{ThingCopyTextPage, ThingEntityDescsPage, ThingNamesPage}, }; pub mod datalists; @@ -31,6 +31,8 @@ pub(crate) mod keyboard_nav; pub(crate) mod reorderable; pub(crate) mod theme_styles_editor; +pub(crate) mod entity_page; +pub(crate) mod entity_tooltips_page; pub(crate) mod entity_types_page; pub(crate) mod processes_page; pub(crate) mod render_options_page; diff --git a/app/playground/src/components/editor/entity_page.rs b/app/playground/src/components/editor/entity_page.rs new file mode 100644 index 0000000..c7ec60d --- /dev/null +++ b/app/playground/src/components/editor/entity_page.rs @@ -0,0 +1,38 @@ +//! Entity editor page. +//! +//! Provides sub-pages for: +//! - Entity Types (`entity_types`: `Id` -> `Set<EntityType>`) +//! - Entity Tooltips (`entity_tooltips`: `Id` -> tooltip) + +use dioxus::{ + prelude::{component, dioxus_core, dioxus_signals, rsx, Element, Props}, + signals::{ReadableExt, Signal}, +}; +use disposition::input_model::InputDiagram; + +use crate::{ + components::editor::{EntityTooltipsPage, EntityTypesPage}, + editor_state::{EditorPage, EditorPageEntity}, +}; + +/// The **Entity** editor page. +/// +/// Dispatches to the active entity sub-page: +/// - [`EntityTypesPage`]: entity type assignments for common styling. +/// - [`EntityTooltipsPage`]: tooltip text shown on hover for nodes and edges. +#[component] +pub fn EntityPage( + active_page: Signal<EditorPage>, + input_diagram: Signal<InputDiagram<'static>>, +) -> Element { + let page = active_page.read().clone(); + match page { + EditorPage::Entity(EditorPageEntity::EntityTypes) => { + rsx! { EntityTypesPage { input_diagram } } + } + EditorPage::Entity(EditorPageEntity::EntityTooltips) => { + rsx! { EntityTooltipsPage { input_diagram } } + } + _ => rsx! {}, + } +} diff --git a/app/playground/src/components/editor/entity_tooltips_page.rs b/app/playground/src/components/editor/entity_tooltips_page.rs new file mode 100644 index 0000000..a9df00f --- /dev/null +++ b/app/playground/src/components/editor/entity_tooltips_page.rs @@ -0,0 +1,127 @@ +//! Entity Tooltips editor page. +//! +//! Allows editing `entity_tooltips`: tooltip text (markdown) shown on hover +//! for both nodes (things) and edges. + +use dioxus::{ + hooks::use_signal, + prelude::{ + component, dioxus_core, dioxus_elements, dioxus_signals, rsx, Element, Props, WritableExt, + }, + signals::{ReadableExt, Signal}, +}; +use disposition::input_model::InputDiagram; +use disposition_input_rt::{OnChangeTarget, ThingsPageOps}; + +use crate::components::editor::{ + common::{RenameRefocus, ADD_BTN, SECTION_HEADING}, + datalists::list_ids, + id_value_row::IdValueRow, + reorderable::ReorderableContainer, +}; + +/// The **Entity: Tooltips** editor sub-page. +/// +/// Edits `entity_tooltips` -- tooltip text (markdown) shown on hover for both +/// nodes (things) and edges. +#[component] +pub fn EntityTooltipsPage(input_diagram: Signal<InputDiagram<'static>>) -> Element { + let tooltip_drag_idx: Signal<Option<usize>> = use_signal(|| None); + let tooltip_drop_target: Signal<Option<usize>> = use_signal(|| None); + let tooltip_focus_idx: Signal<Option<usize>> = use_signal(|| None); + let tooltip_rename_refocus: Signal<Option<RenameRefocus>> = use_signal(|| None); + + let diagram = input_diagram.read(); + let tooltip_entries: Vec<(String, String)> = diagram + .entity_tooltips + .iter() + .map(|(id, tip)| (id.as_str().to_owned(), tip.clone())) + .collect(); + drop(diagram); + + let tooltip_count = tooltip_entries.len(); + + rsx! { + div { + class: "flex flex-col gap-2", + + h3 { class: SECTION_HEADING, "Entity Tooltips" } + p { + class: "text-xs text-gray-500 mb-1", + "Tooltip text (markdown) shown on hover." + } + p { + class: "text-xs text-gray-500 mb-1", + "Note that for edges, you need to add `__{{index}}` to the edge group ID where {{index}} is the n'th edge in the group." + br {} + "For example, for an edge group whose ID is `edge_dep_a_to_b`, the first edge's ID is `edge_dep_a_to_b__0`, the second `edge_dep_a_to_b__1`, and so on." + } + + ReorderableContainer { + data_attr: "data-entry-id".to_owned(), + section_id: "entity_tooltips".to_owned(), + focus_index: tooltip_focus_idx, + rename_refocus: Some(tooltip_rename_refocus), + + for (idx, (id, tip)) in tooltip_entries.iter().enumerate() { + { + let id = id.clone(); + let tip = tip.clone(); + let on_change = OnChangeTarget::EntityTooltip; + let current_value = tip.clone(); + rsx! { + IdValueRow { + key: "tip_{id}", + entry_id: id, + entry_value: tip, + id_list: list_ids::ENTITY_IDS.to_owned(), + id_placeholder: "id".to_owned(), + value_placeholder: "value".to_owned(), + index: idx, + entry_count: tooltip_count, + drag_index: tooltip_drag_idx, + drop_target: tooltip_drop_target, + focus_index: tooltip_focus_idx, + rename_refocus: tooltip_rename_refocus, + on_move: move |(from, to)| { + ThingsPageOps::kv_entry_move(&mut input_diagram.write(), on_change, from, to); + }, + on_rename: { + let current_value = current_value.clone(); + move |(id_old, id_new): (String, String)| { + ThingsPageOps::kv_entry_rename( + &mut input_diagram.write(), + on_change, + &id_old, + &id_new, + ¤t_value, + ); + } + }, + on_update: move |(id, value): (String, String)| { + ThingsPageOps::kv_entry_update(&mut input_diagram.write(), on_change, &id, &value); + }, + on_remove: move |id: String| { + ThingsPageOps::kv_entry_remove(&mut input_diagram.write(), on_change, &id); + }, + on_add: move |insert_at: usize| { + ThingsPageOps::entity_tooltip_add(&mut input_diagram.write()); + ThingsPageOps::kv_entry_move(&mut input_diagram.write(), on_change, tooltip_count, insert_at); + }, + } + } + } + } + } + + button { + class: ADD_BTN, + tabindex: 0, + onclick: move |_| { + ThingsPageOps::entity_tooltip_add(&mut input_diagram.write()); + }, + "+ Add tooltip" + } + } + } +} diff --git a/app/playground/src/components/editor/things_page.rs b/app/playground/src/components/editor/things_page.rs index 6e04607..60a4a7f 100644 --- a/app/playground/src/components/editor/things_page.rs +++ b/app/playground/src/components/editor/things_page.rs @@ -4,7 +4,9 @@ //! - Thing Names (`things`: `ThingId` -> display name) //! - Thing Copy Text (`thing_copy_text`: `ThingId` -> clipboard text) //! - Entity Descriptions (`entity_descs`: `Id` -> description) -//! - Entity Tooltips (`entity_tooltips`: `Id` -> tooltip) +//! +//! Note: Entity Tooltips have moved to the "Entity" group tab +//! ([`EntityTooltipsPage`](crate::components::editor::EntityTooltipsPage)). use dioxus::{ hooks::use_signal, @@ -324,110 +326,3 @@ pub fn ThingEntityDescsPage(input_diagram: Signal<InputDiagram<'static>>) -> Ele } } } - -// === Entity Tooltips sub-page === // - -/// The **Things: Tooltips** editor sub-page. -/// -/// Edits `entity_tooltips` -- tooltip text (markdown) shown on hover. -#[component] -pub fn ThingEntityTooltipsPage(input_diagram: Signal<InputDiagram<'static>>) -> Element { - let tooltip_drag_idx: Signal<Option<usize>> = use_signal(|| None); - let tooltip_drop_target: Signal<Option<usize>> = use_signal(|| None); - let tooltip_focus_idx: Signal<Option<usize>> = use_signal(|| None); - let tooltip_rename_refocus: Signal<Option<RenameRefocus>> = use_signal(|| None); - - let diagram = input_diagram.read(); - let tooltip_entries: Vec<(String, String)> = diagram - .entity_tooltips - .iter() - .map(|(id, tip)| (id.as_str().to_owned(), tip.clone())) - .collect(); - drop(diagram); - - let tooltip_count = tooltip_entries.len(); - - rsx! { - div { - class: "flex flex-col gap-2", - - h3 { class: SECTION_HEADING, "Entity Tooltips" } - p { - class: "text-xs text-gray-500 mb-1", - "Tooltip text (markdown) shown on hover." - } - p { - class: "text-xs text-gray-500 mb-1", - "Note that for edges, you need to add `__{{index}}` to the edge group ID where {{index}} is the n'th edge in the group." - br {} - "For example, for an edge group whose ID is `edge_dep_a_to_b`, the first edge's ID is `edge_dep_a_to_b__0`, the second `edge_dep_a_to_b__1`, and so on." - } - - ReorderableContainer { - data_attr: "data-entry-id".to_owned(), - section_id: "entity_tooltips".to_owned(), - focus_index: tooltip_focus_idx, - rename_refocus: Some(tooltip_rename_refocus), - - for (idx, (id, tip)) in tooltip_entries.iter().enumerate() { - { - let id = id.clone(); - let tip = tip.clone(); - let on_change = OnChangeTarget::EntityTooltip; - let current_value = tip.clone(); - rsx! { - IdValueRow { - key: "tip_{id}", - entry_id: id, - entry_value: tip, - id_list: list_ids::ENTITY_IDS.to_owned(), - id_placeholder: "id".to_owned(), - value_placeholder: "value".to_owned(), - index: idx, - entry_count: tooltip_count, - drag_index: tooltip_drag_idx, - drop_target: tooltip_drop_target, - focus_index: tooltip_focus_idx, - rename_refocus: tooltip_rename_refocus, - on_move: move |(from, to)| { - ThingsPageOps::kv_entry_move(&mut input_diagram.write(), on_change, from, to); - }, - on_rename: { - let current_value = current_value.clone(); - move |(id_old, id_new): (String, String)| { - ThingsPageOps::kv_entry_rename( - &mut input_diagram.write(), - on_change, - &id_old, - &id_new, - ¤t_value, - ); - } - }, - on_update: move |(id, value): (String, String)| { - ThingsPageOps::kv_entry_update(&mut input_diagram.write(), on_change, &id, &value); - }, - on_remove: move |id: String| { - ThingsPageOps::kv_entry_remove(&mut input_diagram.write(), on_change, &id); - }, - on_add: move |insert_at: usize| { - ThingsPageOps::entity_tooltip_add(&mut input_diagram.write()); - ThingsPageOps::kv_entry_move(&mut input_diagram.write(), on_change, tooltip_count, insert_at); - }, - } - } - } - } - } - - button { - class: ADD_BTN, - tabindex: 0, - onclick: move |_| { - ThingsPageOps::entity_tooltip_add(&mut input_diagram.write()); - }, - "+ Add tooltip" - } - } - } -} diff --git a/app/playground/src/editor_state.rs b/app/playground/src/editor_state.rs index b77f68f..8b53335 100644 --- a/app/playground/src/editor_state.rs +++ b/app/playground/src/editor_state.rs @@ -8,11 +8,13 @@ //! fragment. mod editor_page; +mod editor_page_entity; mod editor_page_theme; mod editor_page_thing; pub use self::{ - editor_page::EditorPage, editor_page_theme::EditorPageTheme, editor_page_thing::EditorPageThing, + editor_page::EditorPage, editor_page_entity::EditorPageEntity, + editor_page_theme::EditorPageTheme, editor_page_thing::EditorPageThing, }; use std::fmt; diff --git a/app/playground/src/editor_state/editor_page.rs b/app/playground/src/editor_state/editor_page.rs index 9c44655..b6dad98 100644 --- a/app/playground/src/editor_state/editor_page.rs +++ b/app/playground/src/editor_state/editor_page.rs @@ -6,7 +6,7 @@ use enum_iterator::Sequence; use serde::{Deserialize, Serialize}; -use super::{EditorPageTheme, EditorPageThing}; +use super::{EditorPageEntity, EditorPageTheme, EditorPageThing}; /// Identifies which editor page (tab) is currently active. /// @@ -32,8 +32,8 @@ pub enum EditorPage { Processes, /// Tags: tag names and the things associated with each tag. Tags, - /// Entity Types: entity type assignments for common styling. - EntityTypes, + /// Entity group: sub-pages for entity type assignments and tooltips. + Entity(EditorPageEntity), /// Render options: edge curvature and rank direction. RenderOptions, /// Theme group: sub-pages for style aliases, base styles, etc. @@ -79,7 +79,7 @@ impl EditorPage { Self::ThingInteractions => "Interactions", Self::Processes => "Processes", Self::Tags => "Tags", - Self::EntityTypes => "Entity Types", + Self::Entity(sub) => sub.label(), Self::RenderOptions => "Render Options", Self::Theme(sub) => sub.label(), Self::Text => "Text", @@ -89,10 +89,11 @@ impl EditorPage { /// The label shown on the top-level tab. /// /// For `Thing(_)` this returns `"Things"`, for `Theme(_)` this - /// returns `"Theme"`, otherwise delegates to [`label`](Self::label). + /// `"Theme"`, otherwise delegates to [`label`](Self::label). pub fn top_level_label(&self) -> &'static str { match self { Self::Thing(_) => "Things", + Self::Entity(_) => "Entity", Self::Theme(_) => "Theme", other => other.label(), } @@ -103,6 +104,11 @@ impl EditorPage { matches!(self, Self::Thing(_)) } + /// Returns `true` if this page belongs to the Entity group. + pub fn is_entity(&self) -> bool { + matches!(self, Self::Entity(_)) + } + /// Returns `true` if this page belongs to the Theme group. pub fn is_theme(&self) -> bool { matches!(self, Self::Theme(_)) @@ -117,6 +123,7 @@ impl EditorPage { pub fn same_top_level(&self, other: &EditorPage) -> bool { match (self, other) { (Self::Thing(_), Self::Thing(_)) => true, + (Self::Entity(_), Self::Entity(_)) => true, (Self::Theme(_), Self::Theme(_)) => true, _ => std::mem::discriminant(self) == std::mem::discriminant(other), } diff --git a/app/playground/src/editor_state/editor_page_entity.rs b/app/playground/src/editor_state/editor_page_entity.rs new file mode 100644 index 0000000..f8bb8bb --- /dev/null +++ b/app/playground/src/editor_state/editor_page_entity.rs @@ -0,0 +1,27 @@ +//! Sub-pages within the "Entity" group tab. + +use serde::{Deserialize, Serialize}; + +/// Identifies which sub-page within the "Entity" group is active. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, enum_iterator::Sequence)] +#[serde(rename_all = "snake_case")] +pub enum EditorPageEntity { + #[default] + /// Entity: entity type assignments for common styling. + EntityTypes, + /// Entity: entity tooltip text shown on hover. + EntityTooltips, +} + +impl EditorPageEntity { + /// A human-readable label for each sub-page, suitable for rendering in a + /// sub-tab bar. + /// + /// e.g. `"Entity: Types"`, `"Entity: Tooltips"`. + pub fn label(&self) -> &'static str { + match self { + Self::EntityTypes => "Entity: Types", + Self::EntityTooltips => "Entity: Tooltips", + } + } +} diff --git a/app/playground/src/editor_state/editor_page_thing.rs b/app/playground/src/editor_state/editor_page_thing.rs index fd4ad01..067004b 100644 --- a/app/playground/src/editor_state/editor_page_thing.rs +++ b/app/playground/src/editor_state/editor_page_thing.rs @@ -1,4 +1,7 @@ //! Sub-pages within the "Things" group tab. +//! +//! Note: entity tooltips have moved to the "Entity" group tab +//! ([`EditorPageEntity`](crate::editor_state::EditorPageEntity)). use serde::{Deserialize, Serialize}; @@ -13,8 +16,6 @@ pub enum EditorPageThing { CopyText, /// Things: entity descriptions. EntityDescs, - /// Things: entity tooltips. - EntityTooltips, } impl EditorPageThing { @@ -27,7 +28,6 @@ impl EditorPageThing { Self::Names => "Things: Names", Self::CopyText => "Things: Copy Text", Self::EntityDescs => "Things: Descriptions", - Self::EntityTooltips => "Things: Tooltips", } } } From 5acaf5aad5d9b751dbe3948aa3aaef3df96b4309 Mon Sep 17 00:00:00 2001 From: Azriel Hoh <azriel91@gmail.com> Date: Tue, 21 Apr 2026 07:53:46 +1200 Subject: [PATCH 7/7] Update `README.md` and `CHANGELOG.md`. --- CHANGELOG.md | 7 +++++++ README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8c75f..c2df084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## unreleased + +* Support rendering tooltips. ([#26][#26]) + +[#26]: https://github.com/azriel91/disposition/pull/26 + + ## 0.1.0 (2026-04-11) * Add `playground`. ([#16][#16], [#17][#17]) diff --git a/README.md b/README.md index 316206b..9540aea 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Playground: <https://azriel.im/disposition>. * [x] Circle as node shape. * [x] Light and dark modes. * [x] Dependencies between process steps. -* [ ] Tooltips. +* [x] Tooltips. * [ ] Images in nodes. * [ ] Responsive layout.