1use crate::gpui_compat::PixelsExt;
2use gpui::{
3 App, Background, BorderStyle, Bounds, Component, Corners, Edges, Hsla, IntoElement, Pixels,
4 RenderOnce, Window, point, prelude::*, px, quad, size,
5};
6use liora_core::Config;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum SignalMeterKind {
10 #[default]
11 Mobile,
12 Wifi,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct SignalLevelColor {
17 pub level: usize,
18 pub color: Hsla,
19}
20
21impl SignalLevelColor {
22 pub fn new(level: usize, color: Hsla) -> Self {
23 Self { level, color }
24 }
25}
26
27#[derive(Clone)]
28pub struct SignalMeter {
29 level: usize,
30 max_level: usize,
31 kind: SignalMeterKind,
32 active_color: Option<Hsla>,
33 inactive_color: Option<Hsla>,
34 level_colors: Vec<Hsla>,
35 threshold_colors: Vec<SignalLevelColor>,
36 bar_width: Pixels,
37 gap: Pixels,
38 height: Pixels,
39}
40
41impl SignalMeter {
42 pub fn new(level: usize) -> Self {
43 Self {
44 level,
45 max_level: 4,
46 kind: SignalMeterKind::Mobile,
47 active_color: None,
48 inactive_color: None,
49 level_colors: Vec::new(),
50 threshold_colors: Vec::new(),
51 bar_width: px(6.0),
52 gap: px(4.0),
53 height: px(32.0),
54 }
55 }
56 pub fn max_level(mut self, max_level: usize) -> Self {
57 self.max_level = max_level.max(1);
58 self.level = self.level.min(self.max_level);
59 self
60 }
61
62 pub fn total_signals(self, total: usize) -> Self {
63 self.max_level(total)
64 }
65
66 pub fn signal_count(self, count: usize) -> Self {
67 self.max_level(count)
68 }
69 pub fn wifi(mut self) -> Self {
70 self.kind = SignalMeterKind::Wifi;
71 self
72 }
73 pub fn mobile(mut self) -> Self {
74 self.kind = SignalMeterKind::Mobile;
75 self
76 }
77 pub fn active_color(mut self, color: Hsla) -> Self {
78 self.active_color = Some(color);
79 self
80 }
81 pub fn inactive_color(mut self, color: Hsla) -> Self {
82 self.inactive_color = Some(color);
83 self
84 }
85
86 pub fn level_colors(mut self, colors: impl IntoIterator<Item = Hsla>) -> Self {
87 self.level_colors = colors.into_iter().collect();
88 self
89 }
90
91 pub fn signal_colors(self, colors: impl IntoIterator<Item = Hsla>) -> Self {
92 self.level_colors(colors)
93 }
94
95 pub fn threshold_colors(mut self, colors: impl IntoIterator<Item = SignalLevelColor>) -> Self {
96 self.threshold_colors = colors.into_iter().collect();
97 self.threshold_colors
98 .sort_by_key(|threshold| threshold.level);
99 self
100 }
101
102 pub fn level_threshold_colors(
103 self,
104 colors: impl IntoIterator<Item = SignalLevelColor>,
105 ) -> Self {
106 self.threshold_colors(colors)
107 }
108
109 pub fn level_color(mut self, level: usize, color: Hsla) -> Self {
110 self.threshold_colors
111 .push(SignalLevelColor::new(level, color));
112 self.threshold_colors
113 .sort_by_key(|threshold| threshold.level);
114 self
115 }
116 pub fn bar_width(mut self, width: impl Into<Pixels>) -> Self {
117 self.bar_width = width.into().max(px(2.0));
118 self
119 }
120 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
121 self.gap = gap.into().max(px(0.0));
122 self
123 }
124 pub fn height(mut self, height: impl Into<Pixels>) -> Self {
125 self.height = height.into().max(px(12.0));
126 self
127 }
128}
129
130impl RenderOnce for SignalMeter {
131 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
132 let theme = cx.global::<Config>().theme.clone();
133 let active = self.active_color.unwrap_or(theme.success.base);
134 let inactive = self
135 .inactive_color
136 .unwrap_or(theme.neutral.border.opacity(0.55));
137 let total_width = self.bar_width * self.max_level as f32
138 + self.gap * self.max_level.saturating_sub(1) as f32;
139 let max_level = self.max_level;
140 let level = self.level.min(max_level);
141 let kind = self.kind;
142 let bar_width = self.bar_width;
143 let gap = self.gap;
144 let height = self.height;
145 let level_colors = self.level_colors.clone();
146 let threshold_color = self
147 .threshold_colors
148 .iter()
149 .filter(|threshold| level >= threshold.level)
150 .map(|threshold| threshold.color)
151 .last();
152 gpui::canvas(
153 |_, _, _| (),
154 move |bounds, _, window, _| {
155 for index in 0..max_level {
156 let ratio = (index + 1) as f32 / max_level as f32;
157 let bar_h = match kind {
158 SignalMeterKind::Mobile => height.as_f32() * (0.28 + ratio * 0.72),
159 SignalMeterKind::Wifi => height.as_f32() * ratio,
160 };
161 let x = bounds.left() + (bar_width + gap) * index as f32;
162 let y = bounds.bottom() - px(bar_h);
163 let color = if index < level {
164 threshold_color
165 .or_else(|| level_colors.get(index).copied())
166 .unwrap_or(active)
167 } else {
168 inactive
169 };
170 let rect = Bounds::new(point(x, y), size(bar_width, px(bar_h)));
171 window.paint_quad(quad(
172 rect,
173 Corners::all(bar_width / 2.0).clamp_radii_for_quad_size(rect.size),
174 Background::from(color),
175 Edges::all(px(0.0)),
176 gpui::transparent_black(),
177 BorderStyle::Solid,
178 ));
179 }
180 },
181 )
182 .w(total_width)
183 .h(self.height)
184 }
185}
186
187impl IntoElement for SignalMeter {
188 type Element = Component<Self>;
189 fn into_element(self) -> Self::Element {
190 Component::new(self)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 #[test]
198 fn signal_meter_clamps_levels() {
199 let meter = SignalMeter::new(9)
200 .max_level(5)
201 .total_signals(6)
202 .wifi()
203 .bar_width(px(8.0))
204 .gap(px(3.0))
205 .height(px(24.0))
206 .level_colors([gpui::red(), gpui::yellow(), gpui::green()])
207 .threshold_colors([
208 SignalLevelColor::new(2, gpui::red()),
209 SignalLevelColor::new(3, gpui::yellow()),
210 SignalLevelColor::new(5, gpui::green()),
211 ]);
212 assert_eq!(meter.level, 5);
213 assert_eq!(meter.max_level, 6);
214 assert_eq!(meter.kind, SignalMeterKind::Wifi);
215 assert_eq!(meter.bar_width, px(8.0));
216 assert_eq!(meter.level_colors.len(), 3);
217 assert_eq!(meter.threshold_colors.len(), 3);
218 }
219
220 #[test]
221 fn signal_meter_threshold_colors_sort_by_level() {
222 let meter = SignalMeter::new(4)
223 .total_signals(5)
224 .level_color(5, gpui::green())
225 .level_color(2, gpui::red())
226 .level_color(3, gpui::yellow());
227 let levels = meter
228 .threshold_colors
229 .iter()
230 .map(|threshold| threshold.level)
231 .collect::<Vec<_>>();
232 assert_eq!(levels, vec![2, 3, 5]);
233 }
234}