1use crate::config::{config_bool, config_f64};
2use crate::model::{Bounds, TreeViewDiagramLayout, TreeViewLineLayout, TreeViewNodeLayout};
3use crate::text::{TextMeasurer, TextStyle};
4use crate::theme::PresentationTheme;
5use crate::{Error, Result};
6use merman_core::MAX_DIAGRAM_NESTING_DEPTH;
7use merman_core::diagrams::tree_view::{
8 TreeViewDiagramRenderModel, TreeViewNodeRenderModel as TreeViewNode,
9};
10use serde_json::Value;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
14struct TreeViewConfig {
15 row_indent: f64,
16 padding_x: f64,
17 padding_y: f64,
18 line_thickness: f64,
19 use_max_width: bool,
20 label_font_size: f64,
21}
22
23pub fn layout_tree_view_diagram(
24 semantic: &Value,
25 effective_config: &Value,
26 measurer: &dyn TextMeasurer,
27) -> Result<TreeViewDiagramLayout> {
28 let model: TreeViewDiagramRenderModel = crate::json::from_value_ref(semantic)?;
29 layout_tree_view_diagram_typed(&model, effective_config, measurer)
30}
31
32pub fn layout_tree_view_diagram_typed(
33 model: &TreeViewDiagramRenderModel,
34 effective_config: &Value,
35 measurer: &dyn TextMeasurer,
36) -> Result<TreeViewDiagramLayout> {
37 let cfg = tree_view_config(effective_config);
38 validate_tree_view_render_depth(&model.root)?;
39 let style = TextStyle {
40 font_size: cfg.label_font_size,
41 ..Default::default()
42 };
43 let mut ctx = LayoutCtx {
44 cfg: cfg.clone(),
45 measurer,
46 style,
47 total_height: 0.0,
48 total_width: 0.0,
49 nodes: Vec::new(),
50 lines: Vec::new(),
51 };
52
53 layout_tree(&mut ctx, &model.root);
54
55 let min_x = -ctx.cfg.line_thickness / 2.0;
56 let total_width = ctx.total_width.max(1.0);
57 let total_height = ctx.total_height.max(1.0);
58 Ok(TreeViewDiagramLayout {
59 bounds: Some(Bounds {
60 min_x,
61 min_y: 0.0,
62 max_x: total_width,
63 max_y: total_height,
64 }),
65 total_width,
66 total_height,
67 row_indent: ctx.cfg.row_indent,
68 padding_x: ctx.cfg.padding_x,
69 padding_y: ctx.cfg.padding_y,
70 line_thickness: ctx.cfg.line_thickness,
71 use_max_width: ctx.cfg.use_max_width,
72 label_font_size: ctx.cfg.label_font_size,
73 nodes: ctx.nodes,
74 lines: ctx.lines,
75 })
76}
77
78fn validate_tree_view_render_depth(root: &TreeViewNode) -> Result<()> {
79 let mut stack = vec![(root, 0usize)];
80 while let Some((node, depth)) = stack.pop() {
81 if depth > MAX_DIAGRAM_NESTING_DEPTH {
82 return Err(Error::InvalidModel {
83 message: format!("treeView nesting depth exceeds {MAX_DIAGRAM_NESTING_DEPTH}"),
84 });
85 }
86 for child in &node.children {
87 stack.push((child, depth.saturating_add(1)));
88 }
89 }
90 Ok(())
91}
92
93struct LayoutCtx<'a> {
94 cfg: TreeViewConfig,
95 measurer: &'a dyn TextMeasurer,
96 style: TextStyle,
97 total_height: f64,
98 total_width: f64,
99 nodes: Vec<TreeViewNodeLayout>,
100 lines: Vec<TreeViewLineLayout>,
101}
102
103enum LayoutFrame<'a> {
104 Enter {
105 node: &'a TreeViewNode,
106 depth: usize,
107 },
108 Exit {
109 node: &'a TreeViewNode,
110 node_index: usize,
111 },
112}
113
114fn layout_tree(ctx: &mut LayoutCtx<'_>, root: &TreeViewNode) {
115 let mut stack = vec![LayoutFrame::Enter {
116 node: root,
117 depth: 0,
118 }];
119 let mut node_indices: HashMap<*const TreeViewNode, usize> = HashMap::new();
120
121 while let Some(frame) = stack.pop() {
122 match frame {
123 LayoutFrame::Enter { node, depth } => {
124 let node_index = push_node_layout(ctx, node, depth);
125 node_indices.insert(std::ptr::from_ref(node), node_index);
126 stack.push(LayoutFrame::Exit { node, node_index });
127 for child in node.children.iter().rev() {
128 stack.push(LayoutFrame::Enter {
129 node: child,
130 depth: depth.saturating_add(1),
131 });
132 }
133 }
134 LayoutFrame::Exit { node, node_index } => {
135 if let Some(last_child) = node.children.last() {
136 if let Some(last_child_idx) =
137 node_indices.get(&std::ptr::from_ref(last_child)).copied()
138 {
139 push_vertical_line(ctx, node_index, last_child_idx);
140 }
141 }
142 }
143 }
144 }
145}
146
147fn push_node_layout(ctx: &mut LayoutCtx<'_>, node: &TreeViewNode, depth: usize) -> usize {
148 let indent = depth as f64 * (ctx.cfg.row_indent + ctx.cfg.padding_x);
149 let label_width = tree_view_label_bbox_width_px(ctx.measurer, &node.name, &ctx.style);
150 let label_height = tree_view_label_bbox_height_px(ctx.cfg.label_font_size);
151 let height = label_height + ctx.cfg.padding_y * 2.0;
152 let width = label_width + ctx.cfg.padding_x * 2.0;
153 let y = ctx.total_height;
154 let idx = ctx.nodes.len();
155
156 ctx.nodes.push(TreeViewNodeLayout {
157 id: node.id,
158 level: node.level,
159 name: node.name.clone(),
160 depth,
161 x: indent,
162 y,
163 width,
164 height,
165 label_x: indent + ctx.cfg.padding_x,
166 label_y: y + height / 2.0,
167 label_width,
168 label_height,
169 });
170 ctx.lines.push(TreeViewLineLayout {
171 x1: indent - ctx.cfg.row_indent,
172 y1: y + height / 2.0,
173 x2: indent,
174 y2: y + height / 2.0,
175 stroke_width: ctx.cfg.line_thickness,
176 kind: "horizontal".to_string(),
177 });
178
179 ctx.total_width = ctx.total_width.max(indent + width);
180 ctx.total_height += height;
181
182 idx
183}
184
185fn push_vertical_line(ctx: &mut LayoutCtx<'_>, node_index: usize, last_child_idx: usize) {
186 let Some(current) = ctx.nodes.get(node_index) else {
187 return;
188 };
189 let Some(last_child) = ctx.nodes.get(last_child_idx) else {
190 return;
191 };
192 let current_x = current.x;
193 let current_y = current.y;
194 let current_height = current.height;
195 let last_child_y = last_child.y;
196 let last_child_height = last_child.height;
197
198 ctx.lines.push(TreeViewLineLayout {
199 x1: current_x + ctx.cfg.padding_x,
200 y1: current_y + current_height,
201 x2: current_x + ctx.cfg.padding_x,
202 y2: last_child_y + last_child_height / 2.0 + ctx.cfg.line_thickness / 2.0,
203 stroke_width: ctx.cfg.line_thickness,
204 kind: "vertical".to_string(),
205 });
206}
207
208fn tree_view_config(effective_config: &Value) -> TreeViewConfig {
209 let theme = PresentationTheme::new(effective_config).tree_view();
210 TreeViewConfig {
211 row_indent: config_f64(effective_config, &["treeView", "rowIndent"])
212 .unwrap_or(10.0)
213 .max(0.0),
214 padding_x: config_f64(effective_config, &["treeView", "paddingX"])
215 .unwrap_or(5.0)
216 .max(0.0),
217 padding_y: config_f64(effective_config, &["treeView", "paddingY"])
218 .unwrap_or(5.0)
219 .max(0.0),
220 line_thickness: config_f64(effective_config, &["treeView", "lineThickness"])
221 .unwrap_or(1.0)
222 .max(0.0),
223 use_max_width: config_bool(effective_config, &["treeView", "useMaxWidth"]).unwrap_or(true),
224 label_font_size: theme.label_font_size,
225 }
226}
227
228fn tree_view_label_bbox_height_px(font_size: f64) -> f64 {
229 (font_size.max(1.0) * 1.15).ceil()
230}
231
232fn tree_view_label_bbox_width_px(
233 measurer: &dyn TextMeasurer,
234 label: &str,
235 style: &TextStyle,
236) -> f64 {
237 if style.font_size > 16.0 {
238 measurer.measure(label, style).width.max(0.0)
239 } else {
240 measurer
241 .measure_svg_raw_text_bbox_width_px(label, style)
242 .max(0.0)
243 }
244}