feat: gradient buttons, language selector with flags, SVG support, and UI improvements

- Add linear gradient backgrounds to primary/secondary/danger buttons
- Add language selector with flag icons in ComboBox
- Add SVG support via egui_extras feature
- Add cancel confirmation dialog on exit
- Update Yali logo to SVG icon
- Add flag SVGs for all countries
- Improve users step layout with form helpers
- Refactor button API (ui::theme::*_button now takes ui param)
This commit is contained in:
Erkan IŞIK
2026-06-14 22:40:52 +03:00
parent b9f6df272e
commit a48120b039
305 changed files with 12323 additions and 445 deletions
+2
View File
@@ -125,7 +125,9 @@ pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState
let model = parts.collect::<Vec<_>>().join(" ");
Some(DiskInfo {
name: format!("/dev/{}", name),
vendor: String::new(),
model: if model.is_empty() { "Unknown".to_string() } else { model },
drive_type: String::new(),
size_gb: bytes / 1_073_741_824,
size_bytes: bytes,
})
+1 -1
View File
@@ -136,7 +136,7 @@ pub struct OemConfig {
fn default_title() -> String { "YALI Installation Tool".to_string() }
fn default_subtitle() -> String { "PiSi GNU/LINUX".to_string() }
fn default_yali_logo() -> String { "assets/yali.png".to_string() }
fn default_yali_logo() -> String { "assets/icons/YALI_icon_LIGHT.svg".to_string() }
fn default_pisi_logo_dark() -> String { "assets/pisi-logo-dark.png".to_string() }
fn default_pisi_logo_light() -> String { "assets/pisi-logo-light.png".to_string() }
fn default_interval() -> u64 { 6 }
+135
View File
@@ -0,0 +1,135 @@
use eframe::egui;
#[macro_export]
macro_rules! draw_icon {
($ui:expr, $path:literal, $size:expr, $color:expr) => {
$ui.add(
eframe::egui::Image::new(eframe::egui::include_image!($path))
.max_width($size)
.max_height($size)
.tint($color),
)
};
}
pub fn form_input_text(
ui: &mut eframe::egui::Ui,
text: &mut String,
)-> eframe::egui::Response {
let width_percent = 0.90; // Mevcut genişliğin %90'ı
let height = 32.0; // Standart yükseklik
let hesaplanan_genislik = ui.available_width() * width_percent;
let boyut = eframe::egui::vec2(hesaplanan_genislik, height);
ui.spacing_mut().button_padding = eframe::egui::vec2(10.0, 10.0);
ui.add_sized(
boyut,
eframe::egui::TextEdit::singleline(text)
.vertical_align(eframe::egui::Align::Center)
)
}
pub fn form_input_password(
ui: &mut eframe::egui::Ui,
text: &mut String,
show_password: &mut bool,
) -> eframe::egui::Response {
let width_percent = 0.90;
let height = 32.0;
let toplam_genislik = ui.available_width() * width_percent;
let dugme_genislik = 40.0;
let alan_genislik = toplam_genislik - dugme_genislik - 4.0;
let boyut = eframe::egui::vec2(alan_genislik, height);
ui.spacing_mut().button_padding = eframe::egui::vec2(10.0, 10.0);
ui.horizontal(|ui| {
let r = ui.add_sized(
boyut,
eframe::egui::TextEdit::singleline(text)
.password(!*show_password)
.vertical_align(eframe::egui::Align::Center),
);
let göz = if *show_password { "🙈" } else { "👁" };
if ui.add(
eframe::egui::Button::new(göz)
.min_size(eframe::egui::vec2(dugme_genislik, height))
).clicked() {
*show_password = !*show_password;
}
r
}).inner
}
pub fn form_label(ui: &mut eframe::egui::Ui, text: &str) {
ui.label(
eframe::egui::RichText::new(text)
.strong()
.color(crate::ui::theme::c_text()) // Atomic temandan rengi çekiyor
);
}
pub fn form_checkbox(ui: &mut eframe::egui::Ui, checked: &mut bool, text: &str) {
ui.checkbox(checked, eframe::egui::RichText::new(text).strong().color(crate::ui::theme::c_text()));
}
pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::installer::GlobalState) {
ui.horizontal(|ui| {
// Satır yüksekliğini tam olarak 32px'e eşitliyoruz
ui.set_height(32.0);
// 1. Sol taraftaki ikon alanı
ui.vertical(|ui| {
ui.add_space(4.0);
ui.add(
eframe::egui::Image::new(eframe::egui::include_image!("../assets/icons/language-icon.svg"))
.max_width(40.0)
);
});
ui.add_space(6.0);
// 2. ComboBox alanı (%90 genişlik payı)
let available_width = ui.available_width() * 0.90;
let current_lang_label = if state.language == "tr" { "Türkçe" } else { "English" };
let languages = [
("tr", "Türkçe", eframe::egui::include_image!("../assets/flags/tr.svg")),
("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
ui.add_sized([available_width, item_height], |ui: &mut egui::Ui| {
ui.horizontal(|ui| {
ui.add_space(6.0); // İç sol boşluk
ui.add(eframe::egui::Image::new(image_source.clone()).max_width(18.0));
ui.add_space(4.0);
let mut current_lang = state.language.clone();
let resp = ui.selectable_value(&mut current_lang, code.to_string(), *label);
if resp.clicked() {
state.language = code.to_string();
}
resp
}).response
});
}
});
});
}
+58 -45
View File
@@ -74,7 +74,10 @@ pub trait InstallerStep {
#[derive(Clone, Debug)]
pub struct DiskInfo {
pub name: String, // ör. "/dev/sda"
pub vendor: String, // "ATA", "INTEL", vb.
pub model: String,
#[allow(dead_code)]
pub drive_type: String, // "NVMe SSD", "SSD", "HDD", "USB", "MMC/SD", vb.
pub size_gb: u64,
pub size_bytes: u64,
}
@@ -274,7 +277,7 @@ impl Default for GlobalState {
install_log: Vec::new(),
install_current_file: None,
demo_mode: false,
is_dark: true,
is_dark: false,
rescue_mode: false,
bootloader_device: String::new(),
bootloader_timeout: 5,
@@ -313,6 +316,7 @@ impl Default for GlobalState {
// WELCOME STEP (sistem gereksinimleri + dil seçimi)
// ─────────────────────────────────────────────
use crate::funct::language_selector_ui;
use crate::autoinstall::checker::{CheckResult, CheckStatus, run_all, all_passed};
pub struct WelcomeStep {
@@ -404,16 +408,17 @@ impl InstallerStep for WelcomeStep {
ui.add_space(12.0);
// Dil seçimi (Açılır Liste / ComboBox)
ui.horizontal(|ui| {
ui.label(t!("select_language"));
let current_lang_label = if state.language == "tr" { "🇹🇷 Türkçe" } else { "🇺🇸 English" };
egui::ComboBox::from_id_source("language_select")
.selected_text(current_lang_label)
.show_ui(ui, |ui| {
ui.selectable_value(&mut state.language, "tr".to_string(), "🇹🇷 Türkçe");
ui.selectable_value(&mut state.language, "en".to_string(), "🇺🇸 English");
});
});
// ui.horizontal(|ui| {
// ui.label(t!("select_language"));
// let current_lang_label = if state.language == "tr" { "🇹🇷 Türkçe" } else { "🇺🇸 English" };
// egui::ComboBox::from_id_source("language_select")
// .selected_text(current_lang_label)
// .show_ui(ui, |ui| {
// ui.selectable_value(&mut state.language, "tr".to_string(), "🇹🇷 Türkçe");
// ui.selectable_value(&mut state.language, "en".to_string(), "🇺🇸 English");
// });
// });
language_selector_ui(ui, state);
ui.add_space(14.0);
ui.separator();
@@ -425,7 +430,20 @@ impl InstallerStep for WelcomeStep {
.strong()
.color(theme::c_text()),
);
ui.add_space(6.0);
ui.add_space(10.0);
// Hata varsa uyarı
ui.add_space(10.0);
let has_fail = self.checks.iter().any(|c| c.status == CheckStatus::Fail);
if has_fail {
theme::error_box(ui, &t!("check_fail_warning"));
} else if self.checks.iter().any(|c| c.status == CheckStatus::Warn) {
theme::warning_box(ui, &t!("check_warn_notice"));
} else if !self.checks.is_empty() {
theme::success_box(ui, &t!("check_all_pass"));
}
ui.add_space(10.0);
// Kontrol tablosu
egui::Grid::new("checks_grid")
@@ -452,42 +470,37 @@ impl InstallerStep for WelcomeStep {
// Rescue mode butonu
ui.add_space(10.0);
ui.horizontal(|ui| {
let rescue_available = detect_existing_linux();
if rescue_available {
ui.colored_label(
egui::Color32::from_rgb(80, 170, 80),
"🛟",
);
if ui.add(theme::secondary_button(&t!("rescue_button"))).clicked() {
state.rescue_mode = true;
}
} else {
ui.label(
egui::RichText::new(t!("rescue_not_available"))
.size(12.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
ui.allocate_ui(
egui::vec2(ui.available_width(), 30.0), // Genişlik tam, Yükseklik sadece 24px
|ui| {
// Şimdi dikeyde ortalayarak soldan sağa diziyoruz
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
let rescue_available = detect_existing_linux();
if rescue_available {
if theme::secondary_button(ui, &t!("rescue_button")).clicked() {
state.rescue_mode = true;
}
if theme::secondary_button(ui, &t!("recheck")).clicked() {
self.checked = false;
self.on_enter(state);
}
} else {
ui.label(
egui::RichText::new(t!("rescue_not_available"))
.size(12.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
}
});
}
});
);
// Hata varsa uyarı
ui.add_space(10.0);
let has_fail = self.checks.iter().any(|c| c.status == CheckStatus::Fail);
if has_fail {
theme::error_box(ui, &t!("check_fail_warning"));
} else if self.checks.iter().any(|c| c.status == CheckStatus::Warn) {
theme::warning_box(ui, &t!("check_warn_notice"));
} else if !self.checks.is_empty() {
theme::success_box(ui, &t!("check_all_pass"));
}
// Yeniden tara butonu
ui.add_space(8.0);
if ui.add(theme::secondary_button(&t!("recheck"))).clicked() {
self.checked = false;
self.on_enter(state);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
+8 -5
View File
@@ -922,7 +922,7 @@ impl Job for PartitionJob {
let end_mb = next_mb + self.plan.efi_mb;
ui.log(format!("EFI bölümü oluşturuluyor: {}–{} MB", next_mb, end_mb));
run_parted(&[
"-s", disk, "mkpart", "primary", "fat32",
"-s", disk, "mkpart", "fat32",
&format!("{}MB", next_mb), &format!("{}MB", end_mb),
]).await?;
run_parted(&["-s", disk, "set", "1", "esp", "on"]).await?;
@@ -934,7 +934,7 @@ impl Job for PartitionJob {
let swap_num: u32 = if self.plan.efi_mb > 0 { 2 } else { 1 };
ui.log(format!("Swap bölümü oluşturuluyor: {}–{} MB", next_mb, swap_end));
run_parted(&[
"-s", disk, "mkpart", "primary", "linux-swap",
"-s", disk, "mkpart", "linux-swap",
&format!("{}MB", next_mb), &format!("{}MB", swap_end),
]).await?;
next_mb = swap_end;
@@ -943,7 +943,7 @@ impl Job for PartitionJob {
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
ui.log(format!("Kök bölümü oluşturuluyor: {}MB – disk sonu", next_mb));
run_parted(&[
"-s", disk, "mkpart", "primary", "ext4",
"-s", disk, "mkpart", "ext4",
&format!("{}MB", next_mb), "100%",
]).await?;
@@ -1945,6 +1945,10 @@ impl Job for CustomPartitionJob {
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
prepare_disk(&self.disk, ui).await;
let is_gpt = is_uefi; // Genellikle UEFI ise GPT kullanılır
// MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır.
let part_type_or_name = if is_gpt { "" } else { "primary" };
// 1. Bölüm tablosu oluştur
let table_label = if is_uefi { "gpt" } else { "msdos" };
ui.log(format!("parted {} mktable {}", self.disk, table_label));
@@ -1981,7 +1985,7 @@ impl Job for CustomPartitionJob {
part_num, start_str, end_str, fs_label
));
run_parted(&[
"-s", &self.disk, "mkpart", "primary", fs_label,
"-s", &self.disk, "mkpart", part_type_or_name, fs_label,
&start_str, &end_str
]).await?;
@@ -2389,4 +2393,3 @@ pub fn build_custom_job_queue(
JobQueue::new(jobs)
}
+78 -53
View File
@@ -4,6 +4,7 @@ mod jobs;
mod ui;
mod autoinstall;
mod branding;
mod funct;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
@@ -16,6 +17,7 @@ fn main() -> eframe::Result<()> {
// ── CLI: otomatik kurulum modu ───────────────────────
let args: Vec<String> = std::env::args().collect();
let demo_mode = args.contains(&"--demo".to_string());
if let Some(pos) = args.iter().position(|a| a == "--auto-install") {
if let Some(path) = args.get(pos + 1) {
@@ -31,6 +33,7 @@ fn main() -> eframe::Result<()> {
/// Grafik arayüzü başlatır (normal mod).
fn run_gui(demo_mode: bool) -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1200.0, 900.0])
@@ -45,8 +48,8 @@ fn run_gui(demo_mode: bool) -> eframe::Result<()> {
Box::new(move |cc| {
// Görsel yükleyicileri etkinleştir (PNG desteği)
egui_extras::install_image_loaders(&cc.egui_ctx);
ui::theme::apply(&cc.egui_ctx, true);
ui::theme::set_theme_mode(false);
ui::theme::apply(&cc.egui_ctx, false);
let mut app = YaliApp::default();
app.state.demo_mode = demo_mode;
Box::new(app)
@@ -202,12 +205,13 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
}
struct YaliApp {
state: GlobalState,
steps: Vec<Box<dyn InstallerStep>>,
rescue_step: Option<Box<dyn InstallerStep>>,
last_step: usize,
branding: branding::BrandingConfig,
loaded_language: String,
state: GlobalState,
steps: Vec<Box<dyn InstallerStep>>,
rescue_step: Option<Box<dyn InstallerStep>>,
last_step: usize,
branding: branding::BrandingConfig,
loaded_language: String,
show_cancel_dialog: bool,
}
impl Default for YaliApp {
@@ -233,6 +237,7 @@ impl Default for YaliApp {
last_step: usize::MAX,
branding: branding.clone(),
loaded_language: String::new(),
show_cancel_dialog: false,
};
// OEM modu: varsayılan değerleri ata ve step engelleme
@@ -347,17 +352,17 @@ impl eframe::App for YaliApp {
);
}
ui.add_space(8.0);
ui.label(
egui::RichText::new(t!("app_name"))
.strong()
.size(16.0)
.color(ui::theme::c_text_dim()),
);
// ui.label(
// egui::RichText::new(t!("app_name"))
// .strong()
// .size(16.0)
// .color(ui::theme::c_text_dim()),
// );
ui.colored_label(
egui::Color32::from_rgb(60, 60, 90),
format!("{}: {}", t!("version"), env!("CARGO_PKG_VERSION")),
);
// ui.colored_label(
// egui::Color32::from_rgb(60, 60, 90),
// format!("{}: {}", t!("version"), env!("CARGO_PKG_VERSION")),
// );
});
ui.add_space(16.0);
@@ -366,7 +371,8 @@ impl eframe::App for YaliApp {
// Adım listesi
for (i, step) in self.steps.iter().enumerate() {
let label = format!("{}. {}", i + 1, step.name());
//let label = format!("{}. {}", i + 1, step.name());
let label = step.name();
egui::Frame::none()
.fill(if i == cur {
@@ -374,23 +380,23 @@ impl eframe::App for YaliApp {
} else {
egui::Color32::TRANSPARENT
})
.rounding(egui::Rounding::same(6.0))
//.rounding(egui::Rounding::same(6.0))
.inner_margin(egui::Margin::symmetric(10.0, 5.0))
.show(ui, |ui| {
ui.set_width(175.0);
if i == cur {
ui.horizontal(|ui| {
ui.colored_label(ui::theme::c_accent(), "▶");
ui.colored_label(ui::theme::c_text(), label);
ui.colored_label(ui::theme::c_sidebar_text(), label);
});
} else if i < cur {
ui.horizontal(|ui| {
ui.colored_label(ui::theme::c_success(), "✅");
ui.colored_label(ui::theme::c_text_dim(), label);
ui.colored_label(ui::theme::c_sidebar_text(), label);
});
} else {
ui.colored_label(
egui::Color32::from_rgb(80, 80, 100),
egui::Color32::from_rgb(255, 255, 255),
format!(" {}", label),
);
}
@@ -400,39 +406,35 @@ impl eframe::App for YaliApp {
}
// Alt kısım: tema değiştirme
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.add_space(6.0);
// ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
// ui.add_space(6.0);
let theme_icon = if self.state.is_dark { "☀" } else { "🌙" };
let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
if ui.add(
egui::Button::new(
egui::RichText::new(format!("{} {}", theme_icon, theme_label))
.size(12.0)
)
.rounding(egui::Rounding::same(6.0))
.min_size(egui::Vec2::new(100.0, 28.0))
).clicked() {
self.state.is_dark = !self.state.is_dark;
ui::theme::set_theme_mode(self.state.is_dark);
ui::theme::apply(ctx, self.state.is_dark);
}
});
// let theme_icon = if self.state.is_dark { "☀" } else { "🌙" };
// let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
// if ui.add(
// egui::Button::new(
// egui::RichText::new(format!("{} {}", theme_icon, theme_label))
// .size(12.0)
// )
// .rounding(egui::Rounding::same(6.0))
// .min_size(egui::Vec2::new(100.0, 28.0))
// ).clicked() {
// self.state.is_dark = !self.state.is_dark;
// ui::theme::set_theme_mode(self.state.is_dark);
// ui::theme::apply(ctx, self.state.is_dark);
// }
// });
});
// ── Alt panel: navigasyon ────────────────────────────
egui::TopBottomPanel::bottom("footer")
.frame(egui::Frame::none()
.fill(ui::theme::c_bg_panel())
.stroke(egui::Stroke::new(1.0, ui::theme::c_border())))
.fill(ui::theme::c_footer_bg())
.stroke(egui::Stroke::new(1.0, ui::theme::c_footer_bg())))
.show(ctx, |ui| {
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_space(8.0);
if !is_exec && !is_finish && ui.add(ui::theme::secondary_button(&t!("cancel"))).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
ui.add_space(8.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(8.0);
@@ -449,19 +451,22 @@ impl eframe::App for YaliApp {
} else {
t!("next")
};
let btn = if is_valid {
ui::theme::primary_button(&next_label)
} else {
ui::theme::secondary_button(&next_label)
if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() {
self.show_cancel_dialog = true;
};
if ui.add_enabled(is_valid, btn).clicked() {
let next_clicked = if is_valid {
ui::theme::primary_button(ui, &next_label).clicked()
} else {
ui::theme::secondary_button(ui, &next_label).clicked()
};
if next_clicked {
self.state.current_step += 1;
}
}
if cur > 0 && !is_exec {
ui.add_space(8.0);
if ui.add(ui::theme::secondary_button(&t!("back"))).clicked() {
if ui::theme::secondary_button(ui, &t!("back")).clicked() {
self.state.current_step -= 1;
}
}
@@ -471,6 +476,26 @@ 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;
}
});
});
}
// ── Merkez panel ─────────────────────────────────────
egui::CentralPanel::default()
.frame(egui::Frame::none()
+7 -15
View File
@@ -24,7 +24,7 @@ impl InstallerStep for FinishStep {
.size(24.0)
.strong()
.color(theme::c_text()),
);
);
ui.add_space(8.0);
ui.label(
egui::RichText::new(t!("finish_description"))
@@ -35,26 +35,18 @@ impl InstallerStep for FinishStep {
ui.add_space(32.0);
// Yeniden Başlat — ana eylem butonu
if ui.add(
theme::primary_button(&t!("finish_restart"))
.min_size(egui::vec2(200.0, 44.0)),
).clicked() {
if theme::primary_button(ui, &t!("finish_restart")).clicked() {
let _ = std::process::Command::new("sync").status();
let _ = std::process::Command::new("reboot")
.arg("-f")
.spawn();
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = std::process::Command::new("reboot")
.arg("-f")
.spawn();
});
}
ui.add_space(12.0);
// Canlı masaüstüne dön — ikincil eylem
if ui.add(
theme::secondary_button(&t!("finish_live"))
.min_size(egui::vec2(200.0, 36.0)),
).clicked() {
if theme::secondary_button(ui, &t!("finish_live")).clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
}
});
+398 -108
View File
@@ -81,10 +81,36 @@ fn read_existing_partitions(disk_path: &str) -> Vec<crate::installer::CustomPart
}
/// `/sys/block` altındaki gerçek blok aygıtlarını (partition değil) listeler.
/// Aygıt adına (`sda`, `nvme0n1`) bakarak türünü belirler.
fn detect_drive_type(name: &str) -> String {
if name.starts_with("nvme") {
return "NVMe SSD".into();
}
if name.starts_with("mmcblk") {
return "MMC/SD".into();
}
// removable ortam (USB flash bellek)
let removable_path = format!("/sys/block/{}/removable", name);
if let Ok(val) = std::fs::read_to_string(&removable_path) {
if val.trim() == "1" {
return "USB".into();
}
}
// döner disk mi? (rotational=1 → HDD, 0 → SSD)
let rotational_path = format!("/sys/block/{}/queue/rotational", name);
if let Ok(val) = std::fs::read_to_string(&rotational_path) {
if val.trim() == "0" {
return "SSD".into();
}
}
"HDD".into()
}
/// lsblk'dan gelen çıktı akıllıca ayrıştırılır; başarısız olursa /sys/block fallback devreye girer.
fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
pub fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
// Önce -P (key=value pairs) ile dene, boşluklu MODEL/VENDOR güvenli
let out = std::process::Command::new("lsblk")
.args(["-d", "-o", "NAME,SIZE,MODEL", "-b", "--noheadings"])
.args(["-d", "-o", "NAME,SIZE,MODEL,VENDOR", "-b", "--noheadings", "-P"])
.output();
let mut disks = Vec::new();
@@ -93,45 +119,46 @@ fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
if o.status.success() {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
// split_whitespace ardışık tüm boşlukları tek bir ayırıcı gibi yakalar
let mut tokens = line.split_whitespace();
let name_raw = match tokens.next() {
Some(n) => n,
None => continue,
};
// NAME="sda" SIZE="42949672960" MODEL="VBOX HARDDISK" VENDOR="ATA"
let mut name_raw = String::new();
let mut vendor = String::new();
let mut model = String::new();
let mut size = 0u64;
for pair in line.split_ascii_whitespace() {
let mut parts = pair.splitn(2, '=');
let key = parts.next().unwrap_or("");
let val = parts.next().unwrap_or("").trim_matches('"');
match key {
"NAME" => name_raw = val.to_string(),
"SIZE" => size = val.parse().unwrap_or(0),
"VENDOR"=> vendor = val.to_string(),
"MODEL" => model = val.to_string(),
_ => {}
}
}
if name_raw.is_empty() || size == 0 { continue; }
// loop, ram, sr (CD-ROM) ve zram aygıtlarını atla
if name_raw.starts_with("loop")
|| name_raw.starts_with("ram")
|| name_raw.starts_with("sr")
|| name_raw.starts_with("zram")
{ continue; }
let size_str = match tokens.next() {
Some(s) => s,
None => continue,
};
let size_bytes: u64 = size_str.parse().unwrap_or(0);
// Eğer boyut 0 ise ekleme yapma ki lsblk patlarsa fallback çalışabilsin
if size_bytes == 0 { continue; }
// Kalan tüm token'lar model ismidir (örn: ["VBOX", "HARDDISK"])
let model_tokens: Vec<&str> = tokens.collect();
let model = model_tokens.join(" ");
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name_raw),
vendor,
model,
size_gb: size_bytes / 1_073_741_824,
size_bytes,
drive_type: detect_drive_type(&name_raw),
size_gb: size / 1_073_741_824,
size_bytes: size,
});
}
}
}
// lsblk başarısız olduysa veya geçerli disk bulamadıysa temiz /sys/block fallback'i çalışır
// lsblk başarısız olduysa /sys/block fallback
if disks.is_empty() {
if let Ok(rd) = std::fs::read_dir("/sys/block") {
for entry in rd.flatten() {
@@ -149,13 +176,19 @@ fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
let size_bytes = size_sectors * 512;
if size_bytes == 0 { continue; }
let vendor_path = format!("/sys/block/{}/device/vendor", name);
let vendor = std::fs::read_to_string(&vendor_path)
.unwrap_or_default().trim().to_string();
let model_path = format!("/sys/block/{}/device/model", name);
let model = std::fs::read_to_string(&model_path)
.unwrap_or_default().trim().to_string();
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name),
vendor,
model,
drive_type: detect_drive_type(&name),
size_gb: size_bytes / 1_073_741_824,
size_bytes,
});
@@ -359,22 +392,91 @@ impl PartitionStep {
fn show_auto_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.group(|ui| {
ui.label(t!("select_disk_label"));
ui.add_space(4.0);
ui.add_space(8.0);
let disks = state.available_disks.clone();
for disk in &disks {
let too_small = disk.size_gb < MIN_DISK_GB;
let label = egui::RichText::new(format!("{} — {} GB ({})", disk.name, disk.size_gb, disk.model)).strong().color(crate::ui::theme::c_accent());
ui.add_enabled_ui(!too_small, |ui| {
let selected = state.selected_disk.as_deref() == Some(&disk.name);
if ui.radio(selected, label).clicked() {
state.selected_disk = Some(disk.name.clone());
state.partition_plan = make_plan(disk, self.is_uefi);
let kart_genislik = 180.0;
let kart_yukseklik = 160.0;
egui::Grid::new("disk_grid")
.min_col_width(kart_genislik + 12.0)
.spacing([12.0, 12.0])
.show(ui, |ui| {
for (i, disk) in disks.iter().enumerate() {
let too_small = disk.size_gb < MIN_DISK_GB;
let selected = state.selected_disk.as_deref() == Some(&disk.name);
let (rect, response) = ui.allocate_exact_size(
egui::vec2(kart_genislik, kart_yukseklik),
egui::Sense::click(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
// arka plan
egui::Frame::none()
.fill(if selected {
crate::ui::theme::c_accent_dim()
} else if response.hovered() {
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180)
} else if too_small {
egui::Color32::from_rgba_unmultiplied(60, 30, 30, 180)
} else {
crate::ui::theme::c_bg_widget()
})
.stroke(egui::Stroke::new(
if selected { 2.0 } else { 1.0 },
if selected { crate::ui::theme::c_accent() } else { crate::ui::theme::c_border() },
))
.rounding(egui::Rounding::same(8.0))
.inner_margin(egui::Margin::same(8.0))
.show(child_ui, |ui| {
ui.set_min_size(egui::vec2(kart_genislik - 16.0, kart_yukseklik - 16.0));
ui.add_space(8.0);
ui.add(
egui::Image::new(egui::include_image!("../../assets/disk.png"))
.max_width(64.0)
.max_height(64.0)
.rounding(egui::Rounding::same(6.0))
.tint(if too_small {
egui::Color32::from_gray(80)
} else {
egui::Color32::WHITE
}),
);
ui.add_space(6.0);
let marka = if disk.vendor.is_empty() {
&disk.model
} else {
&disk.vendor
};
ui.label(
egui::RichText::new(format!("{} GB", disk.size_gb))
.size(14.0)
.strong()
.color(crate::ui::theme::c_text()),
);
ui.label(
egui::RichText::new(marka)
.size(11.0)
.color(crate::ui::theme::c_text_dim()),
);
if too_small {
ui.colored_label(
crate::ui::theme::c_error(),
t!("disk_too_small"),
);
}
});
if response.clicked() && !too_small {
state.selected_disk = Some(disk.name.clone());
state.partition_plan = make_plan(disk, self.is_uefi);
}
if (i + 1) % 3 == 0 {
ui.end_row();
}
}
});
if too_small {
ui.small(t!("disk_min_required", err = t!("disk_too_small"), n = MIN_DISK_GB.to_string()));
}
}
});
if let Some(ref plan) = state.partition_plan {
@@ -386,34 +488,96 @@ impl PartitionStep {
partition_bar(ui, plan);
ui.add_space(8.0);
egui::Grid::new("plan_grid")
// Bootstrap 5 tarzı tablo
let col_widths = [0.40, 0.25, 0.35];
let tam_genislik = ui.available_width();
let header_color = egui::Color32::from_rgb(33, 37, 41); // dark bg
let header_text = egui::Color32::WHITE;
let _border_stroke = egui::Stroke::new(1.0, crate::ui::theme::c_border());
let grid = egui::Grid::new("plan_grid")
.num_columns(3)
.spacing([20.0, 6.0])
.striped(true)
.show(ui, |ui| {
ui.label(egui::RichText::new(t!("plan_col_part")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("plan_col_fs")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("plan_col_size")).strong().color(crate::ui::theme::c_text()));
ui.end_row();
.spacing([0.0, 0.0])
.min_col_width(0.0);
if plan.efi_mb > 0 {
ui.label(format!("{}1 (EFI)", plan.disk));
ui.label("FAT32");
ui.label(format!("{} MB", plan.efi_mb));
ui.end_row();
}
let swap_part = if plan.efi_mb > 0 { 2 } else { 1 };
ui.label(format!("{}{} (swap)", plan.disk, swap_part));
ui.label("swap");
ui.label(format!("{} MB", plan.swap_mb));
ui.end_row();
grid.show(ui, |ui| {
// ── Başlık satırı ─────────────────────────────────
let headers = [t!("plan_col_part"), t!("plan_col_fs"), t!("plan_col_size")];
for (j, h) in headers.into_iter().enumerate() {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(tam_genislik * col_widths[j], 30.0),
egui::Sense::hover(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
egui::Frame::none()
.fill(header_color)
.stroke(egui::Stroke::new(1.0, header_color))
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
.show(child_ui, |ui| {
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0));
});
});
}
ui.end_row();
let root_part = swap_part + 1;
ui.label(format!("{}{}{}", plan.disk, root_part, t!("plan_root_suffix")));
ui.label("ext4");
ui.label(format!("{:.1} GB", plan.root_mb as f64 / 1024.0));
ui.end_row();
// ── Veri satırları ────────────────────────────────
struct Satir {
part: String,
fs: &'static str,
size: String,
}
let mut satirlar = Vec::new();
if plan.efi_mb > 0 {
satirlar.push(Satir {
part: format!("{}1 (EFI)", plan.disk),
fs: "FAT32",
size: format!("{} MB", plan.efi_mb),
});
}
let swap_part = if plan.efi_mb > 0 { 2 } else { 1 };
satirlar.push(Satir {
part: format!("{}{} (swap)", plan.disk, swap_part),
fs: "swap",
size: format!("{} MB", plan.swap_mb),
});
let root_part = swap_part + 1;
satirlar.push(Satir {
part: format!("{}{}{}", plan.disk, root_part, t!("plan_root_suffix")),
fs: "ext4",
size: format!("{:.1} GB", plan.root_mb as f64 / 1024.0),
});
for (row_idx, satir) in satirlar.iter().enumerate() {
let row_bg = if row_idx % 2 == 0 {
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 60)
} else {
egui::Color32::TRANSPARENT
};
let hucreler: [&str; 3] = [&satir.part, satir.fs, &satir.size];
for (j, deger) in hucreler.iter().enumerate() {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(tam_genislik * col_widths[j], 34.0),
egui::Sense::hover(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
egui::Frame::none()
.fill(row_bg)
.stroke(egui::Stroke::new(0.0, egui::Color32::TRANSPARENT))
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
.show(child_ui, |ui| {
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new(*deger).size(13.0).color(crate::ui::theme::c_text()));
});
});
}
// satır alt çizgisi
if row_idx < satirlar.len() - 1 {
ui.separator();
}
ui.end_row();
}
});
ui.add_space(8.0);
ui.label(format!("{}: {}", t!("partition_table_type"), plan.table_type));
@@ -464,23 +628,81 @@ impl PartitionStep {
impl PartitionStep {
fn show_manual_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
let accent = crate::ui::theme::c_accent();
let kart_genislik = 180.0;
let kart_yukseklik = 160.0;
// ── Disk seçimi (hedef disk) ────────────────────────────
ui.group(|ui| {
ui.label(t!("select_disk_label"));
ui.add_space(4.0);
ui.add_space(8.0);
let disks = state.available_disks.clone();
for disk in &disks {
let selected = state.selected_disk.as_deref() == Some(&disk.name);
let label = egui::RichText::new(format!("{} — {} GB ({})", disk.name, disk.size_gb, disk.model)).strong().color(crate::ui::theme::c_accent());
if ui.radio(selected, label).clicked() {
state.selected_disk = Some(disk.name.clone());
// Disk değişince mevcut bölümleri oku ve listeyi güncelle.
// Böylece kullanıcı diskin mevcut düzenini görebilir.
state.custom_partitions = read_existing_partitions(&disk.name);
self.manual.error = None;
}
}
egui::Grid::new("manual_disk_grid")
.min_col_width(kart_genislik + 12.0)
.spacing([12.0, 12.0])
.show(ui, |ui| {
for (i, disk) in disks.iter().enumerate() {
let selected = state.selected_disk.as_deref() == Some(&disk.name);
let (rect, response) = ui.allocate_exact_size(
egui::vec2(kart_genislik, kart_yukseklik),
egui::Sense::click(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
egui::Frame::none()
.fill(if selected {
crate::ui::theme::c_accent_dim()
} else if response.hovered() {
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180)
} else {
crate::ui::theme::c_bg_widget()
})
.stroke(egui::Stroke::new(
if selected { 2.0 } else { 1.0 },
if selected { crate::ui::theme::c_accent() } else { crate::ui::theme::c_border() },
))
.rounding(egui::Rounding::same(8.0))
.inner_margin(egui::Margin::same(8.0))
.show(child_ui, |ui| {
ui.set_min_size(egui::vec2(kart_genislik - 16.0, kart_yukseklik - 16.0));
ui.add_space(8.0);
ui.add(
egui::Image::new(egui::include_image!("../../assets/disk.png"))
.max_width(64.0)
.max_height(64.0)
.rounding(egui::Rounding::same(6.0)),
);
ui.add_space(6.0);
let marka = if disk.vendor.is_empty() {
&disk.model
} else {
&disk.vendor
};
ui.label(
egui::RichText::new(format!("{} GB", disk.size_gb))
.size(14.0)
.strong()
.color(crate::ui::theme::c_text()),
);
ui.label(
egui::RichText::new(marka)
.size(11.0)
.color(crate::ui::theme::c_text_dim()),
);
});
if response.clicked() {
state.selected_disk = Some(disk.name.clone());
state.custom_partitions = read_existing_partitions(&disk.name);
self.manual.error = None;
}
if (i + 1) % 3 == 0 {
ui.end_row();
}
}
});
});
ui.add_space(8.0);
@@ -495,46 +717,114 @@ impl PartitionStep {
// ── Mevcut bölüm listesi tablosu ───────────────────────
ui.label(egui::RichText::new(t!("mp_plan_title")).strong().color(crate::ui::theme::c_text()));
ui.add_space(4.0);
ui.add_space(6.0);
let mut delete_idx: Option<usize> = None;
egui::Grid::new("manual_parts_grid")
let col_widths = [0.28, 0.12, 0.18, 0.27, 0.15];
let tam_genislik = ui.available_width();
let header_color = egui::Color32::from_rgb(33, 37, 41);
let header_text = egui::Color32::WHITE;
let _border_stroke = egui::Stroke::new(1.0, crate::ui::theme::c_border());
let grid = egui::Grid::new("manual_parts_grid")
.num_columns(5)
.spacing([12.0, 6.0])
.striped(true)
.show(ui, |ui| {
// Başlık
ui.label(egui::RichText::new(t!("mp_device")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_size")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_fstype")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_mountpoint")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_action")).strong().color(crate::ui::theme::c_text()));
ui.end_row();
.spacing([0.0, 0.0])
.min_col_width(0.0);
for (i, part) in state.custom_partitions.iter().enumerate() {
let size_str = if part.size_mb == 0 {
t!("mp_size_remaining").to_string()
} else if part.size_mb >= 1024 {
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
} else {
format!("{} MB", part.size_mb)
};
grid.show(ui, |ui| {
// ── Başlık satırı ─────────────────────────────────
let headers: [String; 5] = [
t!("mp_device").to_string(),
t!("mp_size").to_string(),
t!("mp_fstype").to_string(),
t!("mp_mountpoint").to_string(),
t!("mp_action").to_string(),
];
for (j, h) in headers.into_iter().enumerate() {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(tam_genislik * col_widths[j], 30.0),
egui::Sense::hover(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
egui::Frame::none()
.fill(header_color)
.stroke(egui::Stroke::new(1.0, header_color))
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
.show(child_ui, |ui| {
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0));
});
});
}
ui.end_row();
ui.label(&part.device);
let mut fstype_label = part.fstype.label().to_string();
if part.encrypt {
fstype_label = format!("🔐 {}", fstype_label);
}
ui.label(&size_str);
ui.label(fstype_label);
ui.label(&part.mountpoint);
if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() {
delete_idx = Some(i);
}
ui.end_row();
// ── Veri satırları ────────────────────────────────
for (row_idx, part) in state.custom_partitions.iter().enumerate() {
let row_bg = if row_idx % 2 == 0 {
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 60)
} else {
egui::Color32::TRANSPARENT
};
let size_str = if part.size_mb == 0 {
t!("mp_size_remaining").to_string()
} else if part.size_mb >= 1024 {
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
} else {
format!("{} MB", part.size_mb)
};
let mut fstype_label = part.fstype.label().to_string();
if part.encrypt {
fstype_label = format!("🔐 {}", fstype_label);
}
});
let hucreler: [(String, bool); 5] = [
(part.device.clone(), false),
(size_str, false),
(fstype_label, false),
(part.mountpoint.clone(), false),
(String::new(), true), // action column - button
];
for (j, (deger, is_action)) in hucreler.iter().enumerate() {
let (rect, _response) = ui.allocate_exact_size(
egui::vec2(tam_genislik * col_widths[j], 34.0),
egui::Sense::click(),
);
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
if *is_action {
egui::Frame::none()
.fill(row_bg)
.stroke(egui::Stroke::new(0.0, egui::Color32::TRANSPARENT))
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
.show(child_ui, |ui| {
ui.vertical_centered(|ui| {
if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() {
delete_idx = Some(row_idx);
}
});
});
} else {
egui::Frame::none()
.fill(row_bg)
.stroke(egui::Stroke::new(0.0, egui::Color32::TRANSPARENT))
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
.show(child_ui, |ui| {
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new(deger).size(13.0).color(crate::ui::theme::c_text()));
});
});
}
}
if row_idx < state.custom_partitions.len() - 1 {
ui.separator();
}
ui.end_row();
}
});
// Silme onayı
if let Some(idx) = delete_idx {
+12 -16
View File
@@ -238,18 +238,15 @@ impl RescueStep {
// İşlem butonları
ui.horizontal(|ui| {
// 1. Bağla
if ui.add_enabled(
self.selected.is_some() && !self.mount_done,
theme::secondary_button("📂 /mnt'a Bağla"),
).clicked() {
let cond = self.selected.is_some() && !self.mount_done;
let resp = theme::secondary_button(ui, "📂 /mnt'a Bağla");
if cond && resp.clicked() {
self.mount_selected();
}
// 2. Chroot
if ui.add_enabled(
self.mount_done,
theme::secondary_button(&t!("rescue_chroot")),
).clicked() {
let resp = theme::secondary_button(ui, &t!("rescue_chroot"));
if self.mount_done && resp.clicked() {
self.log.push(t!("rescue_chroot_start").to_string());
match Self::run_rescue_cmd("chroot", &["/mnt", "/bin/bash", "-c", "echo 'chroot OK'"]) {
Ok(msg) => self.log.push(format!("✓ {}", msg)),
@@ -258,10 +255,8 @@ impl RescueStep {
}
// 3. GRUB yeniden yükle
if ui.add_enabled(
self.mount_done,
theme::secondary_button(&t!("rescue_grub")),
).clicked() {
let resp = theme::secondary_button(ui, &t!("rescue_grub"));
if self.mount_done && resp.clicked() {
self.log.push(t!("rescue_grub_start").to_string());
let boot_dev = state.bootloader_device.clone();
let steps: &[(&str, &[&str])] = &[
@@ -311,7 +306,7 @@ impl RescueStep {
// Tara butonu
ui.horizontal(|ui| {
if ui.add(theme::secondary_button(&t!("rescue_pisi_history_scan"))).clicked() {
if theme::secondary_button(ui, &t!("rescue_pisi_history_scan")).clicked() {
self.scan_pisi_history();
}
if self.pisi.scanned {
@@ -392,7 +387,8 @@ impl RescueStep {
// Geri al butonu
let can_takeback = self.pisi.selected.is_some() && !self.pisi.running;
if ui.add_enabled(can_takeback, theme::primary_button(&t!("rescue_pisi_takeback"))).clicked() {
let resp = theme::primary_button(ui, &t!("rescue_pisi_takeback"));
if can_takeback && resp.clicked() {
if let Some(idx) = self.pisi.selected {
let op = self.pisi.entries[idx].op;
self.run_takeback(op);
@@ -430,7 +426,7 @@ impl InstallerStep for RescueStep {
if self.detected.is_empty() {
theme::warning_box(ui, &t!("rescue_no_installations"));
ui.add_space(6.0);
if ui.add(theme::secondary_button(&t!("rescue_scan"))).clicked() {
if theme::secondary_button(ui, &t!("rescue_scan")).clicked() {
self.detect_installations();
}
return;
@@ -505,7 +501,7 @@ impl InstallerStep for RescueStep {
// Geri dön butonu
ui.add_space(8.0);
if ui.add(theme::secondary_button(&t!("rescue_return"))).clicked() {
if theme::secondary_button(ui, &t!("rescue_return")).clicked() {
state.rescue_mode = false;
}
}
+100 -132
View File
@@ -1,24 +1,21 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::funct::{ form_input_text, form_input_password, form_label, form_checkbox };
/// Kullanıcı adı, şifre, hostname ve yönetici (root) şifresi ayarları adımı.
pub struct UsersStep {
use_same_for_admin: bool,
show_password: bool,
show_password_confirm: bool,
show_root_password: bool,
show_root_password_confirm: bool,
use_same_for_admin: bool,
}
impl Default for UsersStep {
fn default() -> Self {
Self {
use_same_for_admin: true,
show_password: false,
show_password_confirm: false,
show_root_password: false,
show_root_password_confirm: false,
use_same_for_admin: true,
}
}
}
@@ -152,47 +149,47 @@ impl InstallerStep for UsersStep {
);
ui.add_space(12.0);
// ─── Kullanıcı Bilgileri Grubu ──────────────────────────────────
// ─── Kullanıcı Bilgileri Formu ──────────────────────────────────
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.inner_margin(egui::Margin::symmetric(10.0, 10.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Gerçek ad
ui.label(egui::RichText::new(t!("real_name_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
let prev = state.real_name.clone();
ui.text_edit_singleline(&mut state.real_name);
if state.real_name != prev && !state.real_name.is_empty() {
state.username = derive_username(&state.real_name);
}
if !state.real_name.is_empty() {
ui.colored_label(theme::c_success(), "✓");
}
});
ui.end_row();
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.add_space(10.0);
// Gerçek ad
form_label(ui, &t!("real_name_label"));
{
let prev = state.real_name.clone();
form_input_text(ui, &mut state.real_name);
if state.real_name != prev && !state.real_name.is_empty() {
state.username = derive_username(&state.real_name);
}
}
if !state.real_name.is_empty() {
ui.colored_label(theme::c_success(), "✅");
} else {
ui.colored_label(theme::c_error(), "❌");
}
ui.add_space(6.0);
// Kullanıcı adı (otomatik türetilir, düzenlenebilir)
ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), "✗");
}
}
});
ui.end_row();
// Kullanıcı adı (otomatik türetilir, düzenlenebilir)
form_label(ui, &t!("username_label"));
form_input_text(ui, &mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(theme::c_success(), "✅");
} else {
ui.colored_label(theme::c_warning(), t!("username_invalid"));
}
}
ui.add_space(6.0);
// Şifre
ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text()));
form_label(ui, &t!("password_label"));
// OEM modunda varsayılan şifre uyarısı göster
if state.oem_mode && !state.oem_password_changed {
ui.colored_label(
@@ -200,68 +197,56 @@ impl InstallerStep for UsersStep {
t!("oem_change_password_warning"),
);
}
ui.horizontal(|ui| {
{
let show_pwd = &mut self.show_password;
let before = state.password.clone();
if self.show_password {
ui.text_edit_singleline(&mut state.password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password).password(true));
}
form_input_password(ui, &mut state.password, show_pwd);
// Kullanıcı şifreyi değiştirdiyse OEM bayrağını kaldır
if state.oem_mode && state.password != before {
state.oem_password_changed = true;
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
if !state.password.is_empty() {
let (label, color) = password_strength(&state.password);
ui.colored_label(color, label);
}
});
ui.end_row();
}
if !state.password.is_empty() {
let (label, color) = password_strength(&state.password);
ui.colored_label(color, label);
}
ui.add_space(6.0);
// Şifre doğrulama
ui.label(egui::RichText::new(t!("password_confirm_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_password_confirm {
ui.text_edit_singleline(&mut state.password_confirm);
form_label(ui, &t!("password_confirm_label"));
{
let show_pwd = &mut self.show_password_confirm;
form_input_password(ui, &mut state.password_confirm, show_pwd);
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(theme::c_success(), "✅");
} else {
ui.add(egui::TextEdit::singleline(&mut state.password_confirm).password(true));
ui.colored_label(theme::c_warning(), t!("passwords_no_match"));
}
if ui.small_button(if self.show_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_password_confirm = !self.show_password_confirm;
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
}
});
ui.end_row();
}
ui.add_space(6.0);
// Hostname
ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), "✗");
}
form_label(ui, &t!("hostname_label"));
form_input_text(ui, &mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
ui.colored_label(theme::c_success(), "✅");
} else {
ui.colored_label(theme::c_warning(), t!("hostname_invalid"));
}
});
ui.end_row();
}
});
ui.add_space(10.0);
});
});
ui.add_space(14.0);
// ─── Yönetici (root) Ayarları ─────────────────────────────────────
ui.checkbox(&mut self.use_same_for_admin, egui::RichText::new(t!("use_same_for_admin")).strong().color(theme::c_text()));
form_checkbox(ui, &mut self.use_same_for_admin, &t!("use_same_for_admin"));
if self.use_same_for_admin {
state.root_password = state.password.clone();
@@ -277,59 +262,42 @@ impl InstallerStep for UsersStep {
ui.label(egui::RichText::new(t!("admin_description")).size(12.0).color(theme::c_text_dim()));
ui.add_space(8.0);
egui::Grid::new("admin_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Root şifresi
ui.label(egui::RichText::new(t!("admin_password_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_root_password {
ui.text_edit_singleline(&mut state.root_password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.root_password).password(true));
}
if ui.small_button(if self.show_root_password { "🙈" } else { "👁" }).clicked() {
self.show_root_password = !self.show_root_password;
}
if !state.root_password.is_empty() && state.root_password.len() < 8 {
ui.colored_label(theme::c_error(), t!("admin_password_short"));
}
});
ui.end_row();
// Root şifresi
ui.label(egui::RichText::new(t!("admin_password_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(
eframe::egui::TextEdit::singleline(&mut state.root_password)
.password(true)
.desired_width(f32::INFINITY)
.min_size(eframe::egui::vec2(0.0, 32.0))
);
// Şifre doğrulama
ui.label(egui::RichText::new(t!("admin_password_confirm_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_root_password_confirm {
ui.text_edit_singleline(&mut state.root_password_confirm);
} else {
ui.add(egui::TextEdit::singleline(&mut state.root_password_confirm).password(true));
}
if ui.small_button(if self.show_root_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_root_password_confirm = !self.show_root_password_confirm;
}
if !state.root_password_confirm.is_empty() {
if state.root_password == state.root_password_confirm {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
}
});
ui.end_row();
});
if !state.root_password.is_empty() && state.root_password.len() < 8 {
ui.colored_label(theme::c_error(), t!("admin_password_short"));
}
});
ui.add_space(8.0);
// Şifre doğrulama
ui.label(egui::RichText::new(t!("admin_password_confirm_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(
eframe::egui::TextEdit::singleline(&mut state.root_password_confirm)
.password(true)
.desired_width(f32::INFINITY)
.min_size(eframe::egui::vec2(0.0, 32.0))
);
});
if state.root_password == state.root_password_confirm {
ui.colored_label(theme::c_success(), "✅");
} else {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
});
}
// Hata mesajları gösterimi
ui.add_space(8.0);
if !state.username.is_empty() && !is_valid_username(&state.username) {
ui.colored_label(theme::c_error(), t!("username_invalid"));
}
if !state.hostname.is_empty() && !is_valid_hostname(&state.hostname) {
ui.colored_label(theme::c_error(), t!("hostname_invalid"));
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
+2 -2
View File
@@ -136,13 +136,13 @@ impl ErrorScreen {
let side = (ui.available_width() - total) / 2.0;
ui.add_space(side.max(0.0));
if ui.add(theme::danger_button(&t!("error_quit"))).clicked() {
if theme::danger_button(ui, &t!("error_quit")).clicked() {
on_quit();
}
ui.add_space(gap);
if ui.add(theme::primary_button(&t!("error_retry"))).clicked() {
if theme::primary_button(ui, &t!("error_retry")).clicked() {
on_retry();
}
});
+212 -57
View File
@@ -23,6 +23,10 @@ static WARNING: AtomicU32 = AtomicU32::new(0xFF9800);
static ERROR: AtomicU32 = AtomicU32::new(0xF44336);
static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D);
static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b)
static SIDEBAR_TEXT: AtomicU32 = AtomicU32::new(0x000000);
// Helper to convert hex to Color32
fn color_from_hex(rgb: u32) -> Color32 {
@@ -39,6 +43,8 @@ pub fn c_success() -> Color32 { color_from_hex(SUCCESS.load(Ordering::Relaxed)
pub fn c_warning() -> Color32 { color_from_hex(WARNING.load(Ordering::Relaxed)) }
pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) }
pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) }
pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) }
pub fn c_footer_bg() -> Color32 { color_from_hex(0xf7f7f7) }
pub fn set_theme_mode(is_dark: bool) {
if is_dark {
@@ -50,13 +56,19 @@ pub fn set_theme_mode(is_dark: bool) {
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed);
SIDEBAR.store(0x0B0B0D, Ordering::Relaxed);
} else {
BG_DARK.store(0xF0F0F5, Ordering::Relaxed);
BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
BORDER.store(0xD0D0DA, Ordering::Relaxed);
TEXT.store(0x20202A, Ordering::Relaxed);
TEXT_DIM.store(0x60606A, Ordering::Relaxed);
SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
BG_DARK.store(0xf7f7f7, Ordering::Relaxed);
SIDEBAR.store(0x292f34, Ordering::Relaxed);
TEXT.store(0x000000, Ordering::Relaxed);
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);
// BORDER.store(0xD0D0DA, Ordering::Relaxed);
// TEXT.store(0x20202A, Ordering::Relaxed);
// TEXT_DIM.store(0x60606A, Ordering::Relaxed);
// SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
}
}
@@ -85,32 +97,59 @@ pub fn apply(ctx: &egui::Context, is_dark: bool) {
fn setup_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
// egui'nin yerleşik fontlarına ek olarak sistem fontunu dene.
// ISO ortamında genellikle DejaVu veya Noto bulunur.
// Bulunamazsa egui varsayılanı kullanılır.
for path in [
"/usr/share/fonts/exo-2/Exo2-Medium.ttf",
"/usr/share/fonts/exo-2/Exo2-Regular.ttf",
"/usr/share/fonts/google-exo2/Exo2-Medium.ttf",
"/usr/share/fonts/google-exo2/Exo2-Regular.ttf",
// 1. ADIM: Sadece Metin (Text) Fontunu Bul ve Yükle
let text_font_paths = [
"/usr/share/fonts/TTF/OpenSans-Regular.ttf",
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/exo-1/Exo-Regular.otf",
"/usr/share/fonts/exo-1/Exo2-Medium.ttf",
"/usr/share/fonts/exo-1/Exo-Medium.otf",
"/usr/share/fonts/exo-1/Exo-Regular.otf",
"/usr/share/fonts/truetype/exo2/Exo2-Medium.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
] {
];
for path in text_font_paths {
if let Ok(data) = std::fs::read(path) {
fonts.font_data.insert(
"system_font".to_owned(),
"system_text_font".to_owned(),
egui::FontData::from_owned(data),
);
// Metin fontunu en başa (0. indeks) koyuyoruz ki varsayılan olarak bu kullanılsın
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "system_font".to_owned());
.insert(0, "system_text_font".to_owned());
break;
}
}
// 2. ADIM: Renkli Emoji Fontunu Ara ve "Fallback" (Yedek) Olarak Sona Ekle
// ISO ortamlarında Noto Color Emoji genellikle bu yollardan birinde olur
let emoji_font_paths = [
"/usr/share/fonts/noto/NotoColorEmoji.ttf",
];
for path in emoji_font_paths {
if let Ok(data) = std::fs::read(path) {
fonts.font_data.insert(
"system_emoji_font".to_owned(),
egui::FontData::from_owned(data),
);
// ÖNEMLİ: Emojiyi listenin SONUNA (push ile) ekliyoruz.
// Böylece egui normal harfleri sistem fontuyla yazacak,
// harf sistem fontunda yoksa (yani emojiyse) gidip bu fonttan alacak.
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.push("system_emoji_font".to_owned());
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.insert(0, "system_emoji_font".to_owned());
break;
}
}
@@ -118,6 +157,57 @@ 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() };
@@ -131,25 +221,31 @@ fn build_visuals(is_dark: bool) -> Visuals {
// Metin
v.override_text_color = None;
// Kenarlıklar
// ─── 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, 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 dolguları
// ─── WIDGET DOLGULARI (Burası ComboBox ve Butonların Kaderini Belirliyor) ───────
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();
// 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);
// Üzerine Gelme Hali: Parlayan Açık Mavi
v.widgets.hovered.bg_fill = Color32::from_rgb(59, 144, 240);
// Tıklanma/Açık Kalma Hali: Koyu Lacivert/Mavi
v.widgets.active.bg_fill = Color32::from_rgb(20, 70, 140);
// Metin renkleri
// ─── METİN RENKLERİ ────────────────────────────────────────────────────────────
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.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
// ─── YUVARLAK KÖŞELER (Zaten 6.0'dı, Tam İstediğin Calamares Kıvamında) ─────────
v.widgets.noninteractive.rounding = Rounding::same(6.0);
v.widgets.inactive.rounding = Rounding::same(6.0);
v.widgets.hovered.rounding = Rounding::same(6.0);
@@ -193,31 +289,90 @@ fn build_style() -> egui::Style {
// ─── Yardımcı widget'lar ─────────────────────────────────────────
/// Vurgulu "ana eylem" butonu (mavi dolgu).
pub fn primary_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
.fill(c_accent())
.stroke(Stroke::new(1.0, c_accent()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(100.0, 32.0))
fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
Color32::from_rgba_premultiplied(
(a.r() as f32 + (b.r() as f32 - a.r() as f32) * t) as u8,
(a.g() as f32 + (b.g() as f32 - a.g() as f32) * t) as u8,
(a.b() as f32 + (b.b() as f32 - a.b() as f32) * t) as u8,
(a.a() as f32 + (b.a() as f32 - a.a() as f32) * t) as u8,
)
}
/// İkincil "iptal/geri" butonu (şeffaf dolgu).
pub fn secondary_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(c_text_dim()))
.fill(Color32::TRANSPARENT)
.stroke(Stroke::new(1.0, c_border()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(80.0, 32.0))
fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_color: Color32, text_color: Color32) -> egui::Response {
let rounding = Rounding::same(6.0);
let min_size = Vec2::new(100.0, 32.0);
let padding = Vec2::new(12.0, 6.0);
let font_id = ui.style().text_styles.get(&egui::TextStyle::Button)
.cloned().unwrap_or(FontId::proportional(14.0));
let galley = ui.painter().layout_no_wrap(
text.to_string(),
font_id,
text_color,
);
let size = Vec2::new(
min_size.x.max(galley.size().x + 2.0 * padding.x),
min_size.y.max(galley.size().y + 2.0 * padding.y),
);
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click());
// Gradient arka plan (16 yatay şerit)
let n_strips = 16;
for i in 0..n_strips {
let t = i as f32 / n_strips as f32;
let t2 = (i + 1) as f32 / n_strips as f32;
let c = lerp_color(top_color, bottom_color, (t + t2) * 0.5);
let y0 = rect.top() + rect.height() * t;
let y1 = rect.top() + rect.height() * t2;
let strip_rect = egui::Rect::from_min_max(
egui::pos2(rect.left(), y0),
egui::pos2(rect.right(), y1),
);
ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE);
}
// Yuvarlak köşe efekti için border
ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, bottom_color));
// Hover efekti
if response.hovered() {
ui.painter().rect(rect, rounding, Color32::WHITE.linear_multiply(0.10), Stroke::NONE);
}
// Metin
ui.painter().galley(
egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0),
galley,
text_color,
);
response
}
/// Kırmızı "tehlikeli eylem" butonu (disk silme onayı vb.).
pub fn danger_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
.fill(Color32::from_rgb(0xC6, 0x28, 0x28))
.stroke(Stroke::new(1.0, c_error()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(100.0, 32.0))
/// Ana eylem butonu (mavi gradient).
pub fn primary_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(50, 130, 255), // üst: açık mavi
Color32::from_rgb(13, 110, 253), // alt: koyu mavi
Color32::WHITE)
}
/// İkincil buton (gri gradient).
pub fn secondary_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(130, 140, 150), // üst: açık gri
Color32::from_rgb(100, 110, 120), // alt: koyu gri
c_text_dim())
}
/// İptal/Danger butonu (kırmızı gradient).
pub fn danger_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(240, 83, 99), // üst: açık kırmızı
Color32::from_rgb(200, 40, 60), // alt: koyu kırmızı
Color32::WHITE)
}
/// Bölüm başlığı — büyük, açık renkli, alt çizgisiz.
@@ -246,7 +401,7 @@ pub fn error_box(ui: &mut egui::Ui, msg: &str) {
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_error(), "✗");
ui.colored_label(c_error(), "❌");
ui.colored_label(c_error(), msg);
});
});
@@ -261,7 +416,7 @@ pub fn warning_box(ui: &mut egui::Ui, msg: &str) {
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_warning(), "⚠");
ui.colored_label(c_warning(), "⚠️");
ui.colored_label(c_warning(), msg);
});
});
@@ -276,7 +431,7 @@ pub fn success_box(ui: &mut egui::Ui, msg: &str) {
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_success(), "✓");
ui.colored_label(c_success(), "✅");
ui.colored_label(c_success(), msg);
});
});