ascii_dag/render/engine/style.rs
1//! Style vocabulary + per-element style contexts (temp/06 §8, R3/Q4).
2//!
3//! Styles are resolved **once per element at plan time** (the Q4 rule:
4//! never per cell) by plain fn pointers — `no_std`-safe, `Copy`,
5//! override-friendly. Defaults reproduce today's output exactly (R2.1).
6//! The full v1 styling surface is exposed publicly at RW7; until then
7//! these types serve the plan internally.
8
9use super::cell::Weight;
10use super::color::CellColor;
11use super::region::{NodePaintCtx, NodeRegion};
12
13/// Stroke weight for an edge, resolved to cell arms by the compositor.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum LineWeight {
17 /// Light strokes (`─ │`) — the default.
18 #[default]
19 Light,
20 /// Dashed strokes (`┈ ┊`) — the legacy rendering for back edges.
21 Dashed,
22 /// Double strokes (`═ ║`).
23 Double,
24}
25
26impl LineWeight {
27 /// The cell-arm weight for this line weight.
28 pub(crate) fn arm(self) -> Weight {
29 match self {
30 LineWeight::Light => Weight::Light,
31 LineWeight::Dashed => Weight::Dashed,
32 LineWeight::Double => Weight::Double,
33 }
34 }
35}
36
37/// Marker shape at an edge endpoint.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[non_exhaustive]
40pub enum MarkerShape {
41 /// No marker.
42 None,
43 /// Arrowhead (`↓ ↑ → ←`, dashed variants for back edges).
44 #[default]
45 Arrow,
46}
47
48/// A custom node painter: fills the node's declared `width × height`
49/// area through the clipped, node-local [`NodeRegion`]. Plain `fn`
50/// pointer (`no_std`-safe, non-capturing): derive content from the
51/// context (id, label, dimensions).
52pub type NodePaintFn = fn(&mut NodeRegion<'_, '_>, NodePaintCtx<'_>);
53
54/// Subgraph (cluster) box border style.
55///
56/// `None` is not "invisible cluster" — the box still groups its nodes
57/// for layout and its label still paints; only the border ink is
58/// suppressed.
59///
60/// ```
61/// use ascii_dag::{Graph, RenderOptions};
62/// use ascii_dag::render::engine::{SubgraphBorder, SubgraphStyle, SubgraphStyleCtx};
63///
64/// fn borderless(_ctx: SubgraphStyleCtx<'_>) -> SubgraphStyle {
65/// SubgraphStyle { border: SubgraphBorder::None, ..SubgraphStyle::default() }
66/// }
67///
68/// let mut g = Graph::new();
69/// g.add_node(1, "A");
70/// g.add_node(2, "B");
71/// g.add_edge(1, 2, None);
72/// let sg = g.add_subgraph("Group");
73/// g.put_nodes(&[1]).inside(sg).unwrap();
74///
75/// let mut options = RenderOptions::plain();
76/// options.plan.subgraph_style_fn = borderless;
77/// let text = g.compute_layout().render_string(&options);
78/// assert!(!text.contains('╔')); // no border ink
79/// assert!(text.contains("Group")); // label still there
80/// ```
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82#[non_exhaustive]
83pub enum SubgraphBorder {
84 /// Double strokes (`═ ║`) — the default (legacy).
85 #[default]
86 Double,
87 /// Light strokes (`─ │`).
88 Light,
89 /// Dashed strokes (`┈ ┊`).
90 Dashed,
91 /// No border — the cluster groups its nodes without ink (its label
92 /// still paints when it fits).
93 None,
94}
95
96impl SubgraphBorder {
97 /// The cell-arm weight for this border, `None` when invisible.
98 pub(crate) fn arm(self) -> Option<Weight> {
99 match self {
100 SubgraphBorder::Double => Some(Weight::Double),
101 SubgraphBorder::Light => Some(Weight::Light),
102 SubgraphBorder::Dashed => Some(Weight::Dashed),
103 SubgraphBorder::None => Option::None,
104 }
105 }
106}
107
108/// Subgraph label position (zigraph naming; D7).
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110#[non_exhaustive]
111pub enum LabelPosition {
112 /// One row below the top border, left-aligned (legacy).
113 #[default]
114 InsideTop,
115 /// One row above the bottom border, left-aligned.
116 InsideBottom,
117}
118
119/// Edge label placement strategy (zigraph naming).
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121#[non_exhaustive]
122pub enum LabelPlacement {
123 /// The layout-computed position (legacy).
124 #[default]
125 Auto,
126}
127
128// ── Style structs (what a style fn returns) ─────────────────────────────
129
130/// Resolved style for one edge.
131#[derive(Debug, Clone, Copy)]
132pub struct EdgeStyle {
133 /// `CellColor::DEFAULT` = engine default (palette modulo when colors
134 /// are enabled, plain otherwise).
135 pub color: CellColor,
136 /// `None` = engine default (light; dashed for back edges).
137 pub weight: Option<LineWeight>,
138 /// Marker at the logical target end (the arrowhead — legacy always
139 /// paints this).
140 pub marker_end: MarkerShape,
141 /// Marker at the logical source end (legacy never paints one, so
142 /// the default is `MarkerShape::None`; `Arrow` points back at the
143 /// source, giving double-headed edges).
144 pub marker_start: MarkerShape,
145}
146
147// Manual impl: a derived Default would give `marker_start` the enum's
148// default (`Arrow`) and paint tail arrowheads legacy never had.
149impl Default for EdgeStyle {
150 fn default() -> Self {
151 Self {
152 color: CellColor::DEFAULT,
153 weight: None,
154 marker_end: MarkerShape::Arrow,
155 marker_start: MarkerShape::None,
156 }
157 }
158}
159
160/// Resolved style for one subgraph.
161#[derive(Debug, Clone, Copy, Default)]
162pub struct SubgraphStyle {
163 /// Box border form (`Double` = legacy default; `None` = invisible).
164 pub border: SubgraphBorder,
165 /// Border color (`DEFAULT` = terminal default — legacy borders
166 /// never write color).
167 pub color: CellColor,
168 /// Label position within the box.
169 pub label_pos: LabelPosition,
170}
171
172/// Resolved style for one edge label.
173#[derive(Debug, Clone, Copy, Default)]
174pub struct EdgeLabelStyle {
175 /// `DEFAULT` = follow the edge's color (legacy colored behavior).
176 pub color: CellColor,
177 /// Placement strategy.
178 pub placement: LabelPlacement,
179}
180
181// ── Style contexts (what a style fn receives) ───────────────────────────
182
183/// Context for `edge_style_fn`.
184#[derive(Debug, Clone, Copy)]
185pub struct EdgeStyleCtx<'a> {
186 /// Index of this edge (original insertion order).
187 pub edge_index: usize,
188 /// Semantic source node id.
189 pub from_id: usize,
190 /// Semantic target node id.
191 pub to_id: usize,
192 /// The edge's label, if any.
193 pub label: Option<&'a str>,
194 /// Whether the edge is directed (arrowhead).
195 pub directed: bool,
196 /// Whether the edge was reversed during cycle breaking.
197 pub reversed: bool,
198 /// Total number of edges in the layout.
199 pub total_edges: usize,
200}
201
202/// Context for `subgraph_style_fn`.
203#[derive(Debug, Clone, Copy)]
204pub struct SubgraphStyleCtx<'a> {
205 /// The subgraph's id.
206 pub subgraph_id: usize,
207 /// The subgraph's display label.
208 pub label: &'a str,
209 /// Whether this cluster is nested inside another.
210 pub has_parent: bool,
211}
212
213/// Per-element style callbacks — plain fn pointers (`no_std`-safe).
214///
215/// Resolved once per element at plan time, never per cell. A plain
216/// `fn` rather than a closure keeps them `Copy` and usable without an
217/// allocator.
218///
219/// ```
220/// use ascii_dag::{Graph, RenderOptions};
221/// use ascii_dag::render::engine::{EdgeStyle, EdgeStyleCtx, LineWeight, MarkerShape};
222///
223/// // Dash the edges that cycle-breaking reversed; suppress tail arrows.
224/// fn style(ctx: EdgeStyleCtx<'_>) -> EdgeStyle {
225/// EdgeStyle {
226/// weight: Some(if ctx.reversed { LineWeight::Dashed } else { LineWeight::Light }),
227/// marker_end: MarkerShape::Arrow,
228/// marker_start: MarkerShape::None,
229/// ..EdgeStyle::default()
230/// }
231/// }
232///
233/// let g = Graph::from_edges(&[(1, "A"), (2, "B")], &[(1, 2)]);
234/// let mut options = RenderOptions::plain();
235/// options.plan.edge_style_fn = style;
236/// let text = g.compute_layout().render_string(&options);
237/// # #[cfg(feature = "layout-vertical")] // arrow glyph is axis-specific
238/// assert!(text.contains('↓'));
239/// ```
240pub type EdgeStyleFn = fn(EdgeStyleCtx<'_>) -> EdgeStyle;
241/// See [`EdgeStyleFn`].
242pub type SubgraphStyleFn = fn(SubgraphStyleCtx<'_>) -> SubgraphStyle;
243/// See [`EdgeStyleFn`].
244pub type EdgeLabelStyleFn = fn(EdgeStyleCtx<'_>) -> EdgeLabelStyle;
245
246/// Default edge style: engine defaults everywhere (legacy output).
247/// The arrowhead honors the IR's `directed` flag — undirected edges
248/// paint as plain lines.
249pub fn default_edge_style(ctx: EdgeStyleCtx<'_>) -> EdgeStyle {
250 EdgeStyle {
251 marker_end: if ctx.directed {
252 MarkerShape::Arrow
253 } else {
254 MarkerShape::None
255 },
256 ..EdgeStyle::default()
257 }
258}
259
260/// Default subgraph style: label inside-top, terminal default color.
261pub fn default_subgraph_style(_ctx: SubgraphStyleCtx<'_>) -> SubgraphStyle {
262 SubgraphStyle::default()
263}
264
265/// Default edge-label style: follow the edge color, auto placement.
266pub fn default_edge_label_style(_ctx: EdgeStyleCtx<'_>) -> EdgeLabelStyle {
267 EdgeLabelStyle::default()
268}