mirror of
https://gitlab.com/erkanisik/yali-rs.git
synced 2026-07-30 18:59:02 +00:00
635c844710
- Custom combobox implementation with TextEdit + Area popup for keyboard layout/variant search - Added color_from_hex() helper, layout_buf/variant_buf fields - Removed egui-dropdown dependency - Popup items 28px -> 32px with gradient background - Dropdown text color changed to pure white - All color constants converted to hex format - KLE SVG rendering improvements - CHANGELOG.md updated
271 lines
9.3 KiB
Rust
271 lines
9.3 KiB
Rust
use eframe::egui::{self, Vec2};
|
|
use xkbcommon::xkb;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct XkbKey {
|
|
pub x: f32, pub y: f32, pub w: f32, pub h: f32,
|
|
pub labels: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct XkbLayout {
|
|
pub keys: Vec<XkbKey>,
|
|
}
|
|
|
|
pub fn empty_layout() -> XkbLayout {
|
|
XkbLayout { keys: vec![] }
|
|
}
|
|
|
|
fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
|
|
let r = |row| row as f32;
|
|
|
|
match name {
|
|
"TLDE" => Some((0.0, r(1), 1.0, 1.0)),
|
|
"AE01" => Some((1.0, r(1), 1.0, 1.0)),
|
|
"AE02" => Some((2.0, r(1), 1.0, 1.0)),
|
|
"AE03" => Some((3.0, r(1), 1.0, 1.0)),
|
|
"AE04" => Some((4.0, r(1), 1.0, 1.0)),
|
|
"AE05" => Some((5.0, r(1), 1.0, 1.0)),
|
|
"AE06" => Some((6.0, r(1), 1.0, 1.0)),
|
|
"AE07" => Some((7.0, r(1), 1.0, 1.0)),
|
|
"AE08" => Some((8.0, r(1), 1.0, 1.0)),
|
|
"AE09" => Some((9.0, r(1), 1.0, 1.0)),
|
|
"AE10" => Some((10.0, r(1), 1.0, 1.0)),
|
|
"AE11" => Some((11.0, r(1), 1.0, 1.0)),
|
|
"AE12" => Some((12.0, r(1), 1.0, 1.0)),
|
|
"BKSP" => Some((13.0, r(1), 2.25, 1.0)),
|
|
|
|
"TAB" => Some((0.0, r(2), 1.5, 1.0)),
|
|
"AD01" => Some((1.5, r(2), 1.0, 1.0)),
|
|
"AD02" => Some((2.5, r(2), 1.0, 1.0)),
|
|
"AD03" => Some((3.5, r(2), 1.0, 1.0)),
|
|
"AD04" => Some((4.5, r(2), 1.0, 1.0)),
|
|
"AD05" => Some((5.5, r(2), 1.0, 1.0)),
|
|
"AD06" => Some((6.5, r(2), 1.0, 1.0)),
|
|
"AD07" => Some((7.5, r(2), 1.0, 1.0)),
|
|
"AD08" => Some((8.5, r(2), 1.0, 1.0)),
|
|
"AD09" => Some((9.5, r(2), 1.0, 1.0)),
|
|
"AD10" => Some((10.5, r(2), 1.0, 1.0)),
|
|
"AD11" => Some((11.5, r(2), 1.0, 1.0)),
|
|
"AD12" => Some((12.5, r(2), 1.0, 1.0)),
|
|
|
|
"CAPS" => Some((0.0, r(3), 1.75, 1.0)),
|
|
"AC01" => Some((1.75, r(3), 1.0, 1.0)),
|
|
"AC02" => Some((2.75, r(3), 1.0, 1.0)),
|
|
"AC03" => Some((3.75, r(3), 1.0, 1.0)),
|
|
"AC04" => Some((4.75, r(3), 1.0, 1.0)),
|
|
"AC05" => Some((5.75, r(3), 1.0, 1.0)),
|
|
"AC06" => Some((6.75, r(3), 1.0, 1.0)),
|
|
"AC07" => Some((7.75, r(3), 1.0, 1.0)),
|
|
"AC08" => Some((8.75, r(3), 1.0, 1.0)),
|
|
"AC09" => Some((9.75, r(3), 1.0, 1.0)),
|
|
"AC10" => Some((10.75,r(3), 1.0, 1.0)),
|
|
"AC11" => Some((11.75,r(3), 1.0, 1.0)),
|
|
"AC12" => Some((12.75,r(3), 1.0, 1.0)),
|
|
"BKSL" => Some((12.50,r(3), 1.0, 1.0)),
|
|
|
|
"LFSH" => Some((0.0, r(4), 1.25, 1.0)),
|
|
"LSGT" => Some((1.25, r(4), 1.0, 1.0)),
|
|
"AB01" => Some((2.25, r(4), 1.0, 1.0)),
|
|
"AB02" => Some((3.25, r(4), 1.0, 1.0)),
|
|
"AB03" => Some((4.25, r(4), 1.0, 1.0)),
|
|
"AB04" => Some((5.25, r(4), 1.0, 1.0)),
|
|
"AB05" => Some((6.25, r(4), 1.0, 1.0)),
|
|
"AB06" => Some((7.25, r(4), 1.0, 1.0)),
|
|
"AB07" => Some((8.25, r(4), 1.0, 1.0)),
|
|
"AB08" => Some((9.25, r(4), 1.0, 1.0)),
|
|
"AB09" => Some((10.25,r(4), 1.0, 1.0)),
|
|
"AB10" => Some((11.25,r(4), 1.0, 1.0)),
|
|
"RTSH" => Some((12.25,r(4), 3.0, 1.0)),
|
|
|
|
"LCTL" => Some((0.0, r(5), 1.25, 1.0)),
|
|
"LWIN" => Some((1.25, r(5), 1.25, 1.0)),
|
|
"LALT" => Some((2.5, r(5), 1.5, 1.0)),
|
|
"SPCE" => Some((4.0, r(5), 6.25, 1.0)),
|
|
"RALT" | "LVL3" => Some((10.25, r(5), 1.5, 1.0)),
|
|
"COMP" => Some((11.75, r(5), 1.25, 1.0)),
|
|
"RCTL" => Some((13.0, r(5), 2.25, 1.0)),
|
|
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn keysym_to_label(sym: xkb::Keysym) -> String {
|
|
let utf = xkb::keysym_to_utf8(sym);
|
|
if utf.chars().any(|c| !c.is_control()) {
|
|
let trimmed = utf.trim();
|
|
if !trimmed.is_empty() {
|
|
return trimmed.to_uppercase();
|
|
}
|
|
}
|
|
let name = xkb::keysym_get_name(sym);
|
|
if name == "VoidSymbol" || name == "NoSymbol" { return String::new(); }
|
|
match name.as_str() {
|
|
"BackSpace" => "Bksp".into(),
|
|
"Tab" => "Tab".into(),
|
|
"Return" => "Enter".into(),
|
|
"Caps_Lock" => "Caps".into(),
|
|
"Shift_L" | "Shift_R" => "Shift".into(),
|
|
"Control_L" | "Control_R" => "Ctrl".into(),
|
|
"Alt_L" => "Alt".into(),
|
|
"Alt_R" | "ISO_Level3_Shift" => "AltGr".into(),
|
|
"Super_L" | "Super_R" => "Win".into(),
|
|
"Menu" => "Menü".into(),
|
|
"Escape" => "Esc".into(),
|
|
"space" => "".into(),
|
|
_ => name,
|
|
}
|
|
}
|
|
|
|
pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> {
|
|
let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
|
context.set_log_level(xkb::LogLevel::Critical);
|
|
|
|
let keymap = xkb::Keymap::new_from_names::<str>(
|
|
&context, "", "pc105", layout,
|
|
if variant.is_empty() { "" } else { variant },
|
|
None::<String>, xkb::COMPILE_NO_FLAGS,
|
|
)?;
|
|
|
|
let min = keymap.min_keycode();
|
|
let max = keymap.max_keycode();
|
|
let mut keys = Vec::new();
|
|
|
|
let mut min_y = f32::MAX;
|
|
|
|
let mut enter_label = "Enter".to_string();
|
|
|
|
for code_u32 in (min.raw() as u32)..=(max.raw() as u32) {
|
|
let code = xkb::Keycode::new(code_u32);
|
|
let Some(name) = keymap.key_get_name(code) else { continue };
|
|
|
|
if name == "RTRN" {
|
|
let syms = keymap.key_get_syms_by_level(code, 0, 0);
|
|
let label = syms.iter().map(|&s| keysym_to_label(s)).collect::<Vec<_>>().join("");
|
|
if !label.is_empty() { enter_label = label; }
|
|
continue;
|
|
}
|
|
|
|
let Some((x, y, w, h)) = key_position(name) else { continue };
|
|
|
|
let mut raw_labels = Vec::new();
|
|
for level in 0..4 {
|
|
let syms = keymap.key_get_syms_by_level(code, 0, level);
|
|
let label = syms.iter().map(|&s| keysym_to_label(s)).collect::<Vec<_>>().join("");
|
|
if !label.is_empty() && !raw_labels.contains(&label) {
|
|
raw_labels.push(label);
|
|
}
|
|
}
|
|
|
|
let labels: Vec<String> = {
|
|
let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect();
|
|
if single.is_empty() {
|
|
raw_labels.into_iter().take(1).collect()
|
|
} else {
|
|
let uc = single.iter().find(|l| l.chars().next().map_or(false, |c| c.is_uppercase()));
|
|
vec![if let Some(s) = uc { (*s).clone() } else { (*single[0]).clone() }]
|
|
}
|
|
};
|
|
|
|
keys.push(XkbKey { x, y, w, h, labels });
|
|
|
|
if y < min_y { min_y = y; }
|
|
}
|
|
|
|
keys.push(XkbKey { x: 13.5, y: 2.0, w: 1.75, h: 1.0, labels: vec![enter_label.clone()] });
|
|
keys.push(XkbKey { x: 14.75, y: 3.0, w: 0.5, h: 1.0, labels: vec!["_RTRN_BOT".to_string()] });
|
|
|
|
for key in &mut keys {
|
|
key.y -= min_y;
|
|
}
|
|
|
|
Some(XkbLayout {
|
|
keys,
|
|
})
|
|
}
|
|
|
|
pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) {
|
|
let margin = 6.0;
|
|
|
|
let svg_size = Vec2::new(840.0, 300.0);
|
|
let img_scale = ((max_size.x - margin * 2.0) / svg_size.x)
|
|
.min((max_size.y - margin * 2.0) / svg_size.y)
|
|
.max(0.1);
|
|
let draw_size = svg_size * img_scale;
|
|
let total_size = Vec2::new(draw_size.x + margin * 2.0, draw_size.y + margin * 2.0);
|
|
|
|
let (_id, resp) = ui.allocate_exact_size(total_size, egui::Sense::hover());
|
|
let painter = ui.painter_at(resp.rect);
|
|
let origin = egui::pos2(resp.rect.min.x + margin, resp.rect.min.y + margin);
|
|
|
|
let visuals = ui.visuals();
|
|
let bg = visuals.window_fill();
|
|
let is_dark = visuals.dark_mode;
|
|
|
|
painter.rect_filled(resp.rect, egui::Rounding::same(6.0), bg);
|
|
|
|
let img_rect = egui::Rect::from_min_size(origin, draw_size);
|
|
egui::Image::from_bytes(
|
|
"keyboard.svg",
|
|
&include_bytes!("../../assets/keyboard-layout.svg")[..],
|
|
).paint_at(ui, img_rect);
|
|
|
|
// Dark mode: SVG'yi karart
|
|
if is_dark {
|
|
painter.rect_filled(img_rect, egui::Rounding::same(4.0), egui::Color32::from_black_alpha(170));
|
|
}
|
|
|
|
let font_size = (draw_size.x * 0.015).max(6.0);
|
|
|
|
let text_color = if is_dark { egui::Color32::from_gray(230) } else { egui::Color32::from_gray(30) };
|
|
|
|
const PITCH: f32 = 54.0;
|
|
const KEY_PX: f32 = 52.0;
|
|
const CX0: f32 = 42.0;
|
|
const CY0: f32 = 42.0;
|
|
|
|
const ENTER_VBX: f32 = 791.0;
|
|
const ENTER_VBY: f32 = 123.0;
|
|
|
|
let mut enter_text = String::new();
|
|
|
|
for key in &layout.keys {
|
|
if let Some(lbl) = key.labels.first() {
|
|
if lbl == "_RTRN_BOT" { continue; }
|
|
if key.y == 1.0 && (key.x - 13.5).abs() < 0.1 {
|
|
enter_text = lbl.clone();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let svg_cx = CX0 + key.x * PITCH;
|
|
let svg_cy = CY0 + key.y * PITCH;
|
|
let svg_w = KEY_PX + (key.w - 1.0) * PITCH;
|
|
let svg_h = KEY_PX + (key.h - 1.0) * PITCH;
|
|
|
|
let ex = origin.x + (svg_cx - svg_w / 2.0) * img_scale;
|
|
let ey = origin.y + (svg_cy - svg_h / 2.0) * img_scale;
|
|
let ew = svg_w * img_scale;
|
|
let eh = svg_h * img_scale;
|
|
|
|
if let Some(lbl) = key.labels.first() {
|
|
let trimmed = lbl.trim();
|
|
if trimmed.is_empty() { continue; }
|
|
|
|
let text_pos = egui::pos2(ex + ew / 2.0, ey + eh / 2.0);
|
|
|
|
painter.text(text_pos, egui::Align2::CENTER_CENTER, trimmed, egui::FontId::proportional(font_size), text_color);
|
|
}
|
|
}
|
|
|
|
if !enter_text.is_empty() {
|
|
painter.text(
|
|
egui::pos2(origin.x + ENTER_VBX * img_scale, origin.y + ENTER_VBY * img_scale),
|
|
egui::Align2::CENTER_CENTER,
|
|
&enter_text,
|
|
egui::FontId::proportional(font_size),
|
|
text_color,
|
|
);
|
|
}
|
|
}
|