summaryrefslogtreecommitdiffstats
path: root/src/ui.rs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-07-11 01:46:59 +0200
committersalmonik <raisersoft@proton.me>2026-07-11 01:46:59 +0200
commit247d2805db0c1b686ef4c6cc0f786ff416aeb7e3 (patch)
treeb6dae4a848df5ae75c4d1bc8e73bad7c63d17575 /src/ui.rs
downloadxkbpreview-247d2805db0c1b686ef4c6cc0f786ff416aeb7e3.tar.gz
xkbpreview-247d2805db0c1b686ef4c6cc0f786ff416aeb7e3.zip
init
Diffstat (limited to 'src/ui.rs')
-rw-r--r--src/ui.rs221
1 files changed, 221 insertions, 0 deletions
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 @@
1use crate::keymap;
2use eframe::egui::{self, Color32, CornerRadius, Stroke, Vec2};
3
4const LEVEL_NAMES: &[&str; 4] = &[
5 "Level 1 (base)",
6 "Level 2 (Shift)",
7 "Level 3 (AltGr)",
8 "Level 4 (Shift+AltGr)",
9];
10
11const KEY_BG: Color32 = Color32::from_rgb(0x33, 0x33, 0x33);
12const KEY_BG_MOD_SHIFT: Color32 = Color32::from_rgb(0x2a, 0x5a, 0x2a);
13const KEY_BG_MOD_L3: Color32 = Color32::from_rgb(0x4a, 0x2a, 0x5a);
14const KEY_BG_MOD_OTHER: Color32 = Color32::from_rgb(0x3a, 0x3a, 0x3a);
15const KEY_TEXT: Color32 = Color32::from_rgb(0xcc, 0xcc, 0xcc);
16const KEY_TEXT_DIM: Color32 = Color32::from_rgb(0x55, 0x55, 0x55);
17const KEY_TEXT_ACTIVE: Color32 = Color32::from_rgb(0xff, 0xff, 0xff);
18const KEY_TEXT_MOD: Color32 = Color32::from_rgb(0xff, 0xff, 0xcc);
19const KEY_TEXT_DEAD: Color32 = Color32::from_rgb(0xff, 0xaa, 0x44);
20
21const KEY_SPACING: f32 = 4.0;
22const KEY_UNIT: f32 = 52.0;
23const KEY_HEIGHT: f32 = 48.0;
24
25fn key_bg(kind: &keymap::KeyKind) -> Color32 {
26 match kind {
27 keymap::KeyKind::Shift => KEY_BG_MOD_SHIFT,
28 keymap::KeyKind::Level3 => KEY_BG_MOD_L3,
29 keymap::KeyKind::Lock | keymap::KeyKind::Control | keymap::KeyKind::Alt | keymap::KeyKind::Super => {
30 KEY_BG_MOD_OTHER
31 }
32 keymap::KeyKind::Normal => KEY_BG,
33 }
34}
35
36fn text_color_for_level(level: usize, active_level: usize, kind: &keymap::KeyKind) -> Color32 {
37 if level == active_level {
38 match kind {
39 keymap::KeyKind::Shift | keymap::KeyKind::Level3 => KEY_TEXT_MOD,
40 _ => KEY_TEXT_ACTIVE,
41 }
42 } else {
43 KEY_TEXT_DIM
44 }
45}
46
47pub fn render(
48 ui: &mut egui::Ui,
49 key_data: &Option<keymap::KeyData>,
50 selected_level: &mut usize,
51 active_level: usize,
52 active_group: usize,
53 error: &Option<String>,
54) {
55 egui::Panel::top("toolbar").show_inside(ui, |ui| {
56 ui.horizontal(|ui| {
57 ui.label(egui::RichText::new("xkbpreview").strong());
58
59 if let Some(data) = key_data {
60 let g = active_group.min(data.layouts.len().saturating_sub(1));
61 ui.label(format!(" layout: {}", data.layouts[g]));
62 if data.is_iso {
63 ui.label(" [ISO]");
64 } else {
65 ui.label(" [ANSI]");
66 }
67 }
68
69 ui.label(format!(" Lv:{}", active_level));
70
71 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
72 ui.label("Highlight:");
73 for (i, name) in LEVEL_NAMES.iter().enumerate() {
74 let mark = if i == active_level { "[x]" } else { "[ ]" };
75 if ui.selectable_label(*selected_level == i, format!("{} {}", mark, name)).clicked() {
76 *selected_level = i;
77 }
78 }
79 });
80 });
81 });
82
83 if let Some(err) = error {
84 egui::CentralPanel::default().show_inside(ui, |ui| {
85 ui.colored_label(Color32::RED, format!("Error: {}", err));
86 ui.label("Make sure you're running under X11 and the XKB configuration is available.");
87 });
88 return;
89 }
90
91 let data = match key_data {
92 Some(d) => d,
93 None => {
94 egui::CentralPanel::default().show_inside(ui, |ui| {
95 ui.label("Loading keyboard layout...");
96 });
97 return;
98 }
99 };
100
101 egui::CentralPanel::default().show_inside(ui, |ui| {
102 ui.vertical_centered(|ui| {
103 let rows = group_by_row(&data.keys);
104 let max_row = rows.iter().map(|(r, _)| *r).max().unwrap_or(0);
105 let total_w = 15.5 * (KEY_UNIT + KEY_SPACING);
106
107 let (rect, _) = ui.allocate_exact_size(
108 Vec2::new(total_w, (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING)),
109 egui::Sense::hover(),
110 );
111
112 let painter = ui.painter_at(rect);
113 let origin = rect.min;
114
115 for (row_idx, entries) in &rows {
116 for entry in entries {
117 let p = &entry.pos;
118 let x = p.col * (KEY_UNIT + KEY_SPACING);
119 let y = *row_idx as f32 * (KEY_HEIGHT + KEY_SPACING);
120 let w = p.width * KEY_UNIT + (p.width - 1.0) * KEY_SPACING;
121
122 let key_rect = egui::Rect::from_min_size(
123 origin + Vec2::new(x, y),
124 Vec2::new(w, KEY_HEIGHT),
125 );
126
127 let bg = key_bg(&entry.kind);
128 painter.rect_filled(key_rect, CornerRadius::same(3), bg);
129 painter.rect_stroke(key_rect, CornerRadius::same(3), Stroke::new(1.0, Color32::from_rgb(0x55, 0x55, 0x55)), egui::StrokeKind::Middle);
130
131 let inset = 3.0;
132 let cw = key_rect.width() / 2.0;
133
134 let centers: [egui::Pos2; 4] = [
135 key_rect.center(),
136 egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.top() + inset + 4.0),
137 egui::Pos2::new(key_rect.right() - cw / 2.0, key_rect.top() + inset + 4.0),
138 egui::Pos2::new(key_rect.left() + cw / 2.0, key_rect.bottom() - inset - 4.0),
139 ];
140
141 let level_order = [0usize, 1, 2, 3];
142 let font_sizes = [15.0, 10.0, 10.0, 9.0];
143 let aligns = [
144 egui::Align2::CENTER_CENTER,
145 egui::Align2::LEFT_TOP,
146 egui::Align2::RIGHT_TOP,
147 egui::Align2::LEFT_BOTTOM,
148 ];
149
150 let g = active_group.min(3);
151 for &li in &level_order {
152 let label = &entry.labels[g][li];
153 if label.is_empty() {
154 continue;
155 }
156 let color = if entry.dead[g][li] {
157 KEY_TEXT_DEAD
158 } else if li == 0 {
159 if 0 == active_level {
160 text_color_for_level(li, active_level, &entry.kind)
161 } else {
162 KEY_TEXT
163 }
164 } else {
165 text_color_for_level(li, active_level, &entry.kind)
166 };
167 let font = if label.len() > 2 {
168 egui::FontId::proportional(font_sizes[li] - 1.0)
169 } else {
170 egui::FontId::proportional(font_sizes[li])
171 };
172 painter.text(centers[li], aligns[li], label, font, color);
173 }
174 }
175 }
176
177 let legend_y = (max_row + 1) as f32 * (KEY_HEIGHT + KEY_SPACING) + 16.0;
178 let legend_items = [
179 (KEY_BG_MOD_SHIFT, "Shift (triggers L2)"),
180 (KEY_BG_MOD_L3, "AltGr (triggers L3)"),
181 (KEY_BG_MOD_OTHER, "Other modifiers"),
182 (KEY_TEXT_DEAD, "Dead key accent"),
183 ];
184 for (i, (color, text)) in legend_items.iter().enumerate() {
185 let ly = legend_y + i as f32 * 20.0;
186 let r = egui::Rect::from_min_size(
187 origin + Vec2::new(0.0, ly),
188 Vec2::new(14.0, 14.0),
189 );
190 painter.rect_filled(r, CornerRadius::same(2), *color);
191 painter.text(
192 egui::Pos2::new(r.right() + 6.0, r.center().y),
193 egui::Align2::LEFT_CENTER,
194 *text,
195 egui::FontId::proportional(12.0),
196 KEY_TEXT,
197 );
198 }
199 });
200 });
201}
202
203fn group_by_row<'a>(keys: &'a [keymap::KeyEntry]) -> Vec<(usize, Vec<&'a keymap::KeyEntry>)> {
204 let mut rows: Vec<(usize, Vec<&'a keymap::KeyEntry>)> = Vec::new();
205 for key in keys {
206 let row = key.pos.row;
207 if row > 4 {
208 continue;
209 }
210 if let Some(entry) = rows.iter_mut().find(|(r, _)| *r == row) {
211 entry.1.push(key);
212 } else {
213 rows.push((row, vec![key]));
214 }
215 }
216 rows.sort_by_key(|(r, _)| *r);
217 for (_, entries) in &mut rows {
218 entries.sort_by(|a, b| a.pos.col.partial_cmp(&b.pos.col).unwrap());
219 }
220 rows
221}