Skip to content

Commit

Permalink
Use custom dialogs in Steam Deck game mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Dec 23, 2024
1 parent ef32a5f commit 4824a1c
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 19 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
* When the game list is filtered,
the summary line (e.g., "1 of 10 games") now reflects the filtered totals.
* The `enable/disable all` buttons are now constrained by the active filter.
* CLI: On Steam Deck, when game mode is active,
the `wrap --gui` command will use custom dialogs instead of native ones,
because native dialogs don't work properly in game mode.
* Fixed:
* If a custom game's title begins or ends with a space,
that custom game will now be ignored.
Expand Down
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,12 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
};
println!("{serialized}");
}
Subcommand::Dialog { kind, message } => {
if let Err(e) = crate::gui::dialog::run(config.theme, kind, message) {
log::error!("Failed to run custom dialog: {e:?}");
failed = true;
}
}
}
if failed {
Err(Error::SomeEntriesFailed)
Expand Down
9 changes: 9 additions & 0 deletions src/cli/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,15 @@ pub enum Subcommand {
#[clap(subcommand)]
kind: SchemaSubcommand,
},
/// For internal use; not a stable interface.
/// Show custom dialogs.
#[clap(hide = true)]
Dialog {
#[clap(long, value_parser = possible_values!(crate::gui::dialog::Kind, ALL_CLI))]
kind: crate::gui::dialog::Kind,
#[clap(long)]
message: String,
},
}

#[derive(clap::Subcommand, Clone, Debug, PartialEq, Eq)]
Expand Down
69 changes: 50 additions & 19 deletions src/cli/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
use crate::{lang::TRANSLATOR, prelude::Error};
use crate::{
lang::TRANSLATOR,
prelude::{Error, STEAM_DECK_GAME_MODE},
};

enum System {
Native,
Iced,
}

impl System {
fn best() -> Self {
if *STEAM_DECK_GAME_MODE {
// Native dialogs don't work in game mode.
Self::Iced
} else {
Self::Native
}
}
}

/// GUI looks nicer with an extra empty line as separator, but for terminals a single
/// newline is sufficient
Expand Down Expand Up @@ -45,12 +64,19 @@ pub fn alert_with_error(gui: bool, force: bool, msg: &str, error: &Error) -> Res
pub fn alert(gui: bool, force: bool, msg: &str) -> Result<(), Error> {
log::debug!("Showing alert to user (GUI={}, force={}): {}", gui, force, msg);
if gui {
rfd::MessageDialog::new()
.set_title(TRANSLATOR.app_name())
.set_description(msg)
.set_level(rfd::MessageLevel::Error)
.set_buttons(rfd::MessageButtons::Ok)
.show();
match System::best() {
System::Native => {
rfd::MessageDialog::new()
.set_title(TRANSLATOR.app_name())
.set_description(msg)
.set_level(rfd::MessageLevel::Error)
.set_buttons(rfd::MessageButtons::Ok)
.show();
}
System::Iced => {
crate::gui::dialog::error(msg);
}
}
Ok(())
} else if !force {
// TODO: Dialoguer doesn't have an alert type.
Expand Down Expand Up @@ -79,18 +105,23 @@ pub fn confirm(gui: bool, force: Option<bool>, msg: &str) -> Result<bool, Error>
}

if gui {
let choice = match rfd::MessageDialog::new()
.set_title(TRANSLATOR.app_name())
.set_description(msg)
.set_level(rfd::MessageLevel::Info)
.set_buttons(rfd::MessageButtons::YesNo)
.show()
{
rfd::MessageDialogResult::Yes => true,
rfd::MessageDialogResult::No => false,
rfd::MessageDialogResult::Ok => true,
rfd::MessageDialogResult::Cancel => false,
rfd::MessageDialogResult::Custom(_) => false,
let choice = match System::best() {
System::Native => {
match rfd::MessageDialog::new()
.set_title(TRANSLATOR.app_name())
.set_description(msg)
.set_level(rfd::MessageLevel::Info)
.set_buttons(rfd::MessageButtons::YesNo)
.show()
{
rfd::MessageDialogResult::Yes => true,
rfd::MessageDialogResult::No => false,
rfd::MessageDialogResult::Ok => true,
rfd::MessageDialogResult::Cancel => false,
rfd::MessageDialogResult::Custom(_) => false,
}
}
System::Iced => crate::gui::dialog::confirm(msg),
};
log::debug!("User responded: {}", choice);
Ok(choice)
Expand Down
1 change: 1 addition & 0 deletions src/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod app;
mod badge;
mod button;
mod common;
pub mod dialog;
mod editor;
mod file_tree;
mod font;
Expand Down
225 changes: 225 additions & 0 deletions src/gui/dialog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
use iced::{
alignment,
widget::{button, text, Column, Container, Row},
Alignment, Element, Length, Size, Task,
};

use crate::{
gui::icon::Icon,
lang::TRANSLATOR,
prelude::{run_command, Privacy},
resource::config,
};

const POSITIVE_CHOICE: &str = "::ludusavi-positive::";

#[allow(unused)]
pub fn info(message: &str) {
show(Kind::Info, message);
}

pub fn error(message: &str) {
show(Kind::Error, message);
}

pub fn confirm(message: &str) -> bool {
show(Kind::Confirm, message)
}

pub fn show(kind: Kind, message: &str) -> bool {
let exe = std::env::current_exe().unwrap().to_string_lossy().to_string();
match run_command(
&exe,
&["dialog", "--kind", kind.slug(), "--message", message],
&[0],
Privacy::Public,
) {
Ok(info) => info.stdout.contains(POSITIVE_CHOICE),
Err(e) => {
log::error!("Failed to show custom dialog: {e:?}");
false
}
}
}

pub fn run(theme: config::Theme, kind: Kind, message: String) -> iced::Result {
let app = iced::application(DialogApp::title, DialogApp::update, DialogApp::view)
.theme(DialogApp::theme)
.settings(iced::Settings {
default_font: crate::gui::font::TEXT,
..Default::default()
})
.window(iced::window::Settings {
min_size: Some(Size::new(320.0, 180.0)),
exit_on_close_request: true,
position: iced::window::Position::Centered,
#[cfg(target_os = "linux")]
platform_specific: iced::window::settings::PlatformSpecific {
application_id: std::env::var(crate::prelude::ENV_LINUX_APP_ID)
.unwrap_or_else(|_| crate::prelude::LINUX_APP_ID.to_string()),
..Default::default()
},
icon: match image::load_from_memory(include_bytes!("../../assets/icon.png")) {
Ok(buffer) => {
let buffer = buffer.to_rgba8();
let width = buffer.width();
let height = buffer.height();
let dynamic_image = image::DynamicImage::ImageRgba8(buffer);
match iced::window::icon::from_rgba(dynamic_image.into_bytes(), width, height) {
Ok(icon) => Some(icon),
Err(_) => None,
}
}
Err(_) => None,
},
..Default::default()
});

app.run_with(move || {
(
DialogApp::new(theme, kind, message),
Task::batch([
iced::font::load(std::borrow::Cow::Borrowed(crate::gui::font::TEXT_DATA)).map(|_| Message::Ignore),
iced::font::load(std::borrow::Cow::Borrowed(crate::gui::font::ICONS_DATA)).map(|_| Message::Ignore),
iced::window::get_oldest().and_then(iced::window::gain_focus),
iced::window::get_oldest().and_then(|id| iced::window::resize(id, iced::Size::new(320.0, 180.0))),
]),
)
})
}

fn icon<'a>(icon: Icon) -> Element<'a, Message, crate::gui::style::Theme> {
text(icon.as_char().to_string())
.font(crate::gui::font::ICONS)
.size(40)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.into()
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub enum Kind {
Info,
Error,
Confirm,
}

impl Kind {
pub const ALL_CLI: &'static [&'static str] = &[Self::INFO, Self::ERROR, Self::CONFIRM];
const INFO: &'static str = "info";
const ERROR: &'static str = "error";
const CONFIRM: &'static str = "confirm";
}

impl Kind {
pub fn slug(&self) -> &str {
match self {
Self::Info => Self::INFO,
Self::Error => Self::ERROR,
Self::Confirm => Self::CONFIRM,
}
}
}

impl std::str::FromStr for Kind {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
Self::INFO => Ok(Self::Info),
Self::ERROR => Ok(Self::Error),
Self::CONFIRM => Ok(Self::Confirm),
_ => Err(format!("invalid dialog kind: {}", s)),
}
}
}

struct DialogApp {
theme: config::Theme,
kind: Kind,
message: String,
positive: String,
negative: String,
}

#[derive(Debug, Clone, Copy)]
enum Message {
Ignore,
Positive,
Negative,
}

impl DialogApp {
fn new(theme: config::Theme, kind: Kind, message: String) -> Self {
let positive = match kind {
Kind::Info => TRANSLATOR.okay_button(),
Kind::Error => TRANSLATOR.okay_button(),
Kind::Confirm => TRANSLATOR.continue_button(),
};

let negative = TRANSLATOR.cancel_button();

Self {
theme,
kind,
message,
positive,
negative,
}
}

fn theme(&self) -> crate::gui::style::Theme {
crate::gui::style::Theme::from(self.theme)
}

fn title(&self) -> String {
TRANSLATOR.app_name()
}

fn update(&mut self, message: Message) {
match message {
Message::Ignore => {}
Message::Positive => {
println!("{POSITIVE_CHOICE}");
std::process::exit(0);
}
Message::Negative => {
std::process::exit(0);
}
}
}

fn view(&self) -> Element<Message, crate::gui::style::Theme> {
Container::new(
Column::new()
.spacing(20)
.padding(20)
.width(Length::Fill)
.align_x(Alignment::Center)
.push(
Row::new()
.spacing(20)
.align_y(Alignment::Center)
.push(match self.kind {
Kind::Info => icon(Icon::Info),
Kind::Error => icon(Icon::Error),
Kind::Confirm => icon(Icon::Question),
})
.push(text(&self.message)),
)
.push(
Row::new()
.spacing(20)
.align_y(Alignment::Center)
.push(button(text(&self.positive)).on_press(Message::Positive))
.push_maybe(match self.kind {
Kind::Info => None,
Kind::Error => None,
Kind::Confirm => Some(button(text(&self.negative)).on_press(Message::Negative)),
}),
),
)
.center(Length::Fill)
.into()
}
}
2 changes: 2 additions & 0 deletions src/gui/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub enum Icon {
OpenInBrowser,
OpenInNew,
PlayCircleOutline,
Question,
Refresh,
Remove,
RemoveCircle,
Expand Down Expand Up @@ -72,6 +73,7 @@ impl Icon {
Self::OpenInBrowser => '\u{e89d}',
Self::OpenInNew => '\u{E89E}',
Self::PlayCircleOutline => '\u{E039}',
Self::Question => '\u{e8fd}',
Self::Refresh => '\u{E5D5}',
Self::Remove => '\u{E15B}',
Self::RemoveCircle => '\u{E15C}',
Expand Down
2 changes: 2 additions & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub const INVALID_FILE_CHARS: &[char] = &['\\', '/', ':', '*', '?', '"', '<', '>

pub static STEAM_DECK: LazyLock<bool> =
LazyLock::new(|| Os::HOST == Os::Linux && StrictPath::new("/home/deck".to_string()).exists());
pub static STEAM_DECK_GAME_MODE: LazyLock<bool> =
LazyLock::new(|| Os::HOST == Os::Linux && std::env::var("SteamDeck").is_ok_and(|x| &x == "1"));
pub static OS_USERNAME: LazyLock<String> = LazyLock::new(whoami::username);

pub static AVAILABLE_PARALELLISM: LazyLock<Option<NonZeroUsize>> =
Expand Down

0 comments on commit 4824a1c

Please sign in to comment.