Skip to main content
RKTK API Docs RKTK Home Repo

rktk/task/display/
color_bar.rs

1use embassy_futures::select::{Either3, select3};
2use embassy_time::{Duration, Ticker};
3use embedded_graphics::{
4    mono_font::{MonoTextStyleBuilder, ascii::{FONT_6X10, FONT_8X13, FONT_9X15}},
5    pixelcolor::{Rgb565, Rgb888},
6    prelude::*,
7    primitives::{Rectangle, RoundedRectangle, CornerRadii, PrimitiveStyleBuilder, Circle, Line},
8    text::{Baseline, Text},
9};
10
11use crate::{
12    config::CONST_CONFIG,
13    drivers::interface::{display::DisplayDriver, reporter::Output},
14    utils::{Channel, Signal},
15};
16
17use super::{DisplayConfig, DisplayMessage};
18
19// 32-element sine lookup table scaled to 100 to avoid float operations
20const SIN_TABLE: [i32; 32] = [
21    0, 19, 38, 55, 70, 83, 92, 98,
22    100, 98, 92, 83, 70, 55, 38, 19,
23    0, -19, -38, -55, -70, -83, -92, -98,
24    -100, -98, -92, -83, -70, -55, -38, -19
25];
26
27#[inline]
28fn get_sin(idx: u32) -> i32 {
29    SIN_TABLE[(idx % 32) as usize]
30}
31
32#[inline]
33fn rgb(r: u8, g: u8, b: u8) -> Rgb565 {
34    Rgb565::from(Rgb888::new(r, g, b))
35}
36
37fn draw_centered_text<D: DrawTarget<Color = Rgb565>>(
38    target: &mut D,
39    text: &str,
40    font: &embedded_graphics::mono_font::MonoFont,
41    rect: Rectangle,
42    text_color: Rgb565,
43) -> Result<(), D::Error> {
44    let text_width = text.len() as i32 * font.character_size.width as i32;
45    let text_height = font.character_size.height as i32;
46    let x = rect.top_left.x + (rect.size.width as i32 - text_width) / 2;
47    let y = rect.top_left.y + (rect.size.height as i32 - text_height) / 2;
48    
49    let text_style = MonoTextStyleBuilder::new()
50        .font(font)
51        .text_color(text_color)
52        .build();
53        
54    Text::with_baseline(text, Point::new(x, y), text_style, Baseline::Top).draw(target)?;
55    Ok(())
56}
57
58async fn draw_dashboard<D: DisplayDriver<Color = Rgb565>>(
59    display: &mut D,
60    layer_state: &[bool],
61    caps_lock: bool,
62    num_lock: bool,
63    output_mode: Output,
64    mouse_available: bool,
65    anim_tick: u32,
66) {
67    let target = display.draw_target();
68
69    // 1. Draw Background (Synthwave Dark Slate)
70    let bg_style = PrimitiveStyleBuilder::new()
71        .fill_color(rgb(10, 10, 15))
72        .build();
73    let _ = Rectangle::new(Point::zero(), Size::new(284, 76)).into_styled(bg_style).draw(target);
74
75    // Outer framing styling
76    let panel_border_style = PrimitiveStyleBuilder::new()
77        .stroke_color(rgb(0, 180, 216))
78        .stroke_width(1)
79        .build();
80
81    // 2. Left Panel: X = 6, Y = 6, W = 66, H = 64
82    let _ = RoundedRectangle::new(
83        Rectangle::new(Point::new(6, 6), Size::new(66, 64)),
84        CornerRadii::new(Size::new(6, 6)),
85    )
86    .into_styled(panel_border_style)
87    .draw(target);
88
89    // Left Panel - RKTK branding
90    let brand_style = MonoTextStyleBuilder::new()
91        .font(&FONT_9X15)
92        .text_color(rgb(0, 180, 216))
93        .build();
94    let _ = Text::with_baseline("RKTK", Point::new(18, 12), brand_style, Baseline::Top).draw(target);
95
96    // Divider line under branding
97    let line_style = PrimitiveStyleBuilder::new()
98        .stroke_color(rgb(0, 70, 90))
99        .stroke_width(1)
100        .build();
101    let _ = Line::new(Point::new(14, 29), Point::new(64, 29)).into_styled(line_style).draw(target);
102
103    // Left Panel - Connection Badge
104    let (conn_text, conn_bg, conn_fg) = match output_mode {
105        Output::Usb => ("USB", rgb(0, 60, 20), rgb(0, 255, 100)),
106        Output::Ble => ("BLE", rgb(0, 30, 80), rgb(0, 180, 255)),
107    };
108    let badge_rect = Rectangle::new(Point::new(12, 34), Size::new(54, 16));
109    let badge_style = PrimitiveStyleBuilder::new()
110        .fill_color(conn_bg)
111        .build();
112    let _ = RoundedRectangle::new(badge_rect, CornerRadii::new(Size::new(4, 4)))
113        .into_styled(badge_style)
114        .draw(target);
115    let _ = draw_centered_text(target, conn_text, &FONT_6X10, badge_rect, conn_fg);
116
117    // Battery / Status bar
118    let bat_border_style = PrimitiveStyleBuilder::new()
119        .stroke_color(rgb(60, 70, 80))
120        .stroke_width(1)
121        .build();
122    let _ = Rectangle::new(Point::new(14, 55), Size::new(32, 6))
123        .into_styled(bat_border_style)
124        .draw(target);
125
126    let bat_fill_style = PrimitiveStyleBuilder::new()
127        .fill_color(if mouse_available { rgb(0, 220, 100) } else { rgb(0, 200, 255) })
128        .build();
129    let _ = Rectangle::new(Point::new(15, 56), Size::new(26, 4))
130        .into_styled(bat_fill_style)
131        .draw(target);
132
133    // 3. Center Panel: X = 78, Y = 6, W = 128, H = 64
134    let center_border_style = PrimitiveStyleBuilder::new()
135        .stroke_color(rgb(40, 50, 70))
136        .stroke_width(1)
137        .build();
138    let _ = RoundedRectangle::new(
139        Rectangle::new(Point::new(78, 6), Size::new(128, 64)),
140        CornerRadii::new(Size::new(6, 6)),
141    )
142    .into_styled(center_border_style)
143    .draw(target);
144
145    let label_style = MonoTextStyleBuilder::new()
146        .font(&FONT_6X10)
147        .text_color(rgb(120, 130, 150))
148        .build();
149    let _ = Text::with_baseline("ACTIVE LAYER", Point::new(86, 12), label_style, Baseline::Top).draw(target);
150
151    // Determine Active Layer
152    let active_layer = layer_state.iter().position(|&x| x).unwrap_or(0);
153    let (layer_name, layer_color) = match active_layer {
154        0 => ("BASE", rgb(0, 100, 150)),
155        1 => ("LOWER", rgb(180, 80, 0)),
156        2 => ("RAISE", rgb(130, 0, 180)),
157        3 => ("NAV", rgb(0, 120, 40)),
158        4 => ("MEDIA", rgb(180, 0, 80)),
159        _ => ("LAYER X", rgb(60, 60, 60)),
160    };
161
162    let layer_badge_rect = Rectangle::new(Point::new(86, 24), Size::new(112, 20));
163    let layer_badge_style = PrimitiveStyleBuilder::new()
164        .fill_color(layer_color)
165        .build();
166    let _ = RoundedRectangle::new(layer_badge_rect, CornerRadii::new(Size::new(4, 4)))
167        .into_styled(layer_badge_style)
168        .draw(target);
169    let _ = draw_centered_text(target, layer_name, &FONT_8X13, layer_badge_rect, rgb(255, 255, 255));
170
171    // Lock indicators (CAPS & NUM)
172    // CAPS
173    let caps_rect = Rectangle::new(Point::new(86, 49), Size::new(52, 14));
174    if caps_lock {
175        let caps_active_style = PrimitiveStyleBuilder::new()
176            .fill_color(rgb(220, 0, 100))
177            .build();
178        let _ = RoundedRectangle::new(caps_rect, CornerRadii::new(Size::new(3, 3)))
179            .into_styled(caps_active_style)
180            .draw(target);
181        let _ = draw_centered_text(target, "CAPS", &FONT_6X10, caps_rect, rgb(255, 255, 255));
182    } else {
183        let caps_inactive_style = PrimitiveStyleBuilder::new()
184            .stroke_color(rgb(40, 50, 60))
185            .stroke_width(1)
186            .build();
187        let _ = RoundedRectangle::new(caps_rect, CornerRadii::new(Size::new(3, 3)))
188            .into_styled(caps_inactive_style)
189            .draw(target);
190        let _ = draw_centered_text(target, "CAPS", &FONT_6X10, caps_rect, rgb(80, 90, 100));
191    }
192
193    // NUM
194    let num_rect = Rectangle::new(Point::new(146, 49), Size::new(52, 14));
195    if num_lock {
196        let num_active_style = PrimitiveStyleBuilder::new()
197            .fill_color(rgb(0, 180, 100))
198            .build();
199        let _ = RoundedRectangle::new(num_rect, CornerRadii::new(Size::new(3, 3)))
200            .into_styled(num_active_style)
201            .draw(target);
202        let _ = draw_centered_text(target, "NUM", &FONT_6X10, num_rect, rgb(255, 255, 255));
203    } else {
204        let num_inactive_style = PrimitiveStyleBuilder::new()
205            .stroke_color(rgb(40, 50, 60))
206            .stroke_width(1)
207            .build();
208        let _ = RoundedRectangle::new(num_rect, CornerRadii::new(Size::new(3, 3)))
209            .into_styled(num_inactive_style)
210            .draw(target);
211        let _ = draw_centered_text(target, "NUM", &FONT_6X10, num_rect, rgb(80, 90, 100));
212    }
213
214    // 4. Right Panel: X = 212, Y = 6, W = 66, H = 64
215    let _ = RoundedRectangle::new(
216        Rectangle::new(Point::new(212, 6), Size::new(66, 64)),
217        CornerRadii::new(Size::new(6, 6)),
218    )
219    .into_styled(panel_border_style)
220    .draw(target);
221
222    // "LIVE" label
223    let _ = Text::with_baseline("LIVE", Point::new(220, 12), label_style, Baseline::Top).draw(target);
224
225    // Blinking red recording dot
226    let dot_active = (anim_tick % 20) < 10;
227    let dot_style = PrimitiveStyleBuilder::new()
228        .fill_color(if dot_active { rgb(255, 0, 50) } else { rgb(80, 0, 10) })
229        .build();
230    let _ = Circle::new(Point::new(254, 14), 5).into_styled(dot_style).draw(target);
231
232    // Animated bouncing audio-style visualizer bars
233    let bar_color = match active_layer {
234        0 => rgb(0, 200, 255),
235        1 => rgb(255, 120, 0),
236        2 => rgb(200, 0, 255),
237        3 => rgb(0, 255, 100),
238        4 => rgb(255, 0, 120),
239        _ => rgb(150, 150, 150),
240    };
241
242    let bar_fill_style = PrimitiveStyleBuilder::new()
243        .fill_color(bar_color)
244        .build();
245
246    // Draw 8 bars representing dynamic visualizer
247    // Start X = 222, spacing = 6 (width 4 + gap 2), bottom Y = 60
248    for i in 0..8 {
249        let phase1 = (anim_tick.wrapping_mul(2).wrapping_add(i * 3)) % 32;
250        let phase2 = (anim_tick.wrapping_add(i * 7)) % 32;
251        let val = (get_sin(phase1) + get_sin(phase2)) / 2; // -100 to 100
252        
253        // Bar height range: 4 to 34 pixels
254        let bar_height = 4 + ((val + 100) * 30) / 200;
255        
256        let x = 222 + (i as i32 * 6);
257        let y = 60 - bar_height;
258        
259        let _ = Rectangle::new(Point::new(x, y), Size::new(4, bar_height as u32))
260            .into_styled(bar_fill_style)
261            .draw(target);
262    }
263}
264
265pub struct ColorBarDisplayConfig;
266
267impl DisplayConfig for ColorBarDisplayConfig {
268    type Color = Rgb565;
269
270    async fn start<D: DisplayDriver<Color = Self::Color>, const N1: usize, const N2: usize>(
271        &mut self,
272        display: &mut D,
273        display_controller: &Channel<DisplayMessage, N1>,
274        display_dynamic_message_controller: &Signal<heapless::String<N2>>,
275    ) {
276        let mut layer_state = [false; CONST_CONFIG.key_manager.layer_count as usize];
277        layer_state[0] = true; // BASE layer is active initially
278        let mut caps_lock = false;
279        let mut num_lock = false;
280        let mut output_mode = Output::Usb;
281        let mut mouse_available = false;
282        let mut anim_tick = 0u32;
283        let mut anim_ticker = Ticker::every(Duration::from_millis(50));
284
285        // Render initial static screen
286        let _ = display.clear().await;
287        draw_dashboard(
288            display,
289            &layer_state,
290            caps_lock,
291            num_lock,
292            output_mode,
293            mouse_available,
294            anim_tick,
295        )
296        .await;
297        let _ = display.flush().await;
298
299        loop {
300            // Check for incoming updates or tick the animation timer (50ms interval ~ 20 FPS)
301            let select_res = select3(
302                display_controller.receive(),
303                display_dynamic_message_controller.wait(),
304                anim_ticker.next(),
305            )
306            .await;
307
308            let mut state_changed = false;
309
310            match select_res {
311                Either3::First(mes) => match mes {
312                    DisplayMessage::Clear => {
313                        let _ = display.clear().await;
314                        state_changed = true;
315                    }
316                    DisplayMessage::Message(_msg) => {
317                        // Optional custom message rendering
318                    }
319                    DisplayMessage::Output(output) => {
320                        output_mode = output;
321                        state_changed = true;
322                    }
323                    DisplayMessage::LayerState(layers) => {
324                        layer_state = layers;
325                        state_changed = true;
326                    }
327                    DisplayMessage::MouseAvailable(mouse) => {
328                        mouse_available = mouse;
329                        state_changed = true;
330                    }
331                    DisplayMessage::NumLock(nl) => {
332                        num_lock = nl;
333                        state_changed = true;
334                    }
335                    DisplayMessage::CapsLock(cl) => {
336                        caps_lock = cl;
337                        state_changed = true;
338                    }
339                    DisplayMessage::Brightness(brightness) => {
340                        let _ = display.set_brightness(brightness).await;
341                    }
342                    DisplayMessage::On(on) => {
343                        let _ = display.set_display_on(on).await;
344                    }
345                    _ => {}
346                },
347                Either3::Second(_str) => {
348                    // Optional dynamic message string rendering
349                }
350                Either3::Third(_) => {
351                    // Idle animation timer tick
352                    anim_tick = anim_tick.wrapping_add(1);
353                    state_changed = true;
354                }
355            }
356
357            if state_changed {
358                draw_dashboard(
359                    display,
360                    &layer_state,
361                    caps_lock,
362                    num_lock,
363                    output_mode,
364                    mouse_available,
365                    anim_tick,
366                )
367                .await;
368                let _ = display.flush().await;
369            }
370        }
371    }
372}