From 247d2805db0c1b686ef4c6cc0f786ff416aeb7e3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 11 Jul 2026 01:46:59 +0200 Subject: init --- src/ui.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/ui.rs (limited to 'src/ui.rs') diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..b9abeff --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,221 @@ +use crate::keymap; +use eframe::egui::{self, Color32, CornerRadius, Stroke, Vec2}; + +const LEVEL_NAMES: &[&str; 4] = &[ + "Level 1 (base)", + "Level 2 (Shift)", + "Level 3 (AltGr)", + "Level 4 (Shift+AltGr)", +]; + +const KEY_BG: Color32 = Color32::from_rgb(0x33, 0x33, 0x33); +const KEY_BG_MOD_SHIFT: Color32 = Color32::from_rgb(0x2a, 0x5a, 0x2a); +const KEY_BG_MOD_L3: Color32 = Color32::from_rgb(0x4a, 0x2a, 0x5a); +const KEY_BG_MOD_OTHER: Color32 = Color32::from_rgb(0x3a, 0x3a, 0x3a); +const KEY_TEXT: Color32 = Color32::from_rgb(0xcc, 0xcc, 0xcc); +const KEY_TEXT_DIM: Color32 = Color32::from_rgb(0x55, 0x55, 0x55); +const KEY_TEXT_ACTIVE: Color32 = Color32::from_rgb(0xff, 0xff, 0xff); +const KEY_TEXT_MOD: Color32 = Color32::from_rgb(0xff, 0xff, 0xcc); +const KEY_TEXT_DEAD: Color32 = Color32::from_rgb(0xff, 0xaa, 0x44); + +const KEY_SPACING: f32 = 4.0; +const KEY_UNIT: f32 = 52.0; +const KEY_HEIGHT: f32 = 48.0; + +fn key_bg(kind: &keymap::KeyKind) -> Color32 { + match kind { + keymap::KeyKind::Shift => KEY_BG_MOD_SHIFT, + keymap::KeyKind::Level3 => KEY_BG_MOD_L3, + keymap::KeyKind::Lock | keymap::KeyKind::Control | keymap::KeyKind::Alt | keymap::KeyKind::Super => { + KEY_BG_MOD_OTHER + } + keymap::KeyKind::Normal => KEY_BG, + } +} + +fn text_color_for_level(level: usize, active_level: usize, kind: &keymap::KeyKind) -> Color32 { + if level == active_level { + match kind { + keymap::KeyKind::Shift | keymap::KeyKind::Level3 => KEY_TEXT_MOD, + _ => KEY_TEXT_ACTIVE, + } + } else { + KEY_TEXT_DIM + } +} + +pub fn render( + ui: &mut egui::Ui, + key_data: &Option, + selected_level: &mut usize, + active_level: usize, + active_group: usize, + error: &Option, +) { + egui::Panel::top("toolbar").show_inside(ui, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("xkbpreview").strong()); + + if let Some(data) = key_data { + let g = active_group.min(data.layouts.len().saturating_sub(1)); + ui.label(format!(" layout: {}", data.layouts[g])); + if data.is_iso { + ui.label(" [ISO]"); + } else { + ui.label(" [ANSI]"); + } + } + + ui.label(format!(" Lv:{}", active_level)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label("Highlight:"); + for (i, name) in LEVEL_NAMES.iter().enumerate() { + let mark = if i == active_level { "[x]" } else { "[ ]" }; + if ui.selectable_label(*selected_level == i, format!("{} {}", mark, name)).clicked() { + *selected_level = i; + } + } + }); + }); + }); + + if let Some(err) = error { + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.colored_label(Color32::RED, format!("Error: {}", err)); + ui.label("Make sure you're running under X11 and the XKB configuration is available."); + }); + return; + } + + let data = match key_data { + Some(d) => d, + None => { + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.label("Loading keyboard layout..."); + }); + return; + } + }; + + egui::CentralPanel::default().show_inside(ui, |ui| { + ui.vertical_centered(|ui| { + let rows = group_by_row(&data.keys); + let max_row = rows.iter().map(|(r, _)| *r).max().unwrap_or(0); + let total_w = 15.5 * (KEY_UNIT + KEY_SPACING); + + let (rect, _) = ui.allocate_exact_size( + Vec2::new(total_w, (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING)), + egui::Sense::hover(), + ); + + let painter = ui.painter_at(rect); + let origin = rect.min; + + for (row_idx, entries) in &rows { + for entry in entries { + let p = &entry.pos; + let x = p.col * (KEY_UNIT + KEY_SPACING); + let y = *row_idx as f32 * (KEY_HEIGHT + KEY_SPACING); + let w = p.width * KEY_UNIT + (p.width - 1.0) * KEY_SPACING; + + let key_rect = egui::Rect::from_min_size( + origin + Vec2::new(x, y), + Vec2::new(w, KEY_HEIGHT), + ); + + let bg = key_bg(&entry.kind); + painter.rect_filled(key_rect, CornerRadius::same(3), bg); + painter.rect_stroke(key_rect, CornerRadius::same(3), Stroke::new(1.0, Color32::from_rgb(0x55, 0x55, 0x55)), egui::StrokeKind::Middle); + + let inset = 3.0; + let cw = key_rect.width() / 2.0; + + let centers: [egui::Pos2; 4] = [ + key_rect.center(), + egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.top() + inset + 4.0), + egui::Pos2::new(key_rect.right() - cw / 2.0, key_rect.top() + inset + 4.0), + egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.bottom() - inset - 4.0), + ]; + + let level_order = [0usize, 1, 2, 3]; + let font_sizes = [15.0, 10.0, 10.0, 9.0]; + let aligns = [ + egui::Align2::CENTER_CENTER, + egui::Align2::LEFT_TOP, + egui::Align2::RIGHT_TOP, + egui::Align2::LEFT_BOTTOM, + ]; + + let g = active_group.min(3); + for &li in &level_order { + let label = &entry.labels[g][li]; + if label.is_empty() { + continue; + } + let color = if entry.dead[g][li] { + KEY_TEXT_DEAD + } else if li == 0 { + if 0 == active_level { + text_color_for_level(li, active_level, &entry.kind) + } else { + KEY_TEXT + } + } else { + text_color_for_level(li, active_level, &entry.kind) + }; + let font = if label.len() > 2 { + egui::FontId::proportional(font_sizes[li] - 1.0) + } else { + egui::FontId::proportional(font_sizes[li]) + }; + painter.text(centers[li], aligns[li], label, font, color); + } + } + } + + let legend_y = (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING) + 16.0; + let legend_items = [ + (KEY_BG_MOD_SHIFT, "Shift (triggers L2)"), + (KEY_BG_MOD_L3, "AltGr (triggers L3)"), + (KEY_BG_MOD_OTHER, "Other modifiers"), + (KEY_TEXT_DEAD, "Dead key accent"), + ]; + for (i, (color, text)) in legend_items.iter().enumerate() { + let ly = legend_y + i as f32 * 20.0; + let r = egui::Rect::from_min_size( + origin + Vec2::new(0.0, ly), + Vec2::new(14.0, 14.0), + ); + painter.rect_filled(r, CornerRadius::same(2), *color); + painter.text( + egui::Pos2::new(r.right() + 6.0, r.center().y), + egui::Align2::LEFT_CENTER, + *text, + egui::FontId::proportional(12.0), + KEY_TEXT, + ); + } + }); + }); +} + +fn group_by_row<'a>(keys: &'a [keymap::KeyEntry]) -> Vec<(usize, Vec<&'a keymap::KeyEntry>)> { + let mut rows: Vec<(usize, Vec<&'a keymap::KeyEntry>)> = Vec::new(); + for key in keys { + let row = key.pos.row; + if row > 4 { + continue; + } + if let Some(entry) = rows.iter_mut().find(|(r, _)| *r == row) { + entry.1.push(key); + } else { + rows.push((row, vec![key])); + } + } + rows.sort_by_key(|(r, _)| *r); + for (_, entries) in &mut rows { + entries.sort_by(|a, b| a.pos.col.partial_cmp(&b.pos.col).unwrap()); + } + rows +} -- cgit v1.2.3