diff --git a/src/gui.rs b/src/gui.rs index 0ca4d26..6c0e983 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -860,115 +860,204 @@ fn launcher_card<'a>( .style(button::secondary); rows.push( container( - row![ - text("No games added yet.").size(12).style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.45, 0.45, 0.45)), + column![ + text("Games").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.5, 0.5, 0.5)), }), - iced::widget::horizontal_space(), - scan_btn, - browse_btn, + iced::widget::horizontal_rule(1), + 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), ] - .align_y(Alignment::Center) .spacing(6), ) - .padding(Padding { top: 4.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .padding(Padding { top: 8.0, left: 2.0, right: 0.0, bottom: 0.0 }) .into(), ); } if scan_busy { 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 }) + container( + row![ + text("⟳ Scanning for games…").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.55, 0.75, 1.0)), + }), + ] + ) + .padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 }) .into(), ); } if let Some(results) = scan_results { + if !results.is_empty() { + rows.push( + container( + column![ + text("Detected Games").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.5, 0.5, 0.5)), + }), + iced::widget::horizontal_rule(1), + ] + .spacing(4), + ) + .padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .into(), + ); + } for (display, exe) in results { let lname = l.name.clone(); let d = display.clone(); let e = exe.clone(); - let add_btn = button(text("+ Add").size(11)) + let add_btn = button( + text("\u{f64d}").font(iced::Font::with_name("bootstrap-icons")).size(12) + ) .on_press(Message::AddScannedGame(lname, d, e)) - .style(button::primary); + .style(button::primary) + .padding([4, 8]); rows.push( container( row![ - text(display).size(12).style(|_: &Theme| text::Style { - color: Some(Color::from_rgb(0.75, 0.75, 0.75)), - }), + container( + text("○").size(8).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.4, 0.4, 0.4)), + }) + ).padding(Padding { top: 0.0, left: 4.0, right: 4.0, bottom: 0.0 }), + column![ + text(display).size(13).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.8, 0.8, 0.8)), + }), + text(exe).size(10).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.4, 0.4, 0.4)), + }), + ].spacing(1), 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 }) + .padding(Padding { top: 2.0, left: 2.0, right: 0.0, bottom: 2.0 }) .into(), ); } if results.is_empty() && !scan_busy { rows.push( container( - text("No new executables found.").size(11).style(|_: &Theme| text::Style { + text("No new games found in this prefix.").size(11).style(|_: &Theme| text::Style { color: Some(Color::from_rgb(0.45, 0.45, 0.45)), }), ) - .padding(Padding { top: 4.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 }) .into(), ); } } + // ── Configured games section ──────────────────────────────────────────── + if !l.games.is_empty() { + rows.push( + container( + column![ + text("Games").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.5, 0.5, 0.5)), + }), + iced::widget::horizontal_rule(1), + ] + .spacing(4), + ) + .padding(Padding { top: 8.0, left: 2.0, right: 0.0, bottom: 0.0 }) + .into(), + ); + } + for g in &l.games { let lname = l.name.clone(); let gname = g.name.clone(); + let game_installed = g.full_exe_path(l).exists(); - let play = button(text("▶").size(11)) - .on_press(Message::Play(lname.clone(), gname.clone())) + let play = button( + text("▶").size(12) + ) + .on_press_maybe(game_installed.then_some(Message::Play(lname.clone(), gname.clone()))) .style(button::primary) - .padding([4, 8]); + .padding([5, 10]); - let gm_btn = button(text("GM").size(10)) + let gm_label = if g.gamemode { "GM ✓" } else { "GM" }; + let hud_label = if g.mangohud { "HUD ✓" } else { "HUD" }; + let gs_label = if g.gamescope.is_some() { "GS ✓" } else { "GS" }; + + let gm_btn = button(text(gm_label).size(10)) .on_press(Message::ToggleGameMode(lname.clone(), gname.clone())) .style(if g.gamemode { button::primary } else { button::secondary }) .padding([3, 7]); - let hud_btn = button(text("HUD").size(10)) + let hud_btn = button(text(hud_label).size(10)) .on_press(Message::ToggleMangoHud(lname.clone(), gname.clone())) .style(if g.mangohud { button::primary } else { button::secondary }) .padding([3, 7]); - let gs_btn = button(text("GS").size(10)) + let gs_btn = button(text(gs_label).size(10)) .on_press(Message::ToggleGamescope(lname.clone(), gname.clone())) .style(if g.gamescope.is_some() { button::primary } else { button::secondary }) .padding([3, 7]); - let remove_game = button(text("×").size(11)) + let remove_game = button( + text("✕").size(10) + ) .on_press(Message::RemoveGame(lname, gname)) .style(button::danger) .padding([3, 7]); - rows.push( - 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(), - ); + let install_indicator = if game_installed { + text("●").size(8).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.35, 0.85, 0.45)), + }) + } else { + text("●").size(8).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.5, 0.5, 0.5)), + }) + }; + + let game_row = container( + row![ + play, + container(install_indicator).padding(Padding { top: 0.0, left: 2.0, right: 4.0, bottom: 0.0 }), + text(&g.display).size(13), + iced::widget::horizontal_space(), + row![gm_btn, hud_btn, gs_btn].spacing(3), + container(remove_game).padding(Padding { top: 0.0, left: 6.0, right: 0.0, bottom: 0.0 }), + ] + .align_y(Alignment::Center) + .spacing(4), + ) + .padding(Padding { top: 3.0, left: 0.0, right: 0.0, bottom: 3.0 }) + .width(Length::Fill) + .style(|theme: &Theme| { + let p = theme.extended_palette(); + container::Style { + background: Some(Background::Color(Color { + a: 0.3, + ..p.background.strong.color + })), + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 4.0.into(), + }, + ..Default::default() + } + }); + + rows.push(game_row.into()); } if let Some((name_val, exe_val)) = add_form { @@ -979,19 +1068,19 @@ 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(6) + .padding(7) .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(6) + .padding(7) .size(12) .width(Length::FillPortion(3)); let browse_btn = button(text("Browse…").size(11)) .on_press(Message::BrowseGameExe(lname5)) .style(button::secondary); let can_confirm = !name_val.trim().is_empty() && !exe_val.trim().is_empty(); - let confirm_btn = button(text("Add").size(11)) + let confirm_btn = button(text("Add Game").size(11)) .on_press_maybe(can_confirm.then_some(Message::AddGameConfirm(lname3))) .style(button::primary); let cancel_btn = button(text("Cancel").size(11)) @@ -1000,34 +1089,56 @@ fn launcher_card<'a>( rows.push( container( column![ + text("Add Game Manually").size(12).style(|_: &Theme| text::Style { + color: Some(Color::from_rgb(0.5, 0.5, 0.5)), + }), row![name_input, exe_input, browse_btn] .align_y(Alignment::Center) .spacing(6), container( - row![confirm_btn, cancel_btn].spacing(6), + row![cancel_btn, confirm_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 }) + .padding(Padding { top: 8.0, left: 0.0, right: 0.0, bottom: 0.0 }) .into(), ); } - let add_game_btn = { - let lname = l.name.clone(); - let label = if add_form.is_some() { "− Game" } else { "+ Game" }; - button(text(label).size(11)) - .on_press(Message::AddGamePressed(lname)) - .style(button::secondary) - }; + // ── Game toolbar ────────────────────────────────────────────────────────── + let lname_add = l.name.clone(); + let lname_scan = l.name.clone(); + let lname_browse = l.name.clone(); + + let add_label = if add_form.is_some() { "Cancel" } else { "+ Add Game" }; + let add_game_btn = button(text(add_label).size(11)) + .on_press(Message::AddGamePressed(lname_add)) + .style(button::secondary); + + let scan_btn = button(text("Scan").size(11)) + .on_press_maybe((!scan_busy).then_some(Message::ScanGames(lname_scan))) + .style(button::secondary); + + let browse_btn = button(text("Browse exe…").size(11)) + .on_press(Message::BrowseGameExe(lname_browse)) + .style(button::secondary); rows.push( - container(add_game_btn) - .padding(Padding { top: 6.0, right: 0.0, bottom: 0.0, left: 0.0 }) - .into(), + container( + row![ + add_game_btn, + iced::widget::horizontal_space(), + scan_btn, + browse_btn, + ] + .align_y(Alignment::Center) + .spacing(6), + ) + .padding(Padding { top: 8.0, right: 0.0, bottom: 0.0, left: 0.0 }) + .into(), ); Column::with_children(rows).spacing(5).into()