Skip to main content

railroad/nodes/
wrappers.rs

1use std::{
2    cmp,
3    collections::{self, HashMap},
4    fmt,
5};
6
7use crate::{
8    ARC_RADIUS, Empty, HDir, Node, NodeGeometry, RenderBackend, draw_group_with_geometry,
9    render_group_with_geometry, svg,
10};
11
12/// Possible targets for `Link`.
13///
14/// Maps to the HTML `target` attribute on the generated `<a>` element.
15#[derive(Debug, Default, Clone, Copy)]
16pub enum LinkTarget {
17    /// Open in a new tab (`target="_blank"`).
18    #[default]
19    Blank,
20    /// Open in the parent frame (`target="_parent"`).
21    Parent,
22    /// Open in the topmost frame (`target="_top"`).
23    Top,
24}
25
26/// Wraps another primitive, making it a clickable link to some URI.
27#[derive(Debug, Clone)]
28pub struct Link<N> {
29    inner: N,
30    uri: String,
31    target: Option<LinkTarget>,
32    attributes: HashMap<String, String>,
33}
34
35impl<N> Link<N> {
36    /// Wrap `inner` in a clickable link pointing to `uri`.
37    ///
38    /// The URI is placed in an SVG anchor attribute and is HTML-escaped before
39    /// being written into the SVG, so arbitrary strings are safe to pass.
40    ///
41    /// # Example
42    /// ```rust
43    /// use railroad::*;
44    ///
45    /// let node = Link::new(Terminal::new("docs".to_owned()), "https://example.com".to_owned());
46    /// assert!(Diagram::new(node).to_string().starts_with("<svg"));
47    /// ```
48    pub fn new(inner: N, uri: String) -> Self {
49        let mut l = Self {
50            inner,
51            uri,
52            target: None,
53            attributes: HashMap::default(),
54        };
55        l.attributes.insert("class".to_owned(), "link".to_owned());
56        l
57    }
58
59    /// Set the `target` attribute for the generated `<a>` element.
60    ///
61    /// Pass `None` to remove any previously set target.
62    pub fn set_target(&mut self, target: Option<LinkTarget>) {
63        self.target = target;
64    }
65
66    /// Access an attribute on the main SVG-element that will be drawn.
67    pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
68        self.attributes.entry(key)
69    }
70
71    /// Emit the wrapped child once, letting the outer `<a>` wrapper choose the backend.
72    fn emit_with_geometry<B: RenderBackend>(
73        &self,
74        backend: &mut B,
75        x: i64,
76        y: i64,
77        h_dir: HDir,
78        geo: &NodeGeometry,
79    ) -> fmt::Result
80    where
81        N: Node,
82    {
83        backend.push_child(&self.inner, x, y, h_dir, &geo.children[0])
84    }
85}
86
87impl<N> Node for Link<N>
88where
89    N: Node,
90{
91    fn entry_height(&self) -> i64 {
92        self.inner.entry_height()
93    }
94    fn height(&self) -> i64 {
95        self.inner.height()
96    }
97    fn width(&self) -> i64 {
98        self.inner.width()
99    }
100
101    fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
102        let mut a = svg::Element::new("a")
103            .debug("Link", x, y, self)
104            .set("xlink:href", &self.uri);
105        a = match self.target {
106            Some(LinkTarget::Blank) => a.set("target", "_blank"),
107            Some(LinkTarget::Parent) => a.set("target", "_parent"),
108            Some(LinkTarget::Top) => a.set("target", "_top"),
109            None => a,
110        };
111        a.set_all(self.attributes.iter())
112            .add(self.inner.draw(x, y, h_dir))
113    }
114
115    fn compute_geometry(&self) -> NodeGeometry {
116        let inner_geo = self.inner.compute_geometry();
117        let entry_height = inner_geo.entry_height;
118        let height = inner_geo.height;
119        let width = inner_geo.width;
120        NodeGeometry {
121            entry_height,
122            height,
123            width,
124            children: vec![inner_geo],
125        }
126    }
127
128    fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
129        let mut backend = crate::ElementBackend::default();
130        self.emit_with_geometry(&mut backend, x, y, h_dir, geo)
131            .expect("element backend is infallible");
132        let mut a = svg::Element::new("a")
133            .debug_with_geometry("Link", x, y, geo)
134            .set("xlink:href", &self.uri);
135        a = match self.target {
136            Some(LinkTarget::Blank) => a.set("target", "_blank"),
137            Some(LinkTarget::Parent) => a.set("target", "_parent"),
138            Some(LinkTarget::Top) => a.set("target", "_top"),
139            None => a,
140        };
141        let mut a = a.set_all(self.attributes.iter());
142        for child in backend.children {
143            a.push(child);
144        }
145        a
146    }
147
148    fn render_with_geometry(
149        &self,
150        out: &mut svg::Renderer<'_>,
151        x: i64,
152        y: i64,
153        h_dir: HDir,
154        geo: &NodeGeometry,
155    ) -> fmt::Result {
156        let mut a = out.start_element("a")?;
157        a.attr("xlink:href", &self.uri)?;
158        match self.target {
159            Some(LinkTarget::Blank) => a.attr("target", "_blank")?,
160            Some(LinkTarget::Parent) => a.attr("target", "_parent")?,
161            Some(LinkTarget::Top) => a.attr("target", "_top")?,
162            None => {}
163        }
164        a.attr_hashmap(&self.attributes)?;
165        crate::add_debug_attrs(&mut a, "Link", x, y, geo)?;
166        a.finish()?;
167        self.emit_with_geometry(&mut crate::RendererBackend { out }, x, y, h_dir, geo)?;
168        crate::write_debug_overlay(out, x, y, geo)?;
169        out.end_element("a")
170    }
171}
172
173/// Wraps another element to make that element logically optional.
174///
175/// Draws a separate path above, which skips the given element.
176#[derive(Debug, Clone, Default)]
177pub struct Optional<N> {
178    inner: N,
179    attributes: HashMap<String, String>,
180}
181
182impl<N> Optional<N> {
183    /// Wrap `inner` so it can be skipped via an upper bypass path.
184    ///
185    /// # Example
186    /// ```rust
187    /// use railroad::*;
188    ///
189    /// let node = Optional::new(Terminal::new("maybe".to_owned()));
190    /// assert!(Diagram::new(node).to_string().starts_with("<svg"));
191    /// ```
192    pub fn new(inner: N) -> Self {
193        let mut o = Self {
194            inner,
195            attributes: HashMap::default(),
196        };
197        o.attributes
198            .insert("class".to_owned(), "optional".to_owned());
199        o
200    }
201
202    /// Unwrap this wrapper, returning the inner node.
203    pub fn into_inner(self) -> N {
204        self.inner
205    }
206
207    /// Access an attribute on the main SVG-element that will be drawn.
208    pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
209        self.attributes.entry(key)
210    }
211
212    /// Emit the bypass arc and wrapped child once for both render backends.
213    fn emit_with_geometry<B: RenderBackend>(
214        &self,
215        backend: &mut B,
216        x: i64,
217        y: i64,
218        h_dir: HDir,
219        geo: &NodeGeometry,
220    ) -> fmt::Result
221    where
222        N: Node,
223    {
224        let inner_geo = &geo.children[0];
225        backend.push_path(
226            svg::PathData::new(h_dir)
227                .move_to(x, y + geo.entry_height)
228                .horizontal(ARC_RADIUS * 2)
229                .move_rel(-ARC_RADIUS * 2, 0)
230                .arc(ARC_RADIUS, svg::Arc::WestToNorth)
231                .vertical(cmp::min(0, -inner_geo.entry_height + ARC_RADIUS))
232                .arc(ARC_RADIUS, svg::Arc::SouthToEast)
233                .horizontal(inner_geo.width)
234                .arc(ARC_RADIUS, svg::Arc::WestToSouth)
235                .vertical(cmp::max(0, inner_geo.entry_height - ARC_RADIUS))
236                .arc(ARC_RADIUS, svg::Arc::NorthToEast)
237                .horizontal(-ARC_RADIUS * 2),
238        )?;
239        backend.push_child(
240            &self.inner,
241            x + ARC_RADIUS * 2,
242            y + geo.entry_height - inner_geo.entry_height,
243            h_dir,
244            inner_geo,
245        )
246    }
247}
248
249impl<N> Node for Optional<N>
250where
251    N: Node,
252{
253    fn entry_height(&self) -> i64 {
254        ARC_RADIUS + cmp::max(ARC_RADIUS, self.inner.entry_height())
255    }
256
257    fn height(&self) -> i64 {
258        self.entry_height() + self.inner.height_below_entry()
259    }
260
261    fn width(&self) -> i64 {
262        ARC_RADIUS * 2 + self.inner.width() + ARC_RADIUS * 2
263    }
264
265    fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
266        let i = self.inner.draw(
267            x + ARC_RADIUS * 2,
268            y + self.entry_height() - self.inner.entry_height(),
269            h_dir,
270        );
271
272        let v = svg::PathData::new(h_dir)
273            .move_to(x, y + self.entry_height())
274            .horizontal(ARC_RADIUS * 2)
275            .move_rel(-ARC_RADIUS * 2, 0)
276            .arc(ARC_RADIUS, svg::Arc::WestToNorth)
277            .vertical(cmp::min(0, -self.inner.entry_height() + ARC_RADIUS))
278            .arc(ARC_RADIUS, svg::Arc::SouthToEast)
279            .horizontal(self.inner.width())
280            .arc(ARC_RADIUS, svg::Arc::WestToSouth)
281            .vertical(cmp::max(0, self.inner.entry_height() - ARC_RADIUS))
282            .arc(ARC_RADIUS, svg::Arc::NorthToEast)
283            .horizontal(-ARC_RADIUS * 2)
284            .into_path();
285
286        svg::Element::new("g")
287            .debug("Optional", x, y, self)
288            .set_all(self.attributes.iter())
289            .add(v)
290            .add(i)
291    }
292
293    fn compute_geometry(&self) -> NodeGeometry {
294        let inner_geo = self.inner.compute_geometry();
295        let entry_height = ARC_RADIUS + cmp::max(ARC_RADIUS, inner_geo.entry_height);
296        let height = entry_height + inner_geo.height_below_entry();
297        let width = ARC_RADIUS * 2 + inner_geo.width + ARC_RADIUS * 2;
298        NodeGeometry {
299            entry_height,
300            height,
301            width,
302            children: vec![inner_geo],
303        }
304    }
305
306    fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
307        draw_group_with_geometry(&self.attributes, "Optional", x, y, geo, |backend| {
308            self.emit_with_geometry(backend, x, y, h_dir, geo)
309        })
310    }
311
312    fn render_with_geometry(
313        &self,
314        out: &mut svg::Renderer<'_>,
315        x: i64,
316        y: i64,
317        h_dir: HDir,
318        geo: &NodeGeometry,
319    ) -> fmt::Result {
320        render_group_with_geometry(out, &self.attributes, "Optional", x, y, geo, |backend| {
321            self.emit_with_geometry(backend, x, y, h_dir, geo)
322        })
323    }
324}
325
326/// Wraps one element by providing a backwards-path through another element.
327///
328/// The main path flows through `inner` left-to-right. A return arc curves below
329/// and carries the path through `repeat` right-to-left, allowing the sequence to
330/// be traversed multiple times. Use [`Empty`] for `repeat` when no label or
331/// content is needed on the return path.
332#[derive(Debug, Clone)]
333pub struct Repeat<I, R> {
334    inner: I,
335    repeat: R,
336    spacing: i64,
337    attributes: HashMap<String, String>,
338}
339
340impl<I, R> Repeat<I, R> {
341    /// Create a `Repeat` that loops `inner` via the `repeat` node on the return path.
342    ///
343    /// # Example
344    /// ```rust
345    /// use railroad::*;
346    ///
347    /// // Zero-or-more repetitions with no label on the back-arc
348    /// let r = Repeat::new(Terminal::new("item".to_owned()), Empty);
349    /// assert!(Diagram::new(r).to_string().starts_with("<svg"));
350    /// ```
351    pub fn new(inner: I, repeat: R) -> Self {
352        let mut r = Self {
353            inner,
354            repeat,
355            spacing: 10,
356            attributes: HashMap::default(),
357        };
358        r.attributes.insert("class".to_owned(), "repeat".to_owned());
359        r
360    }
361
362    /// Access an attribute on the main SVG-element that will be drawn.
363    pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
364        self.attributes.entry(key)
365    }
366}
367
368impl<I, R> Repeat<I, R>
369where
370    I: Node,
371    R: Node,
372{
373    fn height_between_entries(&self) -> i64 {
374        cmp::max(
375            ARC_RADIUS * 2,
376            self.inner.height_below_entry() + self.spacing + self.repeat.entry_height(),
377        )
378    }
379
380    /// Emit the forward path, repeat arm, and inner branch through the shared backend.
381    fn emit_with_geometry<B: RenderBackend>(
382        &self,
383        backend: &mut B,
384        x: i64,
385        y: i64,
386        h_dir: HDir,
387        geo: &NodeGeometry,
388    ) -> fmt::Result {
389        let inner_geo = &geo.children[0];
390        let repeat_geo = &geo.children[1];
391        let height_between = cmp::max(
392            ARC_RADIUS * 2,
393            inner_geo.height_below_entry() + self.spacing + repeat_geo.entry_height,
394        );
395
396        backend.push_path(
397            svg::PathData::new(h_dir)
398                .move_to(x, y + geo.entry_height)
399                .horizontal(ARC_RADIUS)
400                .move_rel(inner_geo.width, 0)
401                .horizontal(cmp::max(
402                    ARC_RADIUS,
403                    repeat_geo.width - inner_geo.width + ARC_RADIUS,
404                ))
405                .move_rel(-ARC_RADIUS, 0)
406                .arc(ARC_RADIUS, svg::Arc::WestToSouth)
407                .vertical(height_between - ARC_RADIUS * 2)
408                .arc(ARC_RADIUS, svg::Arc::NorthToWest)
409                .move_rel(-repeat_geo.width, 0)
410                .horizontal(cmp::min(0, repeat_geo.width - inner_geo.width))
411                .arc(ARC_RADIUS, svg::Arc::EastToNorth)
412                .vertical(-height_between + ARC_RADIUS * 2)
413                .arc(ARC_RADIUS, svg::Arc::SouthToEast),
414        )?;
415        backend.push_child(
416            &self.repeat,
417            x + geo.width - repeat_geo.width - ARC_RADIUS,
418            y + geo.height - repeat_geo.height_below_entry() - repeat_geo.entry_height,
419            h_dir.invert(),
420            repeat_geo,
421        )?;
422        backend.push_child(&self.inner, x + ARC_RADIUS, y, h_dir, inner_geo)
423    }
424}
425
426impl<I, R> Default for Repeat<I, R>
427where
428    I: Default,
429    R: Default,
430{
431    fn default() -> Self {
432        Self {
433            inner: Default::default(),
434            repeat: Default::default(),
435            spacing: 10,
436            attributes: HashMap::default(),
437        }
438    }
439}
440
441impl<I, R> Node for Repeat<I, R>
442where
443    I: Node,
444    R: Node,
445{
446    fn entry_height(&self) -> i64 {
447        self.inner.entry_height()
448    }
449
450    fn height(&self) -> i64 {
451        self.inner.entry_height() + self.height_between_entries() + self.repeat.height_below_entry()
452    }
453
454    fn width(&self) -> i64 {
455        ARC_RADIUS + cmp::max(self.repeat.width(), self.inner.width()) + ARC_RADIUS
456    }
457
458    fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
459        let mut g = svg::Element::new("g").set_all(self.attributes.iter());
460
461        g.push(
462            svg::PathData::new(h_dir)
463                .move_to(x, y + self.entry_height())
464                .horizontal(ARC_RADIUS)
465                .move_rel(self.inner.width(), 0)
466                .horizontal(cmp::max(
467                    ARC_RADIUS,
468                    self.repeat.width() - self.inner.width() + ARC_RADIUS,
469                ))
470                .move_rel(-ARC_RADIUS, 0)
471                .arc(ARC_RADIUS, svg::Arc::WestToSouth)
472                .vertical(self.height_between_entries() - ARC_RADIUS * 2)
473                .arc(ARC_RADIUS, svg::Arc::NorthToWest)
474                .move_rel(-self.repeat.width(), 0)
475                .horizontal(cmp::min(0, self.repeat.width() - self.inner.width()))
476                .arc(ARC_RADIUS, svg::Arc::EastToNorth)
477                .vertical(-self.height_between_entries() + ARC_RADIUS * 2)
478                .arc(ARC_RADIUS, svg::Arc::SouthToEast)
479                .into_path(),
480        )
481        .push(self.repeat.draw(
482            x + self.width() - self.repeat.width() - ARC_RADIUS,
483            y + self.height() - self.repeat.height_below_entry() - self.repeat.entry_height(),
484            h_dir.invert(),
485        ));
486        g.push(self.inner.draw(x + ARC_RADIUS, y, h_dir));
487        g.debug("Repeat", x, y, self)
488    }
489
490    fn compute_geometry(&self) -> NodeGeometry {
491        let inner_geo = self.inner.compute_geometry();
492        let repeat_geo = self.repeat.compute_geometry();
493        let height_between = cmp::max(
494            ARC_RADIUS * 2,
495            inner_geo.height_below_entry() + self.spacing + repeat_geo.entry_height,
496        );
497        let entry_height = inner_geo.entry_height;
498        let height = inner_geo.entry_height + height_between + repeat_geo.height_below_entry();
499        let width = ARC_RADIUS + cmp::max(repeat_geo.width, inner_geo.width) + ARC_RADIUS;
500        NodeGeometry {
501            entry_height,
502            height,
503            width,
504            children: vec![inner_geo, repeat_geo],
505        }
506    }
507
508    fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
509        draw_group_with_geometry(&self.attributes, "Repeat", x, y, geo, |backend| {
510            self.emit_with_geometry(backend, x, y, h_dir, geo)
511        })
512    }
513
514    fn render_with_geometry(
515        &self,
516        out: &mut svg::Renderer<'_>,
517        x: i64,
518        y: i64,
519        h_dir: HDir,
520        geo: &NodeGeometry,
521    ) -> fmt::Result {
522        render_group_with_geometry(out, &self.attributes, "Repeat", x, y, geo, |backend| {
523            self.emit_with_geometry(backend, x, y, h_dir, geo)
524        })
525    }
526}
527
528/// A box drawn around the given element and a label placed inside the box, above the element.
529///
530/// You may want to use `crate::Comment` or `Empty` for the label.
531#[derive(Debug, Clone)]
532pub struct LabeledBox<T, U> {
533    inner: T,
534    label: U,
535    spacing: i64,
536    padding: i64,
537    attributes: HashMap<String, String>,
538}
539
540impl<T> LabeledBox<T, Empty> {
541    /// Construct a `LabeledBox` around `inner` with no label.
542    ///
543    /// This is a convenience shorthand for `LabeledBox::new(inner, Empty)`.
544    pub fn without_label(inner: T) -> Self {
545        Self::new(inner, Empty)
546    }
547}
548
549impl<T, U> LabeledBox<T, U> {
550    /// Construct a `LabeledBox` that draws a border around `inner` and places
551    /// `label` above it inside the box.
552    ///
553    /// # Example
554    /// ```rust
555    /// use railroad::*;
556    ///
557    /// let labeled = LabeledBox::new(
558    ///     Terminal::new("item".to_owned()),
559    ///     Comment::new("group".to_owned()),
560    /// );
561    /// assert!(Diagram::new(labeled).to_string().starts_with("<svg"));
562    /// ```
563    pub fn new(inner: T, label: U) -> Self {
564        let mut l = Self {
565            inner,
566            label,
567            spacing: 8,
568            padding: 8,
569            attributes: HashMap::default(),
570        };
571        l.attributes
572            .insert("class".to_owned(), "labeledbox".to_owned());
573        l
574    }
575
576    /// Access an attribute on the main SVG-element that will be drawn.
577    pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
578        self.attributes.entry(key)
579    }
580}
581
582impl<T, U> Default for LabeledBox<T, U>
583where
584    T: Default,
585    U: Default,
586{
587    fn default() -> Self {
588        Self {
589            inner: Default::default(),
590            label: Default::default(),
591            spacing: 8,
592            padding: 8,
593            attributes: HashMap::default(),
594        }
595    }
596}
597
598impl<T, U> LabeledBox<T, U>
599where
600    T: Node,
601    U: Node,
602{
603    fn spacing(&self) -> i64 {
604        if self.label.height() > 0 {
605            self.spacing
606        } else {
607            0
608        }
609    }
610
611    fn padding(&self) -> i64 {
612        if self.label.height() + self.inner.height() + self.label.width() + self.inner.width() > 0 {
613            self.padding
614        } else {
615            0
616        }
617    }
618
619    /// Emit the box frame, label, and inner node through the shared backend.
620    fn emit_with_geometry<B: RenderBackend>(
621        &self,
622        backend: &mut B,
623        x: i64,
624        y: i64,
625        h_dir: HDir,
626        geo: &NodeGeometry,
627    ) -> fmt::Result {
628        let inner_geo = &geo.children[0];
629        let label_geo = &geo.children[1];
630        let padding = if label_geo.height + inner_geo.height + label_geo.width + inner_geo.width > 0
631        {
632            self.padding
633        } else {
634            0
635        };
636        let spacing = if label_geo.height > 0 {
637            self.spacing
638        } else {
639            0
640        };
641
642        backend.push_rect(x, y, geo.width, geo.height)?;
643        backend.push_path(
644            svg::PathData::new(h_dir)
645                .move_to(x, y + geo.entry_height)
646                .horizontal(padding)
647                .move_rel(inner_geo.width, 0)
648                .horizontal(geo.width - inner_geo.width - padding),
649        )?;
650        backend.push_child(&self.label, x + padding, y + padding, h_dir, label_geo)?;
651        backend.push_child(
652            &self.inner,
653            x + padding,
654            y + padding + label_geo.height + spacing,
655            h_dir,
656            inner_geo,
657        )
658    }
659}
660
661impl<T, U> Node for LabeledBox<T, U>
662where
663    T: Node,
664    U: Node,
665{
666    fn entry_height(&self) -> i64 {
667        self.padding() + self.label.height() + self.spacing() + self.inner.entry_height()
668    }
669
670    fn height(&self) -> i64 {
671        self.padding() + self.label.height() + self.spacing() + self.inner.height() + self.padding()
672    }
673
674    fn width(&self) -> i64 {
675        self.padding() + cmp::max(self.inner.width(), self.label.width()) + self.padding()
676    }
677
678    fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
679        svg::Element::new("g")
680            .add(
681                svg::Element::new("rect")
682                    .set("x", &x)
683                    .set("y", &y)
684                    .set("height", &self.height())
685                    .set("width", &self.width()),
686            )
687            .add(
688                svg::PathData::new(h_dir)
689                    .move_to(x, y + self.entry_height())
690                    .horizontal(self.padding())
691                    .move_rel(self.inner.width(), 0)
692                    .horizontal(self.width() - self.inner.width() - self.padding())
693                    .into_path(),
694            )
695            .add(
696                self.label
697                    .draw(x + self.padding(), y + self.padding(), h_dir),
698            )
699            .add(self.inner.draw(
700                x + self.padding(),
701                y + self.padding() + self.label.height() + self.spacing(),
702                h_dir,
703            ))
704            .set_all(self.attributes.iter())
705            .debug("LabeledBox", x, y, self)
706    }
707
708    fn compute_geometry(&self) -> NodeGeometry {
709        let inner_geo = self.inner.compute_geometry();
710        let label_geo = self.label.compute_geometry();
711        let padding = if label_geo.height + inner_geo.height + label_geo.width + inner_geo.width > 0
712        {
713            self.padding
714        } else {
715            0
716        };
717        let spacing = if label_geo.height > 0 {
718            self.spacing
719        } else {
720            0
721        };
722        let entry_height = padding + label_geo.height + spacing + inner_geo.entry_height;
723        let height = padding + label_geo.height + spacing + inner_geo.height + padding;
724        let width = padding + cmp::max(inner_geo.width, label_geo.width) + padding;
725        NodeGeometry {
726            entry_height,
727            height,
728            width,
729            children: vec![inner_geo, label_geo],
730        }
731    }
732
733    fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
734        draw_group_with_geometry(&self.attributes, "LabeledBox", x, y, geo, |backend| {
735            self.emit_with_geometry(backend, x, y, h_dir, geo)
736        })
737    }
738
739    fn render_with_geometry(
740        &self,
741        out: &mut svg::Renderer<'_>,
742        x: i64,
743        y: i64,
744        h_dir: HDir,
745        geo: &NodeGeometry,
746    ) -> fmt::Result {
747        render_group_with_geometry(out, &self.attributes, "LabeledBox", x, y, geo, |backend| {
748            self.emit_with_geometry(backend, x, y, h_dir, geo)
749        })
750    }
751}