Skip to main content

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}