refactor: extract ComboBox styling into dedicated module, add location step

- Create src/ui/combobox.rs with helper functions (show_combo_box, selectable_value, selectable_value_custom)
- Create src/ui/combobox_stil.rs with ComboBox visual constants (weak_bg_fill, text/border colors)
- Move theme.rs ComboBox visuals (weak_bg_fill, fg_stroke, bg_stroke) into combobox_stil::apply_visuals
- Move LocationStep from installer.rs to src/steps/location.rs
- Re-register LocationStep in steps/mod.rs
- Update all ComboBox usages across steps (bootloader, display_manager, partition, funct) to use theme::show_combo_box / selectable_value
- Replace manual mesh drawing in language selector with combobox helper
- Add region/timezone i18n keys
- Vertical-align region/zone labels to match ComboBox height
This commit is contained in:
Erkan IŞIK
2026-06-17 10:00:10 +03:00
parent 25226c0f4f
commit ccc851d79f
14 changed files with 547 additions and 340 deletions
+1
View File
@@ -401,6 +401,7 @@ job_media_check = "Verifying installation media"
# ── CLI / Auto-install ─────────────────────────────────────
hostname = "Hostname"
timezone = "Timezone"
region = "Region"
username = "Username"
# ── Slideshow (fallback) ───────────────────────────────────
+1
View File
@@ -422,6 +422,7 @@ lvm_lv_action = "İşlem"
# ── CLI / Otomatik Kurulum ─────────────────────────────────
hostname = "Bilgisayar adı"
timezone = "Zaman dilimi"
region = "Bölge"
username = "Kullanıcı adı"
# ── Slayt Gösterisi (yedek) ────────────────────────────────
+8 -35
View File
@@ -101,36 +101,9 @@ pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::instal
("en", "English", eframe::egui::include_image!("../assets/flags/us.svg")),
];
// Standart egui makrosu - Tema motoru sayesinde otomatik olarak oval ve renk geçişli hissi verir
eframe::egui::ComboBox::from_id_source("language_select_combo")
.selected_text(current_lang_label)
.width(available_width) // Seçim kutusunu yatayda genişletir
.show_ui(ui, |ui| {
// Açılan alt listenin genişliğini de üst butonla eşitliyoruz (HTML SelectBox Mantığı)
ui.set_min_width(available_width);
ui.set_max_width(available_width);
for (code, label, image_source) in languages.iter() {
let item_height = 28.0;
// Eleman kutularını sarmalayarak tam genişlikte tıklanabilir yapıyoruz
let (rect, item_resp) = ui.allocate_exact_size(
egui::vec2(available_width, item_height),
egui::Sense::click(),
);
{
use egui::epaint::Mesh;
let top = egui::Color32::from_rgb(249, 249, 249);
let bot = egui::Color32::from_rgb(233, 233, 233);
let mut mesh = Mesh::default();
let idx = mesh.vertices.len() as u32;
mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.left(), rect.top()), uv: egui::pos2(0.0, 0.0), color: top });
mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.right(), rect.top()), uv: egui::pos2(0.0, 0.0), color: top });
mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.right(), rect.bottom()), uv: egui::pos2(0.0, 0.0), color: bot });
mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.left(), rect.bottom()), uv: egui::pos2(0.0, 0.0), color: bot });
mesh.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx + 2, idx + 3, idx]);
ui.painter().add(egui::Shape::mesh(mesh));
}
crate::ui::theme::show_combo_box(ui, "language_select_combo", current_lang_label, available_width, |ui, w| {
for (code, label, image_source) in languages.iter() {
let resp = crate::ui::theme::selectable_value_custom(ui, w, |ui, rect| {
ui.put(rect, |ui: &mut egui::Ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
@@ -139,11 +112,11 @@ pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::instal
ui.label(*label);
}).response
});
if item_resp.clicked() {
state.language = code.to_string();
}
item_resp.on_hover_cursor(egui::CursorIcon::PointingHand);
});
if resp.clicked() {
state.language = code.to_string();
}
});
}
});
});
}
-205
View File
@@ -510,212 +510,7 @@ impl InstallerStep for WelcomeStep {
}
}
// ─────────────────────────────────────────────
// LOCATION STEP
// ─────────────────────────────────────────────
pub struct LocationStep {
timezones: Vec<String>,
geoip_done: bool,
datetime_init_done: bool,
}
impl Default for LocationStep {
fn default() -> Self {
let tz_list = vec![
"Africa/Cairo", "America/Argentina/Buenos_Aires", "America/New_York",
"America/Sao_Paulo", "Asia/Baku", "Asia/Dubai", "Asia/Istanbul",
"Asia/Tokyo", "Australia/Sydney", "Europe/Berlin", "Europe/Istanbul",
"Europe/London", "Europe/Moscow", "Europe/Paris", "Europe/Rome",
"Pacific/Auckland",
].iter().map(|s| s.to_string()).collect();
Self {
timezones: tz_list,
geoip_done: false,
datetime_init_done: false,
}
}
}
impl LocationStep {
fn geoip_lookup(state: &mut GlobalState) {
let out = std::process::Command::new("curl")
.args(["-s", "--connect-timeout", "3", "http://ip-api.com/json"])
.output();
if let Ok(o) = out {
if let Ok(text) = String::from_utf8(o.stdout) {
let extract = |key: &str| -> Option<String> {
let pat = format!("\"{}\":\"", key);
text.find(&pat).and_then(|pos| {
let start = pos + pat.len();
text[start..].find('"').map(|end| text[start..start+end].to_string())
})
};
if let Some(country) = extract("countryCode") {
match country.as_str() {
"TR" => state.language = "tr".to_string(),
"DE" => state.language = "de".to_string(),
"FR" => state.language = "fr".to_string(),
"IT" => state.language = "it".to_string(),
"ES" => state.language = "es".to_string(),
"RU" => state.language = "ru".to_string(),
"NL" => state.language = "nl".to_string(),
"PL" => state.language = "pl".to_string(),
_ => {}
}
}
if let Some(tz) = extract("timezone") {
if !tz.is_empty() {
state.timezone = tz;
}
}
}
}
}
fn init_from_system(&mut self, state: &mut GlobalState) {
use chrono::{Datelike, Timelike};
let now_str = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&now_str, "%Y-%m-%d %H:%M") {
state.manual_year = dt.year();
state.manual_month = dt.month();
state.manual_day = dt.day();
state.manual_hour = dt.hour();
state.manual_minute = dt.minute();
}
self.datetime_init_done = true;
}
}
impl InstallerStep for LocationStep {
fn name(&self) -> String {
format!("{} / {}", t!("location"), t!("datetime"))
}
fn on_enter(&mut self, state: &mut GlobalState) {
if !self.geoip_done {
Self::geoip_lookup(state);
self.geoip_done = true;
}
if !self.datetime_init_done {
self.init_from_system(state);
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
use crate::ui::theme;
// --- BÖLGE VE ZAMAN DİLİMİ (Region & Timezone) ---
theme::section_heading(ui, &t!("location_title"));
ui.label(
egui::RichText::new(t!("location_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new(t!("location_title")).strong().color(theme::c_text()));
let current_tz = if state.timezone.is_empty() {
"Europe/Istanbul".to_string()
} else {
state.timezone.clone()
};
if state.timezone.is_empty() {
state.timezone = current_tz.clone();
}
egui::ComboBox::from_id_source("timezone_select")
.selected_text(&current_tz)
.width(280.0)
.show_ui(ui, |ui| {
for tz in &self.timezones {
ui.selectable_value(&mut state.timezone, tz.clone(), tz);
}
});
});
ui.add_space(16.0);
ui.separator();
ui.add_space(16.0);
// --- TARİH VE SAAT AYARLARI (Date & Time) ---
theme::section_heading(ui, &t!("datetime_title"));
ui.label(
egui::RichText::new(t!("datetime_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("datetime_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// NTP
ui.label(egui::RichText::new(t!("datetime_ntp_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::Checkbox::new(&mut state.use_ntp, ""));
let status = if state.use_ntp { t!("datetime_ntp_on") } else { t!("datetime_ntp_off") };
ui.colored_label(
if state.use_ntp { theme::c_success() } else { theme::c_error() },
status,
);
});
ui.end_row();
if !state.use_ntp {
// Tarih
ui.label(egui::RichText::new(t!("datetime_date_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.label(t!("datetime_year"));
ui.add(egui::DragValue::new(&mut state.manual_year).clamp_range(2000..=2100).speed(1));
ui.label(t!("datetime_month"));
ui.add(egui::DragValue::new(&mut state.manual_month).clamp_range(1..=12).speed(1));
ui.label(t!("datetime_day"));
ui.add(egui::DragValue::new(&mut state.manual_day).clamp_range(1..=31).speed(1));
});
ui.end_row();
// Saat
ui.label(egui::RichText::new(t!("datetime_time_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut state.manual_hour).clamp_range(0..=23).speed(1));
ui.label(":");
ui.add(egui::DragValue::new(&mut state.manual_minute).clamp_range(0..=59).speed(1));
});
ui.end_row();
}
});
});
ui.add_space(8.0);
if state.use_ntp {
ui.label(
egui::RichText::new(t!("datetime_ntp_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
} else {
ui.label(
egui::RichText::new(t!("datetime_manual_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
!state.timezone.is_empty()
}
}
/// Mevcut bir Linux kurulumu olup olmadığını kontrol eder.
/// Basitçe /mnt/etc/fstab varlığına veya blkid ile Linux bölümlerine bakar.
+2 -2
View File
@@ -6,8 +6,8 @@ mod autoinstall;
mod branding;
mod funct;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
use installer::{GlobalState, InstallerStep, WelcomeStep};
use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
use eframe::egui;
use rust_i18n::t;
+6 -11
View File
@@ -1,6 +1,7 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
#[derive(Default)]
pub struct BootloaderStep {
@@ -47,17 +48,11 @@ impl InstallerStep for BootloaderStep {
} else {
state.bootloader_device.clone()
};
egui::ComboBox::from_id_source("boot_device_select")
.selected_text(&current)
.show_ui(ui, |ui| {
for dev in &device_list {
ui.selectable_value(
&mut state.bootloader_device,
dev.clone(),
dev,
);
}
});
theme::show_combo_box(ui, "boot_device_select", current.as_str(), ui.available_width(), |ui, w| {
for dev in &device_list {
theme::selectable_value(ui, &mut state.bootloader_device, dev.clone(), dev.as_str(), w);
}
});
ui.label(
egui::RichText::new(t!("bootloader_device_hint"))
.size(11.0)
+15 -25
View File
@@ -1,6 +1,7 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"];
const DE_OPTIONS: &[&str] = &["plasma", "xfce", "gnome", "cinnamon", "budgie", "lxde", "lxqt", "mate", "lumina"];
@@ -31,37 +32,26 @@ impl InstallerStep for DisplayManagerStep {
.show(ui, |ui| {
// Ekran yöneticisi
ui.label(t!("display_manager_select"));
egui::ComboBox::from_id_source("dm_combo")
.selected_text(state.display_manager.as_str())
.show_ui(ui, |ui| {
for dm in DM_OPTIONS {
ui.selectable_value(
&mut state.display_manager,
dm.to_string(),
*dm,
);
}
});
let dm_current = state.display_manager.clone();
theme::show_combo_box(ui, "dm_combo", dm_current.as_str(), ui.available_width(), |ui, w| {
for dm in DM_OPTIONS {
theme::selectable_value(ui, &mut state.display_manager, dm.to_string(), *dm, w);
}
});
ui.end_row();
// Masaüstü ortamı (seçilebilir)
ui.label(t!("display_manager_de"));
let current_de = if state.desktop_environment.is_empty() || state.desktop_environment == "default" {
"plasma"
let de_current = if state.desktop_environment.is_empty() || state.desktop_environment == "default" {
"plasma".to_string()
} else {
state.desktop_environment.as_str()
state.desktop_environment.clone()
};
egui::ComboBox::from_id_source("de_combo")
.selected_text(current_de)
.show_ui(ui, |ui| {
for de in DE_OPTIONS {
ui.selectable_value(
&mut state.desktop_environment,
de.to_string(),
*de,
);
}
});
theme::show_combo_box(ui, "de_combo", de_current.as_str(), ui.available_width(), |ui, w| {
for de in DE_OPTIONS {
theme::selectable_value(ui, &mut state.desktop_environment, de.to_string(), *de, w);
}
});
ui.end_row();
// Otomatik oturum açma
+317
View File
@@ -0,0 +1,317 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
pub struct LocationStep {
pub regions: Vec<String>,
pub zones_by_region: std::collections::HashMap<String, Vec<String>>,
pub geoip_done: bool,
pub datetime_init_done: bool,
}
fn load_timezones() -> Vec<String> {
if let Ok(out) = std::process::Command::new("timedatectl")
.args(["list-timezones"])
.output()
{
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
let zones: Vec<String> = text
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if !zones.is_empty() {
return zones;
}
}
}
if let Ok(text) = std::fs::read_to_string("/usr/share/zoneinfo/zone.tab") {
let zones: Vec<String> = text
.lines()
.filter(|l| !l.starts_with('#') && !l.trim().is_empty())
.filter_map(|l| l.split('\t').nth(2))
.map(|s| s.to_string())
.collect();
if !zones.is_empty() {
return zones;
}
}
vec![
"Africa/Cairo", "Africa/Casablanca", "Africa/Johannesburg", "Africa/Lagos",
"Africa/Nairobi", "America/Argentina/Buenos_Aires", "America/Bogota",
"America/Caracas", "America/Chicago", "America/Denver", "America/Lima",
"America/Los_Angeles", "America/Mexico_City", "America/New_York",
"America/Panama", "America/Sao_Paulo", "America/Santiago", "America/Toronto",
"America/Vancouver", "Asia/Almaty", "Asia/Baghdad", "Asia/Baku",
"Asia/Bangkok", "Asia/Dubai", "Asia/Ho_Chi_Minh", "Asia/Hong_Kong",
"Asia/Istanbul", "Asia/Jakarta", "Asia/Jerusalem", "Asia/Kabul",
"Asia/Karachi", "Asia/Kathmandu", "Asia/Kolkata", "Asia/Kuala_Lumpur",
"Asia/Manila", "Asia/Riyadh", "Asia/Seoul", "Asia/Shanghai",
"Asia/Singapore", "Asia/Taipei", "Asia/Tashkent", "Asia/Tehran",
"Asia/Tokyo", "Asia/Yangon", "Australia/Adelaide", "Australia/Brisbane",
"Australia/Melbourne", "Australia/Perth", "Australia/Sydney",
"Europe/Amsterdam", "Europe/Athens", "Europe/Berlin", "Europe/Brussels",
"Europe/Bucharest", "Europe/Copenhagen", "Europe/Dublin",
"Europe/Helsinki", "Europe/Istanbul", "Europe/Kyiv", "Europe/Lisbon",
"Europe/London", "Europe/Madrid", "Europe/Moscow", "Europe/Oslo",
"Europe/Paris", "Europe/Prague", "Europe/Rome", "Europe/Stockholm",
"Europe/Vienna", "Europe/Warsaw", "Europe/Zurich",
"Pacific/Auckland", "Pacific/Fiji", "Pacific/Honolulu",
"Pacific/Port_Moresby",
].iter().map(|s| s.to_string()).collect()
}
fn build_tz_data(zones: &[String]) -> (Vec<String>, std::collections::HashMap<String, Vec<String>>) {
let mut regions_set = std::collections::BTreeSet::new();
let mut zones_by_region: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
for tz in zones {
if let Some(slash) = tz.find('/') {
let region = tz[..slash].to_string();
let zone = tz[slash + 1..].to_string();
regions_set.insert(region.clone());
zones_by_region.entry(region).or_default().push(zone);
}
}
for zones in zones_by_region.values_mut() {
zones.sort();
}
let regions: Vec<String> = regions_set.into_iter().collect();
(regions, zones_by_region)
}
impl Default for LocationStep {
fn default() -> Self {
let all_zones = load_timezones();
let (regions, zones_by_region) = build_tz_data(&all_zones);
Self {
regions,
zones_by_region,
geoip_done: false,
datetime_init_done: false,
}
}
}
impl LocationStep {
fn geoip_lookup(state: &mut GlobalState) {
let out = std::process::Command::new("curl")
.args(["-s", "--connect-timeout", "3", "http://ip-api.com/json"])
.output();
if let Ok(o) = out {
if let Ok(text) = String::from_utf8(o.stdout) {
let extract = |key: &str| -> Option<String> {
let pat = format!("\"{}\":\"", key);
text.find(&pat).and_then(|pos| {
let start = pos + pat.len();
text[start..].find('"').map(|end| text[start..start+end].to_string())
})
};
if let Some(country) = extract("countryCode") {
match country.as_str() {
"TR" => state.language = "tr".to_string(),
"DE" => state.language = "de".to_string(),
"FR" => state.language = "fr".to_string(),
"IT" => state.language = "it".to_string(),
"ES" => state.language = "es".to_string(),
"RU" => state.language = "ru".to_string(),
"NL" => state.language = "nl".to_string(),
"PL" => state.language = "pl".to_string(),
_ => {}
}
}
if let Some(tz) = extract("timezone") {
if !tz.is_empty() {
state.timezone = tz;
}
}
}
}
}
fn init_from_system(&mut self, state: &mut GlobalState) {
use chrono::{Datelike, Timelike};
let now_str = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&now_str, "%Y-%m-%d %H:%M") {
state.manual_year = dt.year();
state.manual_month = dt.month();
state.manual_day = dt.day();
state.manual_hour = dt.hour();
state.manual_minute = dt.minute();
}
self.datetime_init_done = true;
}
fn split_tz(tz: &str) -> (String, String) {
if let Some(slash) = tz.find('/') {
(tz[..slash].to_string(), tz[slash + 1..].to_string())
} else {
("Other".to_string(), tz.to_string())
}
}
}
impl InstallerStep for LocationStep {
fn name(&self) -> String {
format!("{} / {}", t!("location"), t!("datetime"))
}
fn on_enter(&mut self, state: &mut GlobalState) {
if !self.geoip_done {
Self::geoip_lookup(state);
self.geoip_done = true;
}
if !self.datetime_init_done {
self.init_from_system(state);
}
if state.timezone.is_empty() {
state.timezone = "Europe/Istanbul".to_string();
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
use crate::ui::theme;
theme::section_heading(ui, &t!("location_title"));
ui.label(
egui::RichText::new(t!("location_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
let (mut cur_region, mut cur_zone) = Self::split_tz(&state.timezone);
if !self.regions.contains(&cur_region) {
if let Some(first) = self.regions.first() {
cur_region = first.clone();
if let Some(zones) = self.zones_by_region.get(&cur_region) {
cur_zone = zones.first().cloned().unwrap_or_default();
}
state.timezone = format!("{}/{}", cur_region, cur_zone);
}
}
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.add_space(6.0);
ui.label(egui::RichText::new(t!("region")).strong().color(theme::c_text()));
});
ui.add_space(4.0);
let rw = 140.0_f32.min(ui.available_width() / 3.0);
let region_text = cur_region.clone();
theme::show_combo_box(ui, "tz_region_select", region_text.as_str(), rw, |ui, w| {
for region in &self.regions {
if theme::selectable_value(ui, &mut cur_region, region.clone(), region.as_str(), w).clicked() {
if let Some(zones) = self.zones_by_region.get(&cur_region) {
cur_zone = zones.first().cloned().unwrap_or_default();
}
state.timezone = format!("{}/{}", cur_region, cur_zone);
}
}
});
ui.add_space(16.0);
ui.vertical(|ui| {
ui.add_space(6.0);
ui.label(egui::RichText::new(t!("timezone")).strong().color(theme::c_text()));
});
ui.add_space(4.0);
let zw = ui.available_width().max(160.0);
if let Some(zones) = self.zones_by_region.get(&cur_region) {
if !zones.contains(&cur_zone) {
cur_zone = zones.first().cloned().unwrap_or_default();
state.timezone = format!("{}/{}", cur_region, cur_zone);
}
let zone_text = cur_zone.clone();
theme::show_combo_box(ui, "tz_zone_select", zone_text.as_str(), zw, |ui, w| {
for zone in zones {
if theme::selectable_value(ui, &mut cur_zone, zone.clone(), zone.as_str(), w).clicked() {
state.timezone = format!("{}/{}", cur_region, cur_zone);
}
}
});
}
});
ui.add_space(16.0);
ui.separator();
ui.add_space(16.0);
theme::section_heading(ui, &t!("datetime_title"));
ui.label(
egui::RichText::new(t!("datetime_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
egui::Frame::group(ui.style())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("datetime_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
ui.label(egui::RichText::new(t!("datetime_ntp_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::Checkbox::new(&mut state.use_ntp, ""));
let status = if state.use_ntp { t!("datetime_ntp_on") } else { t!("datetime_ntp_off") };
ui.colored_label(
if state.use_ntp { theme::c_success() } else { theme::c_error() },
status,
);
});
ui.end_row();
if !state.use_ntp {
ui.label(egui::RichText::new(t!("datetime_date_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.label(t!("datetime_year"));
ui.add(egui::DragValue::new(&mut state.manual_year).clamp_range(2000..=2100).speed(1));
ui.label(t!("datetime_month"));
ui.add(egui::DragValue::new(&mut state.manual_month).clamp_range(1..=12).speed(1));
ui.label(t!("datetime_day"));
ui.add(egui::DragValue::new(&mut state.manual_day).clamp_range(1..=31).speed(1));
});
ui.end_row();
ui.label(egui::RichText::new(t!("datetime_time_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut state.manual_hour).clamp_range(0..=23).speed(1));
ui.label(":");
ui.add(egui::DragValue::new(&mut state.manual_minute).clamp_range(0..=59).speed(1));
});
ui.end_row();
}
});
});
ui.add_space(8.0);
if state.use_ntp {
ui.label(
egui::RichText::new(t!("datetime_ntp_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
} else {
ui.label(
egui::RichText::new(t!("datetime_manual_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
!state.timezone.is_empty()
}
}
+2
View File
@@ -9,6 +9,7 @@ pub mod network;
pub mod rescue;
pub mod display_manager;
pub mod netinstall;
pub mod location;
pub use keyboard::KeyboardStep;
pub use users::UsersStep;
@@ -20,3 +21,4 @@ pub use bootloader::BootloaderStep;
pub use network::NetworkStep;
pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep;
pub use location::LocationStep;
+28 -47
View File
@@ -2,6 +2,7 @@
use eframe::egui;
use rust_i18n::t;
use crate::ui::theme;
// sysinfo::Disks mount point başına bir kayıt döndürür (partition'lar),
// ham disk listesi için lsblk kullanıyoruz.
@@ -892,15 +893,12 @@ impl PartitionStep {
// Dosya sistemi (boyut önerisi ile)
ui.label(t!("mp_fstype"));
let prev_fs = self.manual.add_fstype;
egui::ComboBox::from_id_source("fs_combo")
.selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_fstype, i, *label
);
}
});
let fs_label = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?");
theme::show_combo_box(ui, "fs_combo", fs_label, ui.available_width(), |ui, w| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
theme::selectable_value(ui, &mut self.manual.add_fstype, i, *label, w);
}
});
// FS tipi değişince akıllı boyut öner
if prev_fs != self.manual.add_fstype && self.manual.add_size_str.is_empty() {
let fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1);
@@ -925,16 +923,11 @@ impl PartitionStep {
} else {
self.manual.add_mountpoint.clone()
};
egui::ComboBox::from_id_source("mp_combo")
.selected_text(&current_mp)
.width(120.0)
.show_ui(ui, |ui| {
for mp in COMMON_MOUNTPOINTS {
if ui.selectable_label(false, *mp).clicked() {
self.manual.add_mountpoint = mp.to_string();
}
}
});
theme::show_combo_box(ui, "mp_combo", &current_mp, 120.0, |ui, w| {
for mp in COMMON_MOUNTPOINTS {
theme::selectable_value(ui, &mut self.manual.add_mountpoint, mp.to_string(), *mp, w);
}
});
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
@@ -1143,15 +1136,11 @@ impl PartitionStep {
let current_vg = state.volume_groups.get(self.manual.add_lv_vg_idx)
.map(|vg| vg.name.as_str())
.unwrap_or("?");
egui::ComboBox::from_id_source("lv_vg_combo")
.selected_text(current_vg)
.show_ui(ui, |ui| {
for (vi, vg) in state.volume_groups.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_lv_vg_idx, vi, &vg.name
);
}
});
theme::show_combo_box(ui, "lv_vg_combo", current_vg, ui.available_width(), |ui, w| {
for (vi, vg) in state.volume_groups.iter().enumerate() {
theme::selectable_value(ui, &mut self.manual.add_lv_vg_idx, vi, vg.name.as_str(), w);
}
});
ui.end_row();
// LV Adı
@@ -1161,15 +1150,12 @@ impl PartitionStep {
// Dosya sistemi
ui.label(t!("mp_fstype"));
egui::ComboBox::from_id_source("lv_fs_combo")
.selected_text(FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.0).unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_lv_fstype, i, *label
);
}
});
let lv_fs_label = FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.0).unwrap_or("?");
theme::show_combo_box(ui, "lv_fs_combo", lv_fs_label, ui.available_width(), |ui, w| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
theme::selectable_value(ui, &mut self.manual.add_lv_fstype, i, *label, w);
}
});
ui.end_row();
// Boyut
@@ -1185,16 +1171,11 @@ impl PartitionStep {
} else {
self.manual.add_lv_mountpoint.clone()
};
egui::ComboBox::from_id_source("lv_mp_combo")
.selected_text(&current_mp)
.width(120.0)
.show_ui(ui, |ui| {
for mp in COMMON_MOUNTPOINTS {
if ui.selectable_label(false, *mp).clicked() {
self.manual.add_lv_mountpoint = mp.to_string();
}
}
});
theme::show_combo_box(ui, "lv_mp_combo", &current_mp, 120.0, |ui, w| {
for mp in COMMON_MOUNTPOINTS {
theme::selectable_value(ui, &mut self.manual.add_lv_mountpoint, mp.to_string(), *mp, w);
}
});
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
+85
View File
@@ -0,0 +1,85 @@
use eframe::egui::{self, Rect, Vec2};
use super::combobox_stil::{draw_gradient_bg, DROPDOWN_TEXT_COLOR};
/// Stil uygulanmış ComboBox oluşturur (genişlik ayarı yapılmaz, Grid içinde kullanım için).
pub fn combo_box(
id_source: impl std::hash::Hash,
selected_text: impl Into<egui::WidgetText>,
) -> egui::ComboBox {
egui::ComboBox::from_id_source(id_source).selected_text(selected_text)
}
/// Stil uygulanmış ComboBox oluşturur ve genişlik verir.
pub fn combo_box_with_width(
id_source: impl std::hash::Hash,
selected_text: impl Into<egui::WidgetText>,
width: f32,
) -> egui::ComboBox {
combo_box(id_source, selected_text).width(width)
}
/// ComboBox dropdown'ını açar, genişlik kısıtlaması uygular ve içeriği çizer.
pub fn show_combo_box<R>(
ui: &mut egui::Ui,
id_source: impl std::hash::Hash,
selected_text: impl Into<egui::WidgetText>,
width: f32,
add_contents: impl FnOnce(&mut egui::Ui, f32) -> R,
) -> Option<R> {
combo_box_with_width(id_source, selected_text, width)
.show_ui(ui, |ui| {
ui.set_min_width(width);
ui.set_max_width(width);
add_contents(ui, width)
})
.inner
}
/// ComboBox dropdown öğesi — gradient arkaplan, tam genişlik tıklanabilir, pointing hand imleci.
pub fn selectable_value<T: PartialEq>(
ui: &mut egui::Ui,
current: &mut T,
value: T,
label: &str,
width: f32,
) -> egui::Response {
let item_height = 28.0;
let (rect, response) = ui.allocate_exact_size(Vec2::new(width, item_height), egui::Sense::click());
draw_gradient_bg(ui, rect);
ui.put(rect, |ui: &mut egui::Ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.colored_label(DROPDOWN_TEXT_COLOR, label);
})
.response
});
if response.clicked() {
*current = value;
}
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
/// ComboBox dropdown öğesi — özel içerik çizmek için (ör. bayrak + metin).
/// `add_contents` callback'i verilen `Rect` içine içerik ekler.
pub fn selectable_value_custom<R>(
ui: &mut egui::Ui,
width: f32,
add_contents: impl FnOnce(&mut egui::Ui, Rect) -> R,
) -> egui::Response {
let item_height = 28.0;
let (rect, mut response) = ui.allocate_exact_size(Vec2::new(width, item_height), egui::Sense::click());
draw_gradient_bg(ui, rect);
add_contents(ui, rect);
if response.clicked() {
response.mark_changed();
}
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
+65
View File
@@ -0,0 +1,65 @@
use eframe::egui::{self, Color32, Rect, Stroke};
use egui::epaint::Mesh;
// ─── ComboBox Popup Renk Sabitleri ──────────────────────────
pub const WEAK_BG_INACTIVE: Color32 = Color32::from_rgb(224, 224, 224);
pub const WEAK_BG_HOVERED: Color32 = Color32::from_rgb(200, 200, 200);
pub const WEAK_BG_ACTIVE: Color32 = Color32::from_rgb(180, 180, 180);
// ─── Gradient Dropdown Öğesi Renkleri ───────────────────────
pub const GRADIENT_TOP: Color32 = Color32::from_rgb(249, 249, 249);
pub const GRADIENT_BOT: Color32 = Color32::from_rgb(233, 233, 233);
// ─── ComboBox Metin Renkleri ────────────────────────────────
/// Seçili metin rengi (ComboBox kapalıyken widget üzerinde görünen metin)
pub const COMBO_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50);
/// Açılır listedeki öğelerin metin rengi
pub const DROPDOWN_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50);
// ─── ComboBox Kenarlık (border) Renkleri ────────────────────
/// Üzerine gelindiğinde / tıklanınca kenarlık rengi
pub const COMBO_BORDER_ACCENT: Color32 = Color32::from_rgb(155, 155, 155);
/// Verilen `Visuals` yapısına ComboBox stillerini uygular.
pub fn apply_visuals(v: &mut egui::Visuals) {
v.widgets.inactive.weak_bg_fill = WEAK_BG_INACTIVE;
v.widgets.hovered.weak_bg_fill = WEAK_BG_HOVERED;
v.widgets.active.weak_bg_fill = WEAK_BG_ACTIVE;
v.widgets.inactive.fg_stroke = Stroke::new(1.0, COMBO_TEXT_COLOR);
v.widgets.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE);
v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE);
// Kenarlıklar
v.widgets.inactive.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
v.widgets.hovered.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
v.widgets.active.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
}
/// Açık gri gradyan mesh arkaplanı çizer (dropdown öğeleri için).
pub fn draw_gradient_bg(ui: &mut egui::Ui, rect: Rect) {
let mut mesh = Mesh::default();
let idx = mesh.vertices.len() as u32;
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.top()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.top()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.bottom()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_BOT,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.bottom()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_BOT,
});
mesh.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx + 2, idx + 3, idx]);
ui.painter().add(egui::Shape::mesh(mesh));
}
+2
View File
@@ -1,5 +1,7 @@
pub mod theme;
pub mod buttons;
pub mod combobox;
pub mod combobox_stil;
pub mod slideshow;
pub mod error_screen;
+15 -15
View File
@@ -64,6 +64,7 @@ pub fn set_theme_mode(is_dark: bool) {
TEXT_DIM.store(0x000000, Ordering::Relaxed);
SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed);
// BG_DARK.store(0xF0F0F5, Ordering::Relaxed);
// BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
// BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
@@ -92,8 +93,8 @@ pub fn c_accent_dim() -> Color32 {
/// `cc.egui_ctx` üzerinden `setup_custom_fonts` ve `set_visuals` çağrısını birleştirir.
pub fn apply(ctx: &egui::Context, is_dark: bool) {
setup_fonts(ctx);
ctx.set_visuals(build_visuals(is_dark));
ctx.set_style(build_style());
ctx.set_visuals(build_visuals(is_dark));
}
fn setup_fonts(ctx: &egui::Context) {
@@ -225,27 +226,25 @@ fn build_visuals(is_dark: bool) -> Visuals {
// ─── KENARLIKLAR (Calamares Uyumu için İnce İnce Yumuşatıldı) ───────────────────
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, c_border());
v.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 30)); // Buton etrafında hafif saydam parlaklık
v.widgets.hovered.bg_stroke = Stroke::new(1.5, c_accent());
v.widgets.active.bg_stroke = Stroke::new(2.0, c_accent());
// ─── WIDGET DOLGULARI (Burası ComboBox ve Butonların Kaderini Belirliyor) ───────
v.widgets.noninteractive.bg_fill = c_bg_widget();
// Pasif Hali: Calamares Mavisi (İstiyorsan c_accent() ile Pisi Vişne Rengine de Çekebilirsin)
v.widgets.inactive.bg_fill = Color32::from_rgb(46, 124, 219);
// 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);
// Üzerine Gelme Hali: Parlayan Açık Mavi
v.widgets.hovered.bg_fill = Color32::from_rgb(59, 144, 240);
// Üzerine Gelme Hali: Hafif açık
v.widgets.hovered.bg_fill = Color32::from_rgb(55, 58, 68);
// Tıklanma/Açık Kalma Hali: Koyu Lacivert/Mavi
v.widgets.active.bg_fill = Color32::from_rgb(20, 70, 140);
// Tıklanma/Açık Kalma Hali: Daha açık
v.widgets.active.bg_fill = Color32::from_rgb(60, 63, 75);
// ComboBox stilleri (combobox_stil.rs)
crate::ui::combobox_stil::apply_visuals(&mut v);
// ─── METİN RENKLERİ ────────────────────────────────────────────────────────────
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c_text());
v.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::WHITE); // Mavi üstüne beyaz metin okunaklı kılar
v.widgets.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE);
v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE);
// ─── YUVARLAK KÖŞELER (Zaten 6.0'dı, Tam İstediğin Calamares Kıvamında) ─────────
v.widgets.noninteractive.rounding = Rounding::same(6.0);
@@ -264,6 +263,8 @@ fn build_visuals(is_dark: bool) -> Visuals {
v.window_stroke = Stroke::new(1.0, c_border());
v.window_rounding = Rounding::same(8.0);
v.interact_cursor = Some(egui::CursorIcon::PointingHand);
v
}
@@ -286,14 +287,13 @@ fn build_style() -> egui::Style {
]
.into();
style.visuals.interact_cursor = Some(egui::CursorIcon::PointingHand);
style
}
// ─── Yardımcı widget'lar ─────────────────────────────────────────
pub use super::buttons::*;
pub use super::combobox::*;
/// Bölüm başlığı — büyük, açık renkli, alt çizgisiz.
pub fn section_heading(ui: &mut egui::Ui, text: &str) {