diff --git a/src/gui.rs b/src/gui.rs index f7fa58d..0ca4d26 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -61,6 +61,7 @@ pub enum Message { ServiceInstall, ServiceUninstall, ServiceActionDone(Result<(), String>), + LaunchProtontricks, } struct Dashboard { @@ -538,6 +539,14 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { } Task::none() } + Message::LaunchProtontricks => { + std::thread::spawn(|| { + let _ = std::process::Command::new("protontricks") + .arg("--gui") + .spawn(); + }); + Task::none() + } Message::ServiceInstall => { state.service_busy = true; state.service_status = "Installing autostart…".into(); @@ -778,16 +787,14 @@ fn launcher_card<'a>( scan_busy: bool, ) -> Element<'a, Message> { let status_color = if running { - Color::from_rgb(0.4, 0.9, 0.4) + Color::from_rgb(0.35, 0.85, 0.45) } else if installed { Color::from_rgb(0.55, 0.75, 1.0) } else { - Color::from_rgb(0.5, 0.5, 0.5) + Color::from_rgb(0.45, 0.45, 0.45) }; - let status_str = if running { "● Running" } else if installed { "○ Installed" } else { "· Not installed" }; - let status_el: Element = text(status_str).size(12).style(move |_: &Theme| text::Style { - color: Some(status_color), - }).into(); + let status_dot = if running { "●" } else if installed { "●" } else { "○" }; + let status_str = if running { "Running" } else if installed { "Installed" } else { "Not installed" }; let action: Element = { let n = l.name.clone(); @@ -810,27 +817,37 @@ fn launcher_card<'a>( }; let version_badge: Element = if let Some(v) = &l.proton_version { - text(format!(" [{v}]")) - .size(11) + text(format!(" {v}")) + .size(10) .style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.55, 0.75, 1.0)), + color: Some(Color::from_rgb(0.45, 0.55, 0.7)), }) .into() } else { text("").into() }; - let header = row![ + let name_row = row![ text(&l.display).size(15), - text(" ").size(12), - status_el, - version_badge, iced::widget::horizontal_space(), action, ] .align_y(Alignment::Center); - let mut rows: Vec> = vec![header.into()]; + let status_row = row![ + text(status_dot).size(10).style(move |_: &Theme| text::Style { + color: Some(status_color), + }), + text(format!(" {status_str}")).size(11).style(move |_: &Theme| text::Style { + color: Some(status_color), + }), + version_badge, + ] + .align_y(Alignment::Center); + + let mut rows: Vec> = vec![ + column![name_row, status_row].spacing(3).into(), + ]; if l.games.is_empty() && scan_results.map(|r| r.is_empty()).unwrap_or(true) && !scan_busy { let lname = l.name.clone(); @@ -842,65 +859,66 @@ fn launcher_card<'a>( .on_press(Message::BrowseGameExe(lname2)) .style(button::secondary); rows.push( - row![ - text(" ").size(12), - text("No games added yet.").size(12).style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.55, 0.55, 0.55)), - }), - iced::widget::horizontal_space(), - scan_btn, - browse_btn, - ] - .align_y(Alignment::Center) - .spacing(6) + container( + row![ + text("No games added yet.").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.45, 0.45, 0.45)), + }), + iced::widget::horizontal_space(), + scan_btn, + browse_btn, + ] + .align_y(Alignment::Center) + .spacing(6), + ) + .padding(Padding { top: 4.0, left: 2.0, right: 0.0, bottom: 0.0 }) .into(), ); } if scan_busy { - rows.push(text(" Scanning…").size(12).into()); + rows.push( + container(text("Scanning…").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.55, 0.75, 1.0)), + })) + .padding(Padding { top: 4.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .into(), + ); } - // Scan results (exes found in prefix, not yet added) if let Some(results) = scan_results { for (display, exe) in results { let lname = l.name.clone(); let d = display.clone(); let e = exe.clone(); - let add_btn = button(text("+").size(11)) + let add_btn = button(text("+ Add").size(11)) .on_press(Message::AddScannedGame(lname, d, e)) .style(button::primary); rows.push( - row![ - text(" ").size(12), - text(display).size(12).style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.75, 0.75, 0.75)), - }), - iced::widget::horizontal_space(), - add_btn, - ] - .align_y(Alignment::Center) - .spacing(4) + container( + row![ + text(display).size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.75, 0.75, 0.75)), + }), + iced::widget::horizontal_space(), + add_btn, + ] + .align_y(Alignment::Center) + .spacing(4), + ) + .padding(Padding { top: 2.0, left: 2.0, right: 0.0, bottom: 0.0 }) .into(), ); } - if !results.is_empty() { + if results.is_empty() && !scan_busy { rows.push( - text(" Found in Wine prefix — click + to add") - .size(10) - .style(|_: &Theme| text::Style { + container( + text("No new executables found.").size(11).style(|_: &Theme| text::Style { color: Some(Color::from_rgb(0.45, 0.45, 0.45)), - }) - .into(), - ); - } else if !scan_busy { - rows.push( - text(" No new executables found in the Wine prefix.") - .size(11) - .style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.45, 0.45, 0.45)), - }) - .into(), + }), + ) + .padding(Padding { top: 4.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .into(), ); } } @@ -911,42 +929,48 @@ fn launcher_card<'a>( let play = button(text("▶").size(11)) .on_press(Message::Play(lname.clone(), gname.clone())) - .style(button::primary); + .style(button::primary) + .padding([4, 8]); - let gamemode_btn = button(text("GameMode").size(11)) + let gm_btn = button(text("GM").size(10)) .on_press(Message::ToggleGameMode(lname.clone(), gname.clone())) - .style(if g.gamemode { button::primary } else { button::secondary }); + .style(if g.gamemode { button::primary } else { button::secondary }) + .padding([3, 7]); - let mangohud_btn = button(text("MangoHud").size(11)) + let hud_btn = button(text("HUD").size(10)) .on_press(Message::ToggleMangoHud(lname.clone(), gname.clone())) - .style(if g.mangohud { button::primary } else { button::secondary }); + .style(if g.mangohud { button::primary } else { button::secondary }) + .padding([3, 7]); - let gamescope_btn = button(text("Gamescope").size(11)) + let gs_btn = button(text("GS").size(10)) .on_press(Message::ToggleGamescope(lname.clone(), gname.clone())) - .style(if g.gamescope.is_some() { button::primary } else { button::secondary }); + .style(if g.gamescope.is_some() { button::primary } else { button::secondary }) + .padding([3, 7]); - let remove_game = button(text("✕").size(10)) + let remove_game = button(text("×").size(11)) .on_press(Message::RemoveGame(lname, gname)) - .style(button::danger); + .style(button::danger) + .padding([3, 7]); rows.push( - row![ - text(" ").size(13), - play, - text(&g.display).size(13), - iced::widget::horizontal_space(), - gamemode_btn, - mangohud_btn, - gamescope_btn, - remove_game, - ] - .align_y(Alignment::Center) - .spacing(6) + container( + row![ + play, + text(&g.display).size(13), + iced::widget::horizontal_space(), + gm_btn, + hud_btn, + gs_btn, + remove_game, + ] + .align_y(Alignment::Center) + .spacing(5), + ) + .padding(Padding { top: 2.0, left: 0.0, right: 0.0, bottom: 0.0 }) .into(), ); } - // Inline add-game form if let Some((name_val, exe_val)) = add_form { let lname = l.name.clone(); let lname2 = l.name.clone(); @@ -955,12 +979,12 @@ fn launcher_card<'a>( let lname5 = l.name.clone(); let name_input = text_input("Game name", name_val) .on_input(move |v| Message::AddGameNameChanged(lname.clone(), v)) - .padding(5) + .padding(6) .size(12) .width(Length::FillPortion(2)); let exe_input = text_input("Exe path (relative to drive_c/)", exe_val) .on_input(move |v| Message::AddGameExeChanged(lname2.clone(), v)) - .padding(5) + .padding(6) .size(12) .width(Length::FillPortion(3)); let browse_btn = button(text("Browse…").size(11)) @@ -974,10 +998,21 @@ fn launcher_card<'a>( .on_press(Message::AddGamePressed(lname4)) .style(button::secondary); rows.push( - row![name_input, exe_input, browse_btn, confirm_btn, cancel_btn] - .align_y(Alignment::Center) - .spacing(6) - .into(), + container( + column![ + row![name_input, exe_input, browse_btn] + .align_y(Alignment::Center) + .spacing(6), + container( + row![confirm_btn, cancel_btn].spacing(6), + ) + .width(Length::Fill) + .align_x(Alignment::End), + ] + .spacing(6), + ) + .padding(Padding { top: 4.0, left: 0.0, right: 0.0, bottom: 0.0 }) + .into(), ); } @@ -991,16 +1026,22 @@ fn launcher_card<'a>( rows.push( container(add_game_btn) - .padding(Padding { top: 4.0, right: 0.0, bottom: 0.0, left: 0.0 }) + .padding(Padding { top: 6.0, right: 0.0, bottom: 0.0, left: 0.0 }) .into(), ); - Column::with_children(rows).spacing(6).into() + Column::with_children(rows).spacing(5).into() } fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> { let header = row![ - text(&l.display).size(15), + column![ + text(&l.display).size(15), + text("Actions").size(11).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.45, 0.45, 0.45)), + }), + ] + .spacing(2), iced::widget::horizontal_space(), button(text("✕").size(12)) .on_press(Message::HideContextMenu) @@ -1008,17 +1049,17 @@ fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> { ] .align_y(Alignment::Center); - let open_prefix = button(text("Open prefix folder").size(13)) + let open_prefix = button(text("Open install folder").size(13)) .on_press(Message::OpenPrefix(l.name.clone())) .style(button::secondary) .width(Length::Fill); - let rerun_setup = button(text("Re-run setup").size(13)) + let rerun_setup = button(text("Re-run setup wizard").size(13)) .on_press(Message::RerunSetup(l.name.clone())) .style(button::secondary) .width(Length::Fill); - let diagnose = button(text("Diagnose").size(13)) + let diagnose = button(text("Run diagnostics").size(13)) .on_press(Message::DiagnosePressed(l.name.clone())) .style(button::secondary) .width(Length::Fill); @@ -1036,7 +1077,7 @@ fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> { diagnose, remove, ] - .spacing(4) + .spacing(5) .into() } @@ -1045,7 +1086,13 @@ fn diagnose_card<'a>( checks: Option<&'a [diagnose::CheckResult]>, ) -> Element<'a, Message> { let header = row![ - text(format!("Diagnose: {}", l.display)).size(15), + column![ + text(format!("Diagnostics")).size(15), + text(&l.display).size(11).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.55, 0.75, 1.0)), + }), + ] + .spacing(2), iced::widget::horizontal_space(), button(text("✕").size(12)) .on_press(Message::HideDiagnose) @@ -1054,15 +1101,17 @@ fn diagnose_card<'a>( .align_y(Alignment::Center); let body: Element = match checks { - None => text("Running checks…").size(12).into(), + None => text("Running checks…").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.55, 0.75, 1.0)), + }).into(), Some(results) => { let rows: Vec> = results .iter() .map(|c| { let (sym, color) = if c.pass { - ("✓", Color::from_rgb(0.4, 0.9, 0.4)) + ("✓", Color::from_rgb(0.35, 0.85, 0.45)) } else { - ("✗", Color::from_rgb(1.0, 0.45, 0.45)) + ("✗", Color::from_rgb(1.0, 0.45, 0.35)) }; row![ text(sym).size(12).style(move |_: &Theme| text::Style { @@ -1074,14 +1123,14 @@ fn diagnose_card<'a>( .into() }) .collect(); - scrollable(Column::with_children(rows).spacing(3)) + scrollable(Column::with_children(rows).spacing(4)) .height(Length::Fixed(160.0)) .into() } }; - column![header, iced::widget::horizontal_rule(1), body,] - .spacing(6) + column![header, iced::widget::horizontal_rule(1), body] + .spacing(8) .into() } @@ -1152,6 +1201,11 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> { row![compat_dir_input, browse_compat_btn].spacing(8).align_y(Alignment::Center), save_btn, iced::widget::horizontal_rule(1), + text("Tools").size(13), + button(text("Launch Protontricks").size(13)) + .on_press(Message::LaunchProtontricks) + .style(button::secondary), + iced::widget::horizontal_rule(1), text(svc_status_label).size(13), row![svc_install_btn, svc_uninstall_btn,].spacing(10), text(&state.service_status).size(12),