use std::{path::PathBuf, rc::Rc}; use chrono::Timelike; use glam::Vec2; use wgui::{ assets::{AssetPath, AssetProvider}, components::button::ComponentButton, font_config::WguiFontConfig, globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutParams, LayoutUpdateParams, LayoutUpdateResult, WidgetID}, parser::{Fetchable, ParseDocumentParams, ParserState}, task::Tasks, widget::{label::WidgetLabel, rectangle::WidgetRectangle}, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement}, }; use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep}; use crate::{ assets, tab::{ Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings, }, util::{ popup_manager::{MountPopupParams, PopupManager, PopupManagerParams}, toast_manager::ToastManager, various::AsyncExecutor, }, views, }; pub struct FrontendWidgets { pub id_label_time: WidgetID, pub id_rect_content: WidgetID, } pub type FrontendTasks = Tasks; pub struct Frontend { pub layout: Layout, globals: WguiGlobals, pub interface: BoxDashInterface, // async runtime executor pub executor: AsyncExecutor, #[allow(dead_code)] state: ParserState, current_tab: Option>>, pub tasks: FrontendTasks, ticks: u32, widgets: FrontendWidgets, popup_manager: PopupManager, toast_manager: ToastManager, timestep: Timestep, sounds_to_play: Vec, window_audio_settings: WguiWindow, view_audio_settings: Option, } pub struct FrontendUpdateParams<'a, T> { pub data: &'a mut T, pub width: f32, pub height: f32, pub timestep_alpha: f32, } pub struct FrontendUpdateResult { pub layout_result: LayoutUpdateResult, pub sounds_to_play: Vec, } pub struct InitParams { pub interface: BoxDashInterface, pub has_monado: bool, } #[derive(Clone)] pub enum SoundType { Startup, Launch, } #[derive(Clone)] pub enum FrontendTask { SetTab(TabType), RefreshClock, RefreshBackground, MountPopup(MountPopupParams), RefreshPopupManager, ShowAudioSettings, UpdateAudioSettingsView, RecenterPlayspace, PushToast(Translation), PlaySound(SoundType), HideDashboard, } impl Frontend { pub fn new(params: InitParams, data: &mut T) -> anyhow::Result> { let mut assets = Box::new(assets::Asset {}); let font_binary_bold = assets.load_from_path_gzip("Quicksand-Bold.ttf.gz")?; let font_binary_regular = assets.load_from_path_gzip("Quicksand-Regular.ttf.gz")?; let font_binary_light = assets.load_from_path_gzip("Quicksand-Light.ttf.gz")?; let globals = WguiGlobals::new( assets, wgui::globals::Defaults::default(), &WguiFontConfig { binaries: vec![&font_binary_regular, &font_binary_bold, &font_binary_light], family_name_sans_serif: "Quicksand", family_name_serif: "Quicksand", family_name_monospace: "", }, PathBuf::new(), //FIXME: pass from somewhere else )?; let (layout, state) = wgui::parser::new_layout_from_assets( &ParseDocumentParams { globals: globals.clone(), path: AssetPath::BuiltIn("gui/dashboard.xml"), extra: Default::default(), }, &LayoutParams { resize_to_parent: true }, )?; let id_popup_manager = state.get_widget_id("popup_manager")?; let popup_manager = PopupManager::new(PopupManagerParams { parent_id: id_popup_manager, }); let toast_manager = ToastManager::new(); let tasks = FrontendTasks::new(); tasks.push(FrontendTask::SetTab(TabType::Home)); let id_label_time = state.get_widget_id("label_time")?; let id_rect_content = state.get_widget_id("rect_content")?; let timestep = Timestep::new(60.0); let mut frontend = Self { layout, state, current_tab: None, globals, tasks, ticks: 0, widgets: FrontendWidgets { id_label_time, id_rect_content, }, timestep, interface: params.interface, popup_manager, toast_manager, window_audio_settings: WguiWindow::default(), view_audio_settings: None, executor: Rc::new(smol::LocalExecutor::new()), sounds_to_play: Vec::new(), }; // init some things first frontend.update_background(data)?; frontend.update_time(data)?; Frontend::register_widgets(&mut frontend)?; Ok(frontend) } fn queue_play_sound(&mut self, sound_type: SoundType) { self.sounds_to_play.push(sound_type); } fn play_sound(&mut self, audio_system: &mut audio::AudioSystem, sound_type: SoundType) -> anyhow::Result<()> { let mut assets = self.globals.assets_builtin(); let sample = audio::AudioSample::from_mp3(&assets.load_from_path(match sound_type { SoundType::Startup => "sound/startup.mp3", SoundType::Launch => "sound/app_start.mp3", })?)?; audio_system.play_sample(&sample); Ok(()) } pub fn update(&mut self, mut params: FrontendUpdateParams) -> anyhow::Result { let mut tasks = self.tasks.drain(); while let Some(task) = tasks.pop_front() { self.process_task(&mut params, task)?; } if let Some(mut tab) = self.current_tab.take() { tab.update(self, params.data)?; self.current_tab = Some(tab); } // process async runtime tasks while self.executor.try_tick() {} let res = self.tick(params)?; self.ticks += 1; Ok(res) } pub fn process_update( &mut self, res: FrontendUpdateResult, audio_system: &mut audio::AudioSystem, audio_sample_player: &mut audio::SamplePlayer, ) -> anyhow::Result<()> { for sound_type in res.sounds_to_play { self.play_sound(audio_system, sound_type)?; } audio_sample_player.play_wgui_samples(audio_system, res.layout_result.sounds_to_play); Ok(()) } fn tick(&mut self, params: FrontendUpdateParams) -> anyhow::Result { // fixme: timer events instead of this thing if self.ticks.is_multiple_of(1000) { self.update_time(params.data)?; } { // always 30 times per second while self.timestep.on_tick() { self.toast_manager.tick(&self.globals, &mut self.layout)?; } } let layout_result = self.layout.update(&mut LayoutUpdateParams { size: Vec2::new(params.width, params.height), timestep_alpha: params.timestep_alpha, })?; Ok(FrontendUpdateResult { layout_result, sounds_to_play: std::mem::take(&mut self.sounds_to_play), }) } fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> { let mut c = self.layout.start_common(); let mut common = c.common(); { let Some(mut label) = common.state.widgets.get_as::(self.widgets.id_label_time) else { anyhow::bail!(""); }; let now = chrono::Local::now(); let hours = now.hour(); let minutes = now.minute(); let text: String = if self.interface.general_config(data).clock_12h { let hours_ampm = (hours + 11) % 12 + 1; let suffix = if hours >= 12 { "PM" } else { "AM" }; format!("{hours_ampm:02}:{minutes:02} {suffix}") } else { format!("{hours:02}:{minutes:02}") }; label.set_text(&mut common, Translation::from_raw_text(&text)); } c.finish()?; Ok(()) } fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> { let config = self.interface.general_config(data); self.popup_manager.mount_popup( self.globals.clone(), &mut self.layout, self.tasks.clone(), params, config, )?; Ok(()) } fn refresh_popup_manager(&mut self) -> anyhow::Result<()> { let mut c = self.layout.start_common(); self.popup_manager.refresh(c.common().alterables); c.finish()?; Ok(()) } fn update_background(&mut self, data: &mut T) -> anyhow::Result<()> { let Some(mut rect) = self .layout .state .widgets .get_as::(self.widgets.id_rect_content) else { anyhow::bail!(""); }; let (alpha1, alpha2) = if self.interface.general_config(data).opaque_background { (1.0, 1.0) } else { (0.8666, 0.9333) }; rect.params.color.a = alpha1; rect.params.color2.a = alpha2; Ok(()) } fn process_task(&mut self, params: &mut FrontendUpdateParams, task: FrontendTask) -> anyhow::Result<()> { match task { FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?, FrontendTask::RefreshClock => self.update_time(params.data)?, FrontendTask::RefreshBackground => self.update_background(params.data)?, FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?, FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?, FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?, FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?, FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?, FrontendTask::PushToast(content) => self.toast_manager.push(content), FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type), FrontendTask::HideDashboard => self.action_hide_dashboard(params.data), }; Ok(()) } fn set_tab(&mut self, data: &mut T, tab_type: TabType) -> anyhow::Result<()> { log::info!("Setting tab to {tab_type:?}"); let widget_content = self.state.fetch_widget(&self.layout.state, "content")?; self.layout.remove_children(widget_content.id); let tab: Box> = match tab_type { TabType::Home => Box::new(TabHome::new(self, widget_content.id, data)?), TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?), TabType::Games => Box::new(TabGames::new(self, widget_content.id)?), TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?), TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?), TabType::Settings => Box::new(TabSettings::new(self, widget_content.id, data)?), }; self.current_tab = Some(tab); Ok(()) } fn register_widgets(&mut self) -> anyhow::Result<()> { // "X" button self.tasks.handle_button( &self.state.fetch_component_as::("btn_close")?, FrontendTask::HideDashboard, ); // ################################ // SIDE BUTTONS // ################################ // "Home" side button self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_home")?, FrontendTask::SetTab(TabType::Home), ); // "Apps" side button self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_apps")?, FrontendTask::SetTab(TabType::Apps), ); // "Games" side button self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_games")?, FrontendTask::SetTab(TabType::Games), ); // "Monado side button" self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_monado")?, FrontendTask::SetTab(TabType::Monado), ); // "Processes" side button self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_processes")?, FrontendTask::SetTab(TabType::Processes), ); // "Settings" side button self.tasks.handle_button( &self.state.fetch_component_as::("btn_side_settings")?, FrontendTask::SetTab(TabType::Settings), ); // ################################ // BOTTOM BAR BUTTONS // ################################ // "Audio" bottom bar button self.tasks.handle_button( &self.state.fetch_component_as::("btn_audio")?, FrontendTask::ShowAudioSettings, ); // "Recenter playspace" bottom bar button self.tasks.handle_button( &self.state.fetch_component_as::("btn_recenter")?, FrontendTask::RecenterPlayspace, ); Ok(()) } fn action_show_audio_settings(&mut self) -> anyhow::Result<()> { self.window_audio_settings.open(&mut WguiWindowParams { globals: &self.globals, position: Vec2::new(64.0, 64.0), layout: &mut self.layout, extra: WguiWindowParamsExtra { fixed_width: Some(400.0), placement: WguiWindowPlacement::BottomLeft, close_if_clicked_outside: true, title: Some(Translation::from_translation_key("AUDIO.SETTINGS")), ..Default::default() }, })?; let content = self.window_audio_settings.get_content(); self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params { globals: self.globals.clone(), frontend_tasks: self.tasks.clone(), layout: &mut self.layout, parent_id: content.id, on_update: { let tasks = self.tasks.clone(); Rc::new(move || { tasks.push(FrontendTask::UpdateAudioSettingsView); }) }, })?); Ok(()) } fn action_update_audio_settings(&mut self) -> anyhow::Result<()> { let Some(view) = &mut self.view_audio_settings else { return Ok(()); }; view.update(&mut self.layout)?; Ok(()) } fn action_recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()> { self.interface.recenter_playspace(data)?; Ok(()) } fn action_hide_dashboard(&mut self, data: &mut T) { self.interface.toggle_dashboard(data); } }