From ae915c25407c7ceb26a343f88cc2c447abc14be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Thu, 18 Jun 2026 13:07:18 +0300 Subject: [PATCH] feat: replace KLE JSON keyboard rendering with xkbcommon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xkbcommon ile XKB keymap derlenip tuş pozisyonları/sembolleri çıkarılıyor, egui Painter ile temaya uygun çiziliyor. KLE JSON indirme/kullanma kalktı, ureq ve serde_json bağımlılıkları kaldırıldı. - compile_layout() — xkb::Keymap::new_from_names() ile keymap derleme - key_position() — ISO‑105 tuşları için ondalıklı x,y pozisyonları - keysym_to_label() — xkbcommon keysym → kısa etiket (⌫, ⇥, AltGr, …) - draw_layout() — egui Painter ile ölçeklenebilir klavye çizimi - Her tuşta tek karakter gösterimi (büyük harf tercihli) - keyboard_layout boşken ilk show()'da varsayılana set edilir - Eski KLE JSON önbellek dosyaları silindi --- Cargo.toml | 1 + src/main.rs | 45 +++--- src/steps/keyboard.rs | 167 +++++++++------------ src/ui/buttons.rs | 13 +- src/ui/kle_svg.rs | 334 ++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/theme.rs | 56 +------ 7 files changed, 436 insertions(+), 181 deletions(-) create mode 100644 src/ui/kle_svg.rs diff --git a/Cargo.toml b/Cargo.toml index 09de98f..caa4aa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ rust-i18n = "3.1" lazy_static = "1.4" async-trait = "0.1" chrono = "0.4" +xkbcommon = "0.9" [patch.crates-io] # Yerel mudur veya comar-api kütüphaneleri buraya eklenebilir diff --git a/src/main.rs b/src/main.rs index 0b4987f..250a631 100644 --- a/src/main.rs +++ b/src/main.rs @@ -211,6 +211,7 @@ struct YaliApp { last_step: usize, branding: branding::BrandingConfig, loaded_language: String, + #[allow(dead_code)] show_cancel_dialog: bool, } @@ -451,9 +452,9 @@ impl eframe::App for YaliApp { } else { t!("next") }; - if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() { - self.show_cancel_dialog = true; - }; + // if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() { + // self.show_cancel_dialog = true; + // }; let next_clicked = if is_valid { ui::theme::primary_button(ui, &next_label).clicked() } else { @@ -476,25 +477,25 @@ impl eframe::App for YaliApp { ui.add_space(10.0); }); - // ── İptal onay diyaloğu ─────────────────────────────── - if self.show_cancel_dialog { - egui::Window::new(t!("cancel_confirm_title")) - .collapsible(false) - .resizable(false) - .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) - .show(ctx, |ui| { - ui.label(t!("cancel_confirm_msg")); - ui.add_space(12.0); - ui.horizontal(|ui| { - if ui.button(t!("cancel_confirm_yes")).clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - if ui.button(t!("cancel_confirm_no")).clicked() { - self.show_cancel_dialog = false; - } - }); - }); - } + // // ── İptal onay diyaloğu ─────────────────────────────── + // if self.show_cancel_dialog { + // egui::Window::new(t!("cancel_confirm_title")) + // .collapsible(false) + // .resizable(false) + // .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + // .show(ctx, |ui| { + // ui.label(t!("cancel_confirm_msg")); + // ui.add_space(12.0); + // ui.horizontal(|ui| { + // if ui.button(t!("cancel_confirm_yes")).clicked() { + // ctx.send_viewport_cmd(egui::ViewportCommand::Close); + // } + // if ui.button(t!("cancel_confirm_no")).clicked() { + // self.show_cancel_dialog = false; + // } + // }); + // }); + // } // ── Merkez panel ───────────────────────────────────── egui::CentralPanel::default() diff --git a/src/steps/keyboard.rs b/src/steps/keyboard.rs index 4bd8f94..b9fb274 100644 --- a/src/steps/keyboard.rs +++ b/src/steps/keyboard.rs @@ -1,4 +1,4 @@ -use eframe::egui; +use eframe::egui::{self, Color32}; use rust_i18n::t; use crate::installer::{GlobalState, InstallerStep}; use crate::ui::theme; @@ -8,8 +8,6 @@ use crate::ui::theme; /// Dosya okunamazsa sabit bir temel liste kullanılır. pub struct KeyboardStep { test_input: String, - layout_filter: String, - variant_filter: String, layouts: Vec<(String, String)>, // (kod, gösterim adı) variants: Vec<(String, String, String)>, // (layout, variant, gösterim adı) } @@ -17,6 +15,7 @@ pub struct KeyboardStep { fn default_keyboard_layouts() -> Vec<(String, String)> { vec![ ("tr".to_string(), "Türkçe (Q)".to_string()), + ("az".to_string(), "Azərbaycanca".to_string()), ("us".to_string(), "İngilizce (US)".to_string()), ("de".to_string(), "Almanca".to_string()), ("fr".to_string(), "Fransızca".to_string()), @@ -29,6 +28,7 @@ fn default_keyboard_layouts() -> Vec<(String, String)> { fn default_keyboard_variants() -> Vec<(String, String, String)> { vec![ ("tr".to_string(), "f".to_string(), "Türkçe (F)".to_string()), + ("az".to_string(), "cyrillic".to_string(), "Azərbaycan (Kiril)".to_string()), ] } @@ -129,8 +129,6 @@ impl Default for KeyboardStep { let (layouts, variants) = load_keyboard_layouts(); Self { test_input: String::new(), - layout_filter: String::new(), - variant_filter: String::new(), layouts, variants, } @@ -155,122 +153,91 @@ impl InstallerStep for KeyboardStep { } fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { + if state.keyboard_layout.is_empty() { + state.keyboard_layout = if state.language == "tr" { "tr" } else { "us" }.to_string(); + } + ui.heading(t!("keyboard_title")); ui.label(t!("keyboard_description")); ui.add_space(12.0); + // ── Klavye görseli (xkbcommon ile çizilir, temaya uyumlu) ─ + if let Some(kle) = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant) { + let max = egui::Vec2::new(ui.available_width(), ui.available_height().max(100.0)); + crate::ui::kle_svg::draw_layout(ui, &kle, max); + } + + ui.add_space(12.0); + + // ── Layout ve Varyant ComboBox'ları yan yana ────────────── let available_variants: Vec<_> = self .variants .iter() .filter(|(layout, _, _)| layout == &state.keyboard_layout) .collect(); - ui.columns(2, |cols| { - cols[0].vertical(|ui| { - egui::Frame::group(ui.style()) - .fill(theme::c_bg_widget()) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) - .rounding(egui::Rounding::same(8.0)) - .inner_margin(egui::Margin::same(8.0)) - .show(ui, |ui| { - ui.label(t!("keyboard_layout_label")); - ui.add( - egui::TextEdit::singleline(&mut self.layout_filter) - .hint_text(t!("keyboard_layout_search_hint")), - ); - ui.add_space(4.0); + let current_variant_name = if state.keyboard_variant.is_empty() { + t!("keyboard_variant_default").to_string() + } else { + let vn = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants); + if vn.is_empty() { state.keyboard_variant.clone() } else { vn } + }; + let current_layout_name = layout_display_name(&state.keyboard_layout, &self.layouts); - egui::ScrollArea::vertical() - .id_source("kbd_layout_scroll") - .auto_shrink([false, false]) - .max_height(240.0) - .show(ui, |ui| { - let filter = self.layout_filter.to_lowercase(); - let filtered_layouts: Vec<_> = self.layouts.iter() - .filter(|(code, display)| { - filter.is_empty() - || code.to_lowercase().contains(&filter) - || display.to_lowercase().contains(&filter) - }) - .collect(); - - if filtered_layouts.is_empty() { - ui.label(t!("keyboard_layout_search_no_match")); - } - for (code, display) in filtered_layouts { - let is_selected = state.keyboard_layout == *code; - if ui.selectable_label(is_selected, display).clicked() { - state.keyboard_layout = code.to_string(); - self.variant_filter.clear(); - if !self.variants.iter().any(|(layout, _, _)| layout == code) { - state.keyboard_variant.clear(); - } - apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); - } - } - }); - }); + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.add_space(6.0); + ui.label(egui::RichText::new(t!("keyboard_layout_label")).strong().color(theme::c_text())); + }); + ui.add_space(8.0); + let lw = ui.available_width().min(200.0); + theme::show_combo_box(ui, "kbd_layout", current_layout_name.as_str(), lw, |ui, w| { + for (code, display) in &self.layouts { + if theme::selectable_value(ui, &mut state.keyboard_layout, code.clone(), display.as_str(), w).clicked() { + if !self.variants.iter().any(|(layout, _, _)| layout == code) { + state.keyboard_variant.clear(); + } + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + } }); - cols[1].vertical(|ui| { - egui::Frame::group(ui.style()) - .fill(theme::c_bg_widget()) - .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) - .rounding(egui::Rounding::same(8.0)) - .inner_margin(egui::Margin::same(8.0)) - .show(ui, |ui| { - ui.label(t!("keyboard_variant_label")); - ui.add( - egui::TextEdit::singleline(&mut self.variant_filter) - .hint_text(t!("keyboard_variant_search_hint")), - ); - ui.add_space(4.0); + ui.add_space(16.0); - egui::ScrollArea::vertical() - .id_source("kbd_variant_scroll") - .auto_shrink([false, false]) - .max_height(240.0) - .show(ui, |ui| { - let variant_filter = self.variant_filter.to_lowercase(); - let filtered_variants: Vec<_> = available_variants - .into_iter() - .filter(|(_, variant, display)| { - variant_filter.is_empty() - || variant.to_lowercase().contains(&variant_filter) - || display.to_lowercase().contains(&variant_filter) - }) - .collect(); - - let mut selected_default = state.keyboard_variant.is_empty(); - if ui.selectable_value(&mut selected_default, true, t!("keyboard_variant_default")).clicked() { - state.keyboard_variant.clear(); - apply_system_keyboard_layout(&state.keyboard_layout, ""); - } - if filtered_variants.is_empty() { - ui.label(t!("keyboard_variant_search_no_match")); - } - for (_, variant, display) in filtered_variants { - let is_selected = state.keyboard_variant == *variant; - if ui.selectable_label(is_selected, display).clicked() { - state.keyboard_variant = variant.to_string(); - apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); - } - } - }); - }); + ui.vertical(|ui| { + ui.add_space(6.0); + ui.label(egui::RichText::new(t!("keyboard_variant_label")).strong().color(theme::c_text())); + }); + ui.add_space(8.0); + let vw = ui.available_width().min(200.0); + theme::show_combo_box(ui, "kbd_variant", current_variant_name.as_str(), vw, |ui, w| { + let default_variant = String::new(); + if theme::selectable_value(ui, &mut state.keyboard_variant, default_variant, &t!("keyboard_variant_default"), w).clicked() { + apply_system_keyboard_layout(&state.keyboard_layout, ""); + } + for (_, variant, display) in &available_variants { + if theme::selectable_value(ui, &mut state.keyboard_variant, variant.clone(), display.as_str(), w).clicked() { + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + } }); }); ui.add_space(12.0); ui.label(t!("keyboard_test_label")); ui.add_space(4.0); - ui.add( - egui::TextEdit::multiline(&mut self.test_input) - .desired_rows(5) - .desired_width(ui.available_width()) - .hint_text(t!("keyboard_test_hint")), - ); + egui::Frame::group(ui.style()) + .fill(Color32::from_rgb(255, 255, 255)) + .rounding(6.0) + .show(ui, |ui| { + ui.add( + egui::TextEdit::singleline(&mut self.test_input) + .desired_width(ui.available_width()) + .hint_text(t!("keyboard_test_hint")) + .frame(false), + ); + }); ui.add_space(8.0); ui.label(format!("{}: {}", t!("keyboard_selected"), { diff --git a/src/ui/buttons.rs b/src/ui/buttons.rs index 819df12..4d299e9 100644 --- a/src/ui/buttons.rs +++ b/src/ui/buttons.rs @@ -87,12 +87,13 @@ pub fn primary_button(ui: &mut egui::Ui, text: &str) -> egui::Response { pub fn secondary_button(ui: &mut egui::Ui, text: &str) -> egui::Response { gradient_button(ui, text, - Color32::from_rgb(130, 140, 150), - Color32::from_rgb(100, 110, 120), - Color32::from_rgb(158, 158, 175), - Color32::from_rgb(150, 160, 170), - Color32::from_rgb(120, 130, 140), - Color32::from_rgb(100, 110, 120)) + Color32::from_rgb(130, 140, 150), // top + Color32::from_rgb(100, 110, 120), // bottom + Color32::from_rgb(255, 255, 255), // text color32 + Color32::from_rgb(150, 160, 170), // hover top + Color32::from_rgb(120, 130, 140), // hover bottom + Color32::from_rgb(100, 110, 120) // border + ) } pub fn danger_button(ui: &mut egui::Ui, text: &str) -> egui::Response { diff --git a/src/ui/kle_svg.rs b/src/ui/kle_svg.rs new file mode 100644 index 0000000..e9d751c --- /dev/null +++ b/src/ui/kle_svg.rs @@ -0,0 +1,334 @@ +use eframe::egui::{self, Rect, Rounding, Stroke, 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, +} + +#[derive(Debug)] +pub struct XkbLayout { + pub keys: Vec, + pub width: f32, pub height: f32, +} + +/// ISO‑105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden. +/// x,y ondalıklı olabilir (geniş tuşlar için). +fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { + // Satır y offset'leri (birim) + let r = |row| row as f32; + + // Standart ISO‑105 düzeni. x değerleri kümülatif. + match name { + // ── Function row ──────────────── + "ESC" => Some((0.0, r(0), 1.0, 1.0)), + "FK01" => Some((2.0, r(0), 1.0, 1.0)), + "FK02" => Some((3.0, r(0), 1.0, 1.0)), + "FK03" => Some((4.0, r(0), 1.0, 1.0)), + "FK04" => Some((5.0, r(0), 1.0, 1.0)), + "FK05" => Some((6.0, r(0), 1.0, 1.0)), + "FK06" => Some((7.0, r(0), 1.0, 1.0)), + "FK07" => Some((8.0, r(0), 1.0, 1.0)), + "FK08" => Some((9.0, r(0), 1.0, 1.0)), + "FK09" => Some((10.0, r(0), 1.0, 1.0)), + "FK10" => Some((11.0, r(0), 1.0, 1.0)), + "FK11" => Some((12.0, r(0), 1.0, 1.0)), + "FK12" => Some((13.0, r(0), 1.0, 1.0)), + "PRSC" => Some((15.0, r(0), 1.0, 1.0)), + "SCLK" => Some((16.0, r(0), 1.0, 1.0)), + "PAUS" => Some((17.0, r(0), 1.0, 1.0)), + + // ── Number row ───────────────── + "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.0, 1.0)), + + // ── Top alpha row (Q–P) ──────── + "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)), + "BKSL" => Some((14.0, r(2), 1.5, 1.0)), // ISO: solundaki tuş (\|) + "RTRN" => Some((14.0, r(2), 1.5, 2.0)), // ISO Enter (2 satır) + + // ── Home row (A–L) ──────────── + "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)), + + // ── Bottom row (Z–M) ────────── + "LFSH" => Some((0.0, r(4), 1.25, 1.0)), + "LSGT" => Some((1.25, r(4), 1.0, 1.0)), // ISO extra (< > |) + "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)), + "AB11" => Some((12.25,r(4), 1.0, 1.0)), + "RTSH" => Some((13.25,r(4), 2.75, 1.0)), + + // ── Space bar row ───────────── + "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)), + "RWIN" => Some((11.75,r(5), 1.25, 1.0)), + "COMP" => Some((13.0, r(5), 1.25, 1.0)), + "RCTL" => Some((14.25,r(5), 1.25, 1.0)), + + // ── Nav cluster ─────────────── + "INS" => Some((15.5, r(2), 1.0, 1.0)), + "HOME" => Some((16.5, r(2), 1.0, 1.0)), + "PGUP" => Some((17.5, r(2), 1.0, 1.0)), + "DELE" => Some((15.5, r(3), 1.0, 1.0)), + "END" => Some((16.5, r(3), 1.0, 1.0)), + "PGDN" => Some((17.5, r(3), 1.0, 1.0)), + "UP" => Some((16.5, r(4), 1.0, 1.0)), + "LEFT" => Some((15.5, r(5), 1.0, 1.0)), + "DOWN" => Some((16.5, r(5), 1.0, 1.0)), + "RGHT" => Some((17.5, r(5), 1.0, 1.0)), + + // ── Numpad ──────────────────── + "NMLK" => Some((19.0, r(2), 1.0, 1.0)), + "KSLU" => Some((20.0, r(2), 1.0, 1.0)), // / + "KAST" => Some((21.0, r(2), 1.0, 1.0)), // * + "KMIN" => Some((22.0, r(2), 1.0, 1.0)), // - + "KP7" => Some((19.0, r(3), 1.0, 1.0)), + "KP8" => Some((20.0, r(3), 1.0, 1.0)), + "KP9" => Some((21.0, r(3), 1.0, 1.0)), + "KPLU" => Some((22.0, r(3), 1.0, 2.0)), // + + "KP4" => Some((19.0, r(4), 1.0, 1.0)), + "KP5" => Some((20.0, r(4), 1.0, 1.0)), + "KP6" => Some((21.0, r(4), 1.0, 1.0)), + "KP1" => Some((19.0, r(5), 1.0, 1.0)), + "KP2" => Some((20.0, r(5), 1.0, 1.0)), + "KP3" => Some((21.0, r(5), 1.0, 1.0)), + "KP0" => Some((19.0, r(6), 2.0, 1.0)), + "KPDL" => Some((21.0, r(6), 1.0, 1.0)), // . + "KPEN" => Some((22.0, r(5), 1.0, 2.0)), // Enter + "K7" => Some((19.0, r(3), 1.0, 1.0)), // old alias + "K8" => Some((20.0, r(3), 1.0, 1.0)), + "K9" => Some((21.0, r(3), 1.0, 1.0)), + "K4" => Some((19.0, r(4), 1.0, 1.0)), + "K5" => Some((20.0, r(4), 1.0, 1.0)), + "K6" => Some((21.0, r(4), 1.0, 1.0)), + "K1" => Some((19.0, r(5), 1.0, 1.0)), + "K2" => Some((20.0, r(5), 1.0, 1.0)), + "K3" => Some((21.0, r(5), 1.0, 1.0)), + "K0" => Some((19.0, r(6), 2.0, 1.0)), + "KDOT" => Some((21.0, r(6), 1.0, 1.0)), + "KENT" => Some((22.0, r(5), 1.0, 2.0)), + + _ => None, + } +} + +fn keysym_to_label(sym: xkb::Keysym) -> String { + let utf = xkb::keysym_to_utf8(sym); + if !utf.is_empty() { return utf; } + + let name = xkb::keysym_get_name(sym); + if name == "VoidSymbol" || name == "NoSymbol" { + return String::new(); + } + + // Yaygın XKB sembol adlarını kısa etiketlere çevir + match name.as_str() { + "BackSpace" => "⌫".into(), + "Tab" => "⇥".into(), + "Return" => "↵".into(), + "Caps_Lock" => "⇪".into(), + "Shift_L" => "⇧".into(), + "Shift_R" => "⇧".into(), + "Control_L" => "Ctrl".into(), + "Control_R" => "Ctrl".into(), + "Alt_L" => "Alt".into(), + "Alt_R" => "AltGr".into(), + "ISO_Level3_Shift" => "AltGr".into(), + "Super_L" => "Win".into(), + "Super_R" => "Win".into(), + "Menu" => "Menü".into(), + "Escape" => "Esc".into(), + "Delete" => "Del".into(), + "Insert" => "Ins".into(), + "Prior" => "PgUp".into(), + "Next" => "PgDn".into(), + "space" => "".into(), + "Multi_key" => "Compose".into(), + _ if name.starts_with("XF86") => String::new(), + _ => name, + } +} + +/// XKB keymap'inden klavye düzenini oluşturur. +pub fn compile_layout(layout: &str, variant: &str) -> Option { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() { + "pc105" + } else { + "pc105" + }; + + let keymap = xkb::Keymap::new_from_names::( + &context, + "", // rules + model, // model + layout, // layout + if variant.is_empty() { "" } else { variant }, // variant + None::, // options + xkb::COMPILE_NO_FLAGS, + )?; + + let min = keymap.min_keycode(); + let max = keymap.max_keycode(); + let mut keys = Vec::new(); + let mut min_x = f32::MAX; + let mut min_y = f32::MAX; + let mut max_x = f32::MIN; + let mut max_y = f32::MIN; + + 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 }; + let Some((x, y, w, h)) = key_position(name) else { continue }; + + // Sembolleri al (base, shift, altgr, shift+altgr), boş olanları atla + 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::>() + .join(""); + if !label.is_empty() && !raw_labels.contains(&label) { + raw_labels.push(label); + } + } + + // Her tuşta sadece 1 karakter göster (büyük harf tercihli) + let labels: Vec = { + let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect(); + if single.is_empty() { + // Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …) + raw_labels.into_iter().take(1).collect() + } else { + // Büyük harf varsa onu göster, yoksa ilk tek karakteri + 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 x < min_x { min_x = x; } + if y < min_y { min_y = y; } + if x + w > max_x { max_x = x + w; } + if y + h > max_y { max_y = y + h; } + } + + // normalize x,y — tüm tuşlar (0,0) bazlı olsun + for key in &mut keys { + key.x -= min_x; + key.y -= min_y; + } + + Some(XkbLayout { + keys, + width: max_x - min_x, + height: max_y - min_y, + }) +} + +/// Klavyeyi egui ile çizer. +pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) { + let margin = 6.0; + let spacing = 0.08; + let corner = 3.0; + + let uw = (max_size.x - margin * 2.0) / layout.width; + let uh = (max_size.y - margin * 2.0) / layout.height; + let unit = uw.min(uh).max(12.0); + + let draw_w = layout.width * unit; + let draw_h = layout.height * unit; + + let (_id, resp) = ui.allocate_exact_size(Vec2::new(draw_w + margin * 2.0, draw_h + margin * 2.0), egui::Sense::hover()); + let painter = ui.painter_at(resp.rect); + let origin = resp.rect.min; + + let visuals = ui.visuals(); + let key_fill = visuals.widgets.inactive.bg_fill; + let key_stroke = visuals.widgets.noninteractive.bg_stroke; + let text_color = visuals.text_color(); + let bg = visuals.window_fill(); + + painter.rect_filled(resp.rect, Rounding::same(6.0), bg); + + let font_size = (unit * 0.28).max(6.0); + let small_size = (unit * 0.22).max(5.0); + + let sp = unit * spacing; + + for key in &layout.keys { + let kx = origin.x + margin + key.x * unit + sp; + let ky = origin.y + margin + key.y * unit + sp; + let kw = key.w * unit - sp * 2.0; + let kh = key.h * unit - sp * 2.0; + + if kw <= 0.0 || kh <= 0.0 { continue; } + + let key_rect = Rect::from_min_size(egui::pos2(kx, ky), Vec2::new(kw, kh)); + painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color)); + + for (i, label) in key.labels.iter().enumerate() { + let lbl = label.trim(); + if lbl.is_empty() { continue; } + let (lx, ly, sz) = match i { + 0 => (kx + kw * 0.5, ky + kh * 0.3, font_size), + 1 => (kx + kw * 0.5, ky + kh * 0.65, small_size), + _ => (kx + kw * 0.5, ky + kh * 0.9, small_size), + }; + painter.text( + egui::pos2(lx, ly), egui::Align2::CENTER_CENTER, + lbl, egui::FontId::proportional(sz), text_color, + ); + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e176bac..7b2e71a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,7 @@ pub mod theme; pub mod buttons; pub mod combobox; pub mod combobox_stil; +pub mod kle_svg; pub mod slideshow; pub mod error_screen; diff --git a/src/ui/theme.rs b/src/ui/theme.rs index e657dd0..592d47b 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -160,56 +160,7 @@ fn setup_fonts(ctx: &egui::Context) { ctx.set_fonts(fonts); } -// fn build_visuals(is_dark: bool) -> Visuals { -// let mut v = if is_dark { Visuals::dark() } else { Visuals::light() }; -// // Genel arka planlar -// v.window_fill = c_bg_dark(); -// v.panel_fill = c_bg_panel(); -// v.faint_bg_color = c_bg_widget(); -// v.extreme_bg_color = c_sidebar(); -// v.code_bg_color = if is_dark { Color32::from_rgb(0x18, 0x18, 0x28) } else { Color32::from_rgb(0xEE, 0xEE, 0xF5) }; - -// // Metin -// v.override_text_color = None; - -// // Kenarlıklar -// v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, c_border()); -// v.widgets.inactive.bg_stroke = Stroke::new(1.0, c_border()); -// v.widgets.hovered.bg_stroke = Stroke::new(1.5, c_accent()); -// v.widgets.active.bg_stroke = Stroke::new(2.0, c_accent()); - -// // Widget dolguları -// v.widgets.noninteractive.bg_fill = c_bg_widget(); -// v.widgets.inactive.bg_fill = c_bg_widget(); -// v.widgets.hovered.bg_fill = if is_dark { Color32::from_rgb(0x35, 0x35, 0x55) } else { Color32::from_rgb(0xD5, 0xD5, 0xE5) }; -// v.widgets.active.bg_fill = c_accent_dim(); - -// // Metin renkleri -// v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c_text()); -// v.widgets.inactive.fg_stroke = Stroke::new(1.0, c_text()); -// v.widgets.hovered.fg_stroke = Stroke::new(1.5, c_text()); -// v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); - -// // Yuvarlak köşeler -// v.widgets.noninteractive.rounding = Rounding::same(6.0); -// v.widgets.inactive.rounding = Rounding::same(6.0); -// v.widgets.hovered.rounding = Rounding::same(6.0); -// v.widgets.active.rounding = Rounding::same(6.0); - -// // Seçim rengi -// v.selection.bg_fill = c_accent_dim(); -// v.selection.stroke = Stroke::new(1.0, c_accent()); - -// // Hyperlink -// v.hyperlink_color = c_accent(); - -// // Pencere kenarlığı -// v.window_stroke = Stroke::new(1.0, c_border()); -// v.window_rounding = Rounding::same(8.0); - -// v -// } fn build_visuals(is_dark: bool) -> Visuals { let mut v = if is_dark { Visuals::dark() } else { Visuals::light() }; @@ -230,15 +181,14 @@ fn build_visuals(is_dark: bool) -> Visuals { // ─── WIDGET DOLGULARI (Burası ComboBox ve Butonların Kaderini Belirliyor) ─────── v.widgets.noninteractive.bg_fill = c_bg_widget(); - // Pasif Hali: Koyu widget arka planı - //v.widgets.inactive.bg_fill = Color32::from_rgb(40, 43, 52); - //v.widgets.inactive.bg_fill = Color32::from_rgb(224, 224, 224); + // Pasif Hali: Widget arka planı (TextEdit, ComboBox giriş alanı) + v.widgets.inactive.bg_fill = Color32::from_rgb(255, 255, 255); // Üzerine Gelme Hali: Hafif açık v.widgets.hovered.bg_fill = Color32::from_rgb(55, 58, 68); // Tıklanma/Açık Kalma Hali: Daha açık - v.widgets.active.bg_fill = Color32::from_rgb(60, 63, 75); + v.widgets.active.bg_fill = Color32::WHITE; // ComboBox stilleri (combobox_stil.rs) crate::ui::combobox_stil::apply_visuals(&mut v);