settings: add Launch Protontricks button

This commit is contained in:
funman300
2026-04-19 00:47:26 -07:00
parent 32c6e1fce0
commit a1afa59f1a
+149 -95
View File
@@ -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<Message> {
}
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<Message> = 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<Message> = {
let n = l.name.clone();
@@ -810,27 +817,37 @@ fn launcher_card<'a>(
};
let version_badge: Element<Message> = 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<Element<Message>> = 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<Element<Message>> = 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<Message> = 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<Element<Message>> = 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),