Skip to main content

demo_common/
content.rs

1//! Themed node-interior helpers for the demos.
2//!
3//! [`NodeContentStyle`] derives title/body colors from the active iced theme so
4//! demo nodes look consistent across every built-in theme, and
5//! [`node_title_bar`] renders a node's header in those colors. These are demo
6//! conveniences, not part of the `iced_nodegraph` widget: the widget renders
7//! the node fill/border/pins; the app decides what goes inside. The title bar
8//! is built with the library's [`node_header`](iced_nodegraph::node_header) so
9//! its rounded top corners match the rendered node silhouette.
10
11use iced::{
12    Color, Padding, Theme,
13    widget::{Container, container, text},
14};
15use iced_nodegraph::node_header;
16
17/// Default node corner radius. Kept in sync with the widget's `default_node_style`
18/// so a [`node_title_bar`] lines up with the rendered fill (asserted in tests).
19const DEFAULT_CORNER_RADIUS: f32 = 5.0;
20/// Default node border width. Kept in sync with `default_node_style`.
21const DEFAULT_BORDER_WIDTH: f32 = 1.0;
22
23/// Theme-derived color palette for a node's interior.
24///
25/// Provides color palettes derived from the current theme for consistent
26/// node interior styling, plus geometry values for title bars and content containers.
27#[derive(Debug, Clone)]
28pub struct NodeContentStyle {
29    /// Background color for the title bar area
30    pub title_background: Color,
31    /// Text color for the title
32    pub title_text: Color,
33    /// Background color for the node body
34    pub body_background: Color,
35    /// Text color for body content
36    pub body_text: Color,
37    /// Accent color for highlights and decorations
38    pub accent: Color,
39    /// Corner radius of the node (for title bar and content container)
40    pub corner_radius: f32,
41    /// Border width of the node (for inset calculations)
42    pub border_width: f32,
43}
44
45impl NodeContentStyle {
46    /// Sets the corner radius for this style.
47    pub fn with_geometry(mut self, corner_radius: f32, border_width: f32) -> Self {
48        self.corner_radius = corner_radius;
49        self.border_width = border_width;
50        self
51    }
52
53    /// Creates a themed node content style from an accent color.
54    ///
55    /// Dark themes tint the title background by darkening the accent color.
56    /// Light themes tint it by lightening towards white.
57    fn from_accent(accent: Color, theme: &Theme) -> Self {
58        const DARK_TINT: f32 = 0.35;
59        const LIGHT_TINT: f32 = 0.15;
60
61        let palette = theme.extended_palette();
62        let title_background = if palette.is_dark {
63            Color::from_rgba(
64                accent.r * DARK_TINT,
65                accent.g * DARK_TINT,
66                accent.b * DARK_TINT,
67                0.9,
68            )
69        } else {
70            Color::from_rgba(
71                1.0 - (1.0 - accent.r) * LIGHT_TINT,
72                1.0 - (1.0 - accent.g) * LIGHT_TINT,
73                1.0 - (1.0 - accent.b) * LIGHT_TINT,
74                0.9,
75            )
76        };
77
78        Self {
79            title_background,
80            title_text: palette.background.base.text,
81            body_background: Color::TRANSPARENT,
82            body_text: palette.background.base.text,
83            accent,
84            corner_radius: DEFAULT_CORNER_RADIUS,
85            border_width: DEFAULT_BORDER_WIDTH,
86        }
87    }
88
89    /// Creates an input node style derived from theme's primary color.
90    pub fn input(theme: &Theme) -> Self {
91        Self::from_accent(theme.extended_palette().primary.base.color, theme)
92    }
93
94    /// Creates a process node style derived from theme's success color.
95    pub fn process(theme: &Theme) -> Self {
96        Self::from_accent(theme.extended_palette().success.base.color, theme)
97    }
98
99    /// Creates an output node style derived from theme's secondary color.
100    pub fn output(theme: &Theme) -> Self {
101        Self::from_accent(theme.extended_palette().secondary.base.color, theme)
102    }
103
104    /// Creates a comment node style from theme's background weak color.
105    pub fn comment(theme: &Theme) -> Self {
106        let palette = theme.extended_palette();
107        let weak = palette.background.weak.color;
108        let weak_text = palette.background.weak.text;
109
110        Self {
111            title_background: Color::from_rgba(weak.r, weak.g, weak.b, 0.7),
112            title_text: weak_text,
113            body_background: Color::TRANSPARENT,
114            body_text: Color::from_rgba(weak_text.r, weak_text.g, weak_text.b, 0.8),
115            accent: weak,
116            corner_radius: DEFAULT_CORNER_RADIUS,
117            border_width: DEFAULT_BORDER_WIDTH,
118        }
119    }
120
121    /// Creates a custom style with the specified accent color.
122    pub fn custom(theme: &Theme, accent: Color) -> Self {
123        let palette = theme.extended_palette();
124        let is_dark = palette.is_dark;
125
126        if is_dark {
127            Self {
128                title_background: Color::from_rgba(
129                    accent.r * 0.4,
130                    accent.g * 0.4,
131                    accent.b * 0.4,
132                    0.9,
133                ),
134                title_text: palette.background.base.text,
135                body_background: Color::TRANSPARENT,
136                body_text: palette.background.base.text,
137                accent,
138                corner_radius: DEFAULT_CORNER_RADIUS,
139                border_width: DEFAULT_BORDER_WIDTH,
140            }
141        } else {
142            Self {
143                title_background: Color::from_rgba(
144                    1.0 - (1.0 - accent.r) * 0.3,
145                    1.0 - (1.0 - accent.g) * 0.3,
146                    1.0 - (1.0 - accent.b) * 0.3,
147                    0.9,
148                ),
149                title_text: palette.background.base.text,
150                body_background: Color::TRANSPARENT,
151                body_text: palette.background.base.text,
152                accent,
153                corner_radius: DEFAULT_CORNER_RADIUS,
154                border_width: DEFAULT_BORDER_WIDTH,
155            }
156        }
157    }
158}
159
160/// Creates a themed title bar container for nodes.
161///
162/// Built on the library's [`node_header`], so its rounded top corners match the
163/// silhouette the widget renders behind it.
164pub fn node_title_bar<'a, Message>(
165    title: impl Into<String>,
166    style: NodeContentStyle,
167) -> Container<'a, Message, Theme, iced::Renderer>
168where
169    Message: Clone + 'a,
170{
171    let title_text = text(title.into()).size(13).color(style.title_text);
172
173    node_header(
174        container(title_text).padding(Padding {
175            top: 4.0,
176            bottom: 4.0,
177            left: 8.0,
178            right: 8.0,
179        }),
180        style.title_background,
181        style.corner_radius,
182    )
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use iced_nodegraph::{NodeStatus, default_node_style};
189
190    /// The default `NodeContentStyle` geometry must match the node the widget
191    /// actually renders: the fill uses `NodeStyle::corner_radius`, and the
192    /// content style defaults to the same value, so header and fill line up flush.
193    #[test]
194    fn default_content_matches_rendered_node() {
195        let theme = Theme::Dark;
196        let fill = default_node_style(&theme, NodeStatus::Idle).corner_radius;
197        let content = NodeContentStyle::input(&theme).corner_radius;
198        assert_eq!(
199            content, fill,
200            "NodeContentStyle default radius must equal the rendered fill radius"
201        );
202    }
203}