Skip to main content

gpui_graph/
style.rs

1use gpui::{App, BoxShadow, Hsla, Pixels};
2use gpui_base::{ColorTokens, TextStyleToken, Theme};
3
4use crate::layout::with_fields;
5
6/// Categorical palette for data, after AntV's. The base theme has no chart
7/// roles, so this is the one place the graph defines raw colors; replace it
8/// with [`NetworkGraph::with_palette`](crate::NetworkGraph::with_palette) to
9/// match a product theme.
10const DEFAULT_PALETTE: [u32; 10] = [
11    0x5b8ff9, 0x61ddaa, 0x65789b, 0xf6bd16, 0x7262fd, 0x78d3f8, 0x9661bc, 0xf6903d, 0x008685,
12    0xf08bb4,
13];
14
15/// Colors the graph paints with. By default they are derived from the
16/// gpui-base [`Theme`] every frame, so appearance changes apply immediately.
17#[derive(Clone, Debug)]
18#[non_exhaustive]
19pub struct GraphColors {
20    pub background: Hsla,
21    /// Tooltip and label-chip background.
22    pub surface: Hsla,
23    pub foreground: Hsla,
24    pub muted: Hsla,
25    pub border: Hsla,
26    pub edge: Hsla,
27    /// Edges touching the hovered / selected item.
28    pub edge_active: Hsla,
29    /// Ring around the selected item.
30    pub selection: Hsla,
31    /// Keyboard focus indicator, distinct from selection.
32    pub focus_ring: Hsla,
33    /// Nodes without a group when coloring by group.
34    pub ungrouped: Hsla,
35    /// Text drawn on top of node fills; the one with better contrast is used.
36    pub on_fill_light: Hsla,
37    pub on_fill_dark: Hsla,
38}
39
40impl GraphColors {
41    pub fn from_theme(cx: &App) -> Self {
42        let t = Theme::global(cx).tokens.colors;
43        Self {
44            background: t.background,
45            surface: t.surface,
46            foreground: t.foreground,
47            muted: t.muted_foreground,
48            border: t.border,
49            edge: t.muted_foreground.opacity(0.45),
50            edge_active: t.foreground.opacity(0.8),
51            selection: t.primary,
52            focus_ring: t.ring,
53            ungrouped: t.muted_foreground.opacity(0.6),
54            on_fill_light: ColorTokens::light().background,
55            on_fill_dark: ColorTokens::light().foreground,
56        }
57    }
58
59    /// Readable text color on `fill`.
60    pub fn on_fill(&self, fill: Hsla) -> Hsla {
61        if fill.l > 0.62 {
62            self.on_fill_dark
63        } else {
64            self.on_fill_light
65        }
66    }
67}
68
69with_fields!(GraphColors {
70    background: Hsla => with_background,
71    surface: Hsla => with_surface,
72    foreground: Hsla => with_foreground,
73    muted: Hsla => with_muted,
74    border: Hsla => with_border,
75    edge: Hsla => with_edge,
76    edge_active: Hsla => with_edge_active,
77    selection: Hsla => with_selection,
78    focus_ring: Hsla => with_focus_ring,
79    ungrouped: Hsla => with_ungrouped,
80});
81
82/// Type, radius and spacing scales the graph reads from the theme.
83#[derive(Clone, Debug)]
84pub(crate) struct GraphMetrics {
85    /// Node labels.
86    pub label: TextStyleToken,
87    /// Edge labels, counts.
88    pub caption: TextStyleToken,
89    pub radius_sm: Pixels,
90    pub radius_md: Pixels,
91    pub space_xs: Pixels,
92    pub space_sm: Pixels,
93    pub space_md: Pixels,
94    pub shadow: Vec<BoxShadow>,
95}
96
97impl GraphMetrics {
98    pub fn from_theme(cx: &App) -> Self {
99        let t = Theme::global(cx).tokens;
100        Self {
101            label: t.typography.sm,
102            caption: t.typography.xs,
103            radius_sm: t.radius.sm,
104            radius_md: t.radius.md,
105            space_xs: t.spacing.xs,
106            space_sm: t.spacing.sm,
107            space_md: t.spacing.md,
108            shadow: t.shadow.md,
109        }
110    }
111}
112
113/// Category / group colors, assigned in the order they first appear.
114#[derive(Clone, Debug)]
115pub struct Palette(pub Vec<Hsla>);
116
117impl Palette {
118    /// Color for the `ix`-th category / group. An empty palette yields the theme's muted color.
119    pub fn get(&self, ix: usize) -> Hsla {
120        match self.0.len() {
121            0 => ColorTokens::default().muted_foreground,
122            n => self.0[ix % n],
123        }
124    }
125}
126
127impl Default for Palette {
128    fn default() -> Self {
129        Self(
130            DEFAULT_PALETTE
131                .iter()
132                .map(|&c| gpui::rgb(c).into())
133                .collect(),
134        )
135    }
136}
137
138/// How node radius (in world units) is derived when a node has no explicit size.
139#[derive(Clone, Copy, Debug, PartialEq)]
140pub enum NodeSizing {
141    Fixed(f32),
142    /// Area grows with degree ("node importance"), clamped to `max`.
143    Degree {
144        min: f32,
145        max: f32,
146    },
147}
148
149impl Default for NodeSizing {
150    fn default() -> Self {
151        Self::Degree { min: 7., max: 28. }
152    }
153}
154
155impl NodeSizing {
156    pub fn radius(&self, degree: usize) -> f32 {
157        match *self {
158            Self::Fixed(r) => r,
159            Self::Degree { min, max } => (min + (degree as f32).sqrt() * 2.6).min(max),
160        }
161    }
162}
163
164/// Which node labels are drawn.
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
166pub enum LabelMode {
167    /// Only the hovered / selected node and its neighbors.
168    Focus,
169    /// Every node once the zoom makes labels readable; focus set otherwise.
170    #[default]
171    Auto,
172    Always,
173    Never,
174}
175
176/// What node colors encode.
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
178pub enum ColorBy {
179    /// [`Node::category`](crate::Node::category), e.g. vertex type.
180    #[default]
181    Category,
182    /// [`Node::group`](crate::Node::group), e.g. community.
183    Group,
184}