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 { self.on_fill_dark } else { self.on_fill_light }
62    }
63}
64
65with_fields!(GraphColors {
66    background: Hsla => with_background,
67    surface: Hsla => with_surface,
68    foreground: Hsla => with_foreground,
69    muted: Hsla => with_muted,
70    border: Hsla => with_border,
71    edge: Hsla => with_edge,
72    edge_active: Hsla => with_edge_active,
73    selection: Hsla => with_selection,
74    focus_ring: Hsla => with_focus_ring,
75    ungrouped: Hsla => with_ungrouped,
76});
77
78/// Type, radius and spacing scales the graph reads from the theme.
79#[derive(Clone, Debug)]
80pub(crate) struct GraphMetrics {
81    /// Node labels.
82    pub label: TextStyleToken,
83    /// Edge labels, counts.
84    pub caption: TextStyleToken,
85    pub radius_sm: Pixels,
86    pub radius_md: Pixels,
87    pub space_xs: Pixels,
88    pub space_sm: Pixels,
89    pub space_md: Pixels,
90    pub shadow: Vec<BoxShadow>,
91}
92
93impl GraphMetrics {
94    pub fn from_theme(cx: &App) -> Self {
95        let t = Theme::global(cx).tokens;
96        Self {
97            label: t.typography.sm,
98            caption: t.typography.xs,
99            radius_sm: t.radius.sm,
100            radius_md: t.radius.md,
101            space_xs: t.spacing.xs,
102            space_sm: t.spacing.sm,
103            space_md: t.spacing.md,
104            shadow: t.shadow.md,
105        }
106    }
107}
108
109/// Category / group colors, assigned in the order they first appear.
110#[derive(Clone, Debug)]
111pub struct Palette(pub Vec<Hsla>);
112
113impl Palette {
114    /// Color for the `ix`-th category / group. An empty palette yields the theme's muted color.
115    pub fn get(&self, ix: usize) -> Hsla {
116        match self.0.len() {
117            0 => ColorTokens::default().muted_foreground,
118            n => self.0[ix % n],
119        }
120    }
121}
122
123impl Default for Palette {
124    fn default() -> Self {
125        Self(DEFAULT_PALETTE.iter().map(|&c| gpui::rgb(c).into()).collect())
126    }
127}
128
129/// How node radius (in world units) is derived when a node has no explicit size.
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub enum NodeSizing {
132    Fixed(f32),
133    /// Area grows with degree ("node importance"), clamped to `max`.
134    Degree { min: f32, max: f32 },
135}
136
137impl Default for NodeSizing {
138    fn default() -> Self {
139        Self::Degree { min: 7., max: 28. }
140    }
141}
142
143impl NodeSizing {
144    pub fn radius(&self, degree: usize) -> f32 {
145        match *self {
146            Self::Fixed(r) => r,
147            Self::Degree { min, max } => (min + (degree as f32).sqrt() * 2.6).min(max),
148        }
149    }
150}
151
152/// Which node labels are drawn.
153#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
154pub enum LabelMode {
155    /// Only the hovered / selected node and its neighbors.
156    Focus,
157    /// Every node once the zoom makes labels readable; focus set otherwise.
158    #[default]
159    Auto,
160    Always,
161    Never,
162}
163
164/// What node colors encode.
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
166pub enum ColorBy {
167    /// [`Node::category`](crate::Node::category), e.g. vertex type.
168    #[default]
169    Category,
170    /// [`Node::group`](crate::Node::group), e.g. community.
171    Group,
172}