gui: overhaul games section with polished professional layout

This commit is contained in:
funman300
2026-04-19 00:56:52 -07:00
parent b81c7fd863
commit 3c1742174b
+153 -42
View File
@@ -860,6 +860,11 @@ fn launcher_card<'a>(
.style(button::secondary); .style(button::secondary);
rows.push( rows.push(
container( 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),
row![ row![
text("No games added yet.").size(12).style(|_: &Theme| text::Style { text("No games added yet.").size(12).style(|_: &Theme| text::Style {
color: Some(Color::from_rgb(0.45, 0.45, 0.45)), color: Some(Color::from_rgb(0.45, 0.45, 0.45)),
@@ -870,105 +875,189 @@ fn launcher_card<'a>(
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(6), .spacing(6),
]
.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(), .into(),
); );
} }
if scan_busy { if scan_busy {
rows.push( rows.push(
container(text("Scanning…").size(12).style(|_: &Theme| text::Style { container(
row![
text("⟳ Scanning for games…").size(12).style(|_: &Theme| text::Style {
color: Some(Color::from_rgb(0.55, 0.75, 1.0)), 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 }) ]
)
.padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 })
.into(), .into(),
); );
} }
if let Some(results) = scan_results { 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 { for (display, exe) in results {
let lname = l.name.clone(); let lname = l.name.clone();
let d = display.clone(); let d = display.clone();
let e = exe.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)) .on_press(Message::AddScannedGame(lname, d, e))
.style(button::primary); .style(button::primary)
.padding([4, 8]);
rows.push( rows.push(
container( container(
row![ row![
text(display).size(12).style(|_: &Theme| text::Style { container(
color: Some(Color::from_rgb(0.75, 0.75, 0.75)), 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(), iced::widget::horizontal_space(),
add_btn, add_btn,
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(4), .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(), .into(),
); );
} }
if results.is_empty() && !scan_busy { if results.is_empty() && !scan_busy {
rows.push( rows.push(
container( 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)), 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(), .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 { for g in &l.games {
let lname = l.name.clone(); let lname = l.name.clone();
let gname = g.name.clone(); let gname = g.name.clone();
let game_installed = g.full_exe_path(l).exists();
let play = button(text("").size(11)) let play = button(
.on_press(Message::Play(lname.clone(), gname.clone())) text("").size(12)
)
.on_press_maybe(game_installed.then_some(Message::Play(lname.clone(), gname.clone())))
.style(button::primary) .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())) .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]); .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())) .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]); .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())) .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]); .padding([3, 7]);
let remove_game = button(text("×").size(11)) let remove_game = button(
text("").size(10)
)
.on_press(Message::RemoveGame(lname, gname)) .on_press(Message::RemoveGame(lname, gname))
.style(button::danger) .style(button::danger)
.padding([3, 7]); .padding([3, 7]);
rows.push( let install_indicator = if game_installed {
container( 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![ row![
play, play,
container(install_indicator).padding(Padding { top: 0.0, left: 2.0, right: 4.0, bottom: 0.0 }),
text(&g.display).size(13), text(&g.display).size(13),
iced::widget::horizontal_space(), iced::widget::horizontal_space(),
gm_btn, row![gm_btn, hud_btn, gs_btn].spacing(3),
hud_btn, container(remove_game).padding(Padding { top: 0.0, left: 6.0, right: 0.0, bottom: 0.0 }),
gs_btn,
remove_game,
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(5), .spacing(4),
) )
.padding(Padding { top: 2.0, left: 0.0, right: 0.0, bottom: 0.0 }) .padding(Padding { top: 3.0, left: 0.0, right: 0.0, bottom: 3.0 })
.into(), .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 { if let Some((name_val, exe_val)) = add_form {
@@ -979,19 +1068,19 @@ fn launcher_card<'a>(
let lname5 = l.name.clone(); let lname5 = l.name.clone();
let name_input = text_input("Game name", name_val) let name_input = text_input("Game name", name_val)
.on_input(move |v| Message::AddGameNameChanged(lname.clone(), v)) .on_input(move |v| Message::AddGameNameChanged(lname.clone(), v))
.padding(6) .padding(7)
.size(12) .size(12)
.width(Length::FillPortion(2)); .width(Length::FillPortion(2));
let exe_input = text_input("Exe path (relative to drive_c/)", exe_val) let exe_input = text_input("Exe path (relative to drive_c/)", exe_val)
.on_input(move |v| Message::AddGameExeChanged(lname2.clone(), v)) .on_input(move |v| Message::AddGameExeChanged(lname2.clone(), v))
.padding(6) .padding(7)
.size(12) .size(12)
.width(Length::FillPortion(3)); .width(Length::FillPortion(3));
let browse_btn = button(text("Browse…").size(11)) let browse_btn = button(text("Browse…").size(11))
.on_press(Message::BrowseGameExe(lname5)) .on_press(Message::BrowseGameExe(lname5))
.style(button::secondary); .style(button::secondary);
let can_confirm = !name_val.trim().is_empty() && !exe_val.trim().is_empty(); 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))) .on_press_maybe(can_confirm.then_some(Message::AddGameConfirm(lname3)))
.style(button::primary); .style(button::primary);
let cancel_btn = button(text("Cancel").size(11)) let cancel_btn = button(text("Cancel").size(11))
@@ -1000,33 +1089,55 @@ fn launcher_card<'a>(
rows.push( rows.push(
container( container(
column![ 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] row![name_input, exe_input, browse_btn]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(6), .spacing(6),
container( container(
row![confirm_btn, cancel_btn].spacing(6), row![cancel_btn, confirm_btn].spacing(6),
) )
.width(Length::Fill) .width(Length::Fill)
.align_x(Alignment::End), .align_x(Alignment::End),
] ]
.spacing(6), .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(), .into(),
); );
} }
let add_game_btn = { // ── Game toolbar ──────────────────────────────────────────────────────────
let lname = l.name.clone(); let lname_add = l.name.clone();
let label = if add_form.is_some() { " Game" } else { "+ Game" }; let lname_scan = l.name.clone();
button(text(label).size(11)) let lname_browse = l.name.clone();
.on_press(Message::AddGamePressed(lname))
.style(button::secondary) 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( rows.push(
container(add_game_btn) container(
.padding(Padding { top: 6.0, right: 0.0, bottom: 0.0, left: 0.0 }) 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(), .into(),
); );