add topbar dragging! also fix a lot of perf issues with rerenders

This commit is contained in:
RabbyDevs 2025-04-18 14:11:14 +03:00
parent 574340749b
commit 43d76ca7a0
4 changed files with 128 additions and 69 deletions

25
Cargo.lock generated
View file

@ -2071,7 +2071,7 @@ dependencies = [
"log", "log",
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"spirv", "spirv",
"strum", "strum 0.26.3",
"termcolor", "termcolor",
"thiserror 2.0.12", "thiserror 2.0.12",
"unicode-xid", "unicode-xid",
@ -2845,6 +2845,8 @@ dependencies = [
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"strum 0.27.1",
"strum_macros 0.27.1",
"tempfile", "tempfile",
"url", "url",
] ]
@ -3225,9 +3227,15 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [ dependencies = [
"strum_macros", "strum_macros 0.26.4",
] ]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.26.4" version = "0.26.4"
@ -3241,6 +3249,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "svg_fmt" name = "svg_fmt"
version = "0.4.4" version = "0.4.4"

View file

@ -15,6 +15,8 @@ iced_video_player = {path = "./iced_video_player"}
url = "2.5.4" url = "2.5.4"
rust-embed = "8.7.0" rust-embed = "8.7.0"
file-format = "0.26.0" file-format = "0.26.0"
strum = "0.27.1"
strum_macros = "0.27.1"
[profile.release] [profile.release]
strip = true # Automatically strip symbols from the binary. strip = true # Automatically strip symbols from the binary.

View file

@ -10,27 +10,27 @@ use std::{marker::PhantomData, sync::atomic::Ordering};
use std::{sync::Arc, time::Instant}; use std::{sync::Arc, time::Instant};
/// Video player widget which displays the current frame of a [`Video`](crate::Video). /// Video player widget which displays the current frame of a [`Video`](crate::Video).
pub struct VideoPlayer<Message, Theme = iced::Theme, Renderer = iced::Renderer> pub struct VideoPlayer<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
where where
Renderer: PrimitiveRenderer Renderer: PrimitiveRenderer
{ {
video: Video, video: &'a Video,
content_fit: iced::ContentFit, content_fit: iced::ContentFit,
width: iced::Length, width: iced::Length,
height: iced::Length, height: iced::Length,
on_end_of_stream: Option<Message>, on_end_of_stream: Option<Message>,
on_new_frame: Option<Message>, on_new_frame: Option<Message>,
on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message>>, on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message + 'a>>,
on_error: Option<Box<dyn Fn(&glib::Error) -> Message>>, on_error: Option<Box<dyn Fn(&glib::Error) -> Message + 'a>>,
_phantom: PhantomData<(Theme, Renderer)>, _phantom: PhantomData<(Theme, Renderer)>,
} }
impl<Message, Theme, Renderer> VideoPlayer<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> VideoPlayer<'a, Message, Theme, Renderer>
where where
Renderer: PrimitiveRenderer Renderer: PrimitiveRenderer
{ {
/// Creates a new video player widget for a given video. /// Creates a new video player widget for a given video.
pub fn new(video: Video) -> Self { pub fn new(video: &'a Video) -> Self {
VideoPlayer { VideoPlayer {
video, video,
content_fit: iced::ContentFit::default(), content_fit: iced::ContentFit::default(),
@ -108,7 +108,7 @@ where
} }
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for VideoPlayer<Message, Theme, Renderer> for VideoPlayer<'_, Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Renderer: PrimitiveRenderer, Renderer: PrimitiveRenderer,
@ -292,14 +292,14 @@ where
} }
} }
impl<Message, Theme, Renderer> From<VideoPlayer<Message, Theme, Renderer>> impl<'a, Message, Theme, Renderer> From<VideoPlayer<'a, Message, Theme, Renderer>>
for Element<'_, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'static + Clone, Message: 'a + Clone,
Theme: 'static, Theme: 'a,
Renderer: 'static + PrimitiveRenderer, Renderer: 'a + PrimitiveRenderer,
{ {
fn from(video_player: VideoPlayer<Message, Theme, Renderer>) -> Self { fn from(video_player: VideoPlayer<'a, Message, Theme, Renderer>) -> Self {
Self::new(video_player) Self::new(video_player)
} }
} }

View file

@ -1,19 +1,19 @@
mod utils;
use file_format::FileFormat; use file_format::FileFormat;
use ::image::ImageReader; use ::image::{DynamicImage, ImageReader};
use iced::{ use iced::{
alignment::Vertical::Top, border, gradient, mouse, widget::{button, center, column, container, image, mouse_area, row, stack, text, Column, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Font, Length, Point, Renderer, Size, Subscription, Task, Theme alignment::Vertical::Top, border, gradient, mouse, wgpu::naga::back, widget::{button, center, column, container, image, mouse_area, row, stack, text, Column, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Font, Length, Point, Renderer, Size, Subscription, Task, Theme
}; };
use iced_video_player::{Video, VideoPlayer}; use iced_video_player::{Video, VideoPlayer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use utils::img_utils::round_image; use utils::img_utils::round_image;
use std::{ use std::{
env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Read, Write}, path::PathBuf, sync::Arc collections::HashMap, env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Read, Write}, path::PathBuf, sync::Arc
}; };
mod utils;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use iced::event::{self, Event};
use iced::keyboard::Event as KeyboardEvent;
use iced::mouse::Event as MouseEvent;
#[derive(rust_embed::Embed)] #[derive(rust_embed::Embed)]
#[folder = "resources"] #[folder = "resources"]
@ -38,7 +38,7 @@ pub fn main() -> iced::Result {
max_size: None, max_size: None,
min_size: None, min_size: None,
visible: true, visible: true,
resizable: true, resizable: false,
transparent: false, transparent: false,
level: window::Level::Normal, level: window::Level::Normal,
exit_on_close_request: true, exit_on_close_request: true,
@ -52,7 +52,7 @@ pub fn main() -> iced::Result {
.run() .run()
} }
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Hash, Clone, EnumIter, Default, Serialize, Deserialize)]
enum PossibleGames { enum PossibleGames {
#[default] #[default]
WutheringWaves, WutheringWaves,
@ -64,31 +64,55 @@ enum PossibleGames {
#[derive(Debug)] #[derive(Debug)]
enum Launcher { enum Launcher {
Loading, Loading,
Loaded(State), Loaded(Box<State>),
} }
#[derive(Debug, Default, Clone)] #[derive(Debug)]
enum LauncherBackground {
Video(Video),
Image(image::Handle),
}
impl LauncherBackground {
fn inner(&self) -> Element<Message> {
match self {
LauncherBackground::Video(video) => VideoPlayer::new(video).into(),
LauncherBackground::Image(handle) => image(handle).into(),
}
}
}
#[derive(Debug, Default)]
struct State { struct State {
selected_game: PossibleGames, selected_game: PossibleGames,
installed_games: Vec<PossibleGames>, installed_games: Vec<PossibleGames>,
installed_game_servers: Vec<PossibleGames>, installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool, db_software_installed: bool,
background: Option<LauncherBackground>,
icon_images: HashMap<PossibleGames, image::Handle>
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct SavedState { struct SavedState {
installed_games: Vec<PossibleGames>, installed_games: Vec<PossibleGames>,
installed_game_servers: Vec<PossibleGames>, installed_game_servers: Vec<PossibleGames>,
db_software_installed: bool, db_software_installed: bool,
} }
impl From<SavedState> for Box<State> {
fn from(val: SavedState) -> Self {
Box::new(State { installed_games: val.installed_games, installed_game_servers: val.installed_game_servers, db_software_installed: val.db_software_installed, ..State::default() })
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum LoadError { enum LoadError {
File, File,
Format, Format,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
enum SaveError { enum SaveError {
Write, Write,
Format, Format,
@ -96,7 +120,7 @@ enum SaveError {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Loaded(Result<State, LoadError>), Loaded(Result<SavedState, LoadError>),
DragStarted, DragStarted,
GameSelected(PossibleGames) GameSelected(PossibleGames)
} }
@ -151,8 +175,8 @@ fn rad(deg: f32) -> f32 {
deg * std::f32::consts::PI / 180.0 deg * std::f32::consts::PI / 180.0
} }
fn get_game_background(game: &PossibleGames) -> Element<Message> { fn get_game_background(state: &State) -> LauncherBackground {
let file_path: &str = match game { let file_path: &str = match state.selected_game {
PossibleGames::WutheringWaves => "wutheringwaves-bg.mp4", PossibleGames::WutheringWaves => "wutheringwaves-bg.mp4",
PossibleGames::ZenlessZoneZero => "zenlesszonezero-bg.png", PossibleGames::ZenlessZoneZero => "zenlesszonezero-bg.png",
PossibleGames::HonkaiStarRail => "honkaistarrail-bg.png", PossibleGames::HonkaiStarRail => "honkaistarrail-bg.png",
@ -170,8 +194,7 @@ fn get_game_background(game: &PossibleGames) -> Element<Message> {
match Video::new(url::Url::from_file_path(temp_path).unwrap()) { match Video::new(url::Url::from_file_path(temp_path).unwrap()) {
Ok(mut video) => { Ok(mut video) => {
video.set_looping(true); video.set_looping(true);
LauncherBackground::Video(video)
VideoPlayer::new(video).into()
}, },
Err(err) => { Err(err) => {
panic!("{:#?}", err) panic!("{:#?}", err)
@ -183,20 +206,19 @@ fn get_game_background(game: &PossibleGames) -> Element<Message> {
.unwrap() .unwrap()
.decode() .decode()
.unwrap(); .unwrap();
let handle = image::Handle::from_rgba( LauncherBackground::Image(image::Handle::from_rgba(
img.width(), img.width(),
img.height(), img.height(),
img.to_rgba8().into_raw() img.to_rgba8().into_raw()
); ))
image(handle).content_fit(iced::ContentFit::Fill).into()
} }
} else { } else {
panic!("Missing icon for {:?}, path: {}", game, file_path) panic!("Missing icon for {:?}, path: {}", state.selected_game, file_path)
} }
} }
fn get_game_icon(game: &PossibleGames) -> Element<Message> { fn get_game_icon_handle(game: &PossibleGames) -> image::Handle {
let file_path: &str = match game { let file_path: &str = match game {
PossibleGames::WutheringWaves => "wutheringwaves-icon.png", PossibleGames::WutheringWaves => "wutheringwaves-icon.png",
PossibleGames::ZenlessZoneZero => "zenlesszonezero-icon.png", PossibleGames::ZenlessZoneZero => "zenlesszonezero-icon.png",
@ -208,24 +230,22 @@ fn get_game_icon(game: &PossibleGames) -> Element<Message> {
let img = round_image(data_cursor) let img = round_image(data_cursor)
.unwrap() .unwrap()
.resize(126, 126, ::image::imageops::FilterType::Lanczos3); .resize(126, 126, ::image::imageops::FilterType::Lanczos3);
let handle = image::Handle::from_rgba(
image::Handle::from_rgba(
img.width(), img.width(),
img.height(), img.height(),
img.to_rgba8().into_raw() img.to_rgba8().into_raw()
); )
container(image(handle).content_fit(iced::ContentFit::Contain).height(Length::Fixed(64.0)).filter_method(image::FilterMethod::Linear))
.style(move |_| {
container::Style {
border: border::rounded(20),
..container::Style::default()
}
})
.into()
} else { } else {
panic!("Missing icon for {:?}, path: {}", game, file_path) panic!("Missing icon for {:?}, path: {}", game, file_path)
} }
} }
fn get_game_icon<'a>(state: &'a State, game: &'a PossibleGames) -> Element<'a, Message> {
let handle = state.icon_images.get(game).unwrap();
container(image(handle).content_fit(iced::ContentFit::Contain).height(Length::Fixed(64.0)).filter_method(image::FilterMethod::Linear)).into()
}
fn style_container(direction: f32, use_gradient: bool) -> container::Style { fn style_container(direction: f32, use_gradient: bool) -> container::Style {
let angle = rad(direction); let angle = rad(direction);
let gradient: Option<iced::Background> = if use_gradient { let gradient: Option<iced::Background> = if use_gradient {
@ -241,7 +261,18 @@ fn style_container(direction: f32, use_gradient: bool) -> container::Style {
} }
impl Launcher { impl Launcher {
fn boot() -> (Self, Task<Message>) { fn boot() -> (Self, Task<Message>) {
(Self::Loaded(State::default()), Task::none()) let launcher_bg = get_game_background(&State::default());
let mut icons = HashMap::new();
for game in PossibleGames::iter() {
let icon = get_game_icon_handle(&game);
icons.insert(game, icon);
}
let final_state = State {
background: Some(launcher_bg),
icon_images: icons,
..State::default()
};
(Self::Loaded(Box::new(final_state)), Task::none())
} }
fn title(&self) -> String { fn title(&self) -> String {
@ -251,8 +282,8 @@ impl Launcher {
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match self { match self {
Launcher::Loading => match message { Launcher::Loading => match message {
Message::Loaded(Ok(state)) => { Message::Loaded(Ok(save_state)) => {
*self = Launcher::Loaded(state); *self = Launcher::Loaded(save_state.into());
Task::none() Task::none()
}, },
_ => Task::none(), _ => Task::none(),
@ -271,12 +302,16 @@ impl Launcher {
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
println!("rerender triggered");
match self {
Launcher::Loading => center(text("Loading...").size(50)).into(),
Launcher::Loaded(state) => {
let game_selector = container( let game_selector = container(
row![ row![
get_game_icon(&PossibleGames::WutheringWaves), get_game_icon(state, &PossibleGames::WutheringWaves),
get_game_icon(&PossibleGames::ZenlessZoneZero), get_game_icon(state, &PossibleGames::ZenlessZoneZero),
get_game_icon(&PossibleGames::HonkaiStarRail), get_game_icon(state, &PossibleGames::HonkaiStarRail),
get_game_icon(&PossibleGames::GenshinImpact), get_game_icon(state, &PossibleGames::GenshinImpact),
] ]
.spacing(10), .spacing(10),
) )
@ -286,10 +321,6 @@ impl Launcher {
.width(Length::Fill) .width(Length::Fill)
.style(move |_| style_container(0.0, true)); .style(move |_| style_container(0.0, true));
println!("whuh");
match self {
Launcher::Loading => center(text("Loading...").size(50)).into(),
Launcher::Loaded(state) => {
let topbar = container( let topbar = container(
mouse_area(row![ mouse_area(row![
text("Reversed Rooms").size(25), text("Reversed Rooms").size(25),
@ -322,8 +353,13 @@ impl Launcher {
column![topbar, Space::new(Length::Fill, Length::Fill), bottom_bar].width(Length::Fill); column![topbar, Space::new(Length::Fill, Length::Fill), bottom_bar].width(Length::Fill);
let content = container(user_area).center(Length::Fill); let content = container(user_area).center(Length::Fill);
let background = state.background.as_ref().unwrap();
let bg_element: Element<Message> = match background {
LauncherBackground::Video(video) => VideoPlayer::new(video).into(),
LauncherBackground::Image(handle) => image(handle.clone()).into(),
};
stack![get_game_background(&state.selected_game), game_selector, content].into() stack![bg_element, game_selector, content].into()
} }
} }
} }