dash-frontend: Re-work skymap list, show downloaded resolution pips

This commit is contained in:
Aleksander 2026-04-14 22:11:37 +02:00 committed by galister
parent 994dffc4fb
commit c59e195d94
16 changed files with 258 additions and 47 deletions

View File

@ -1,11 +1,11 @@
<layout>
<elements>
<div flex_direction="column">
<div flex_direction="column" gap="8" width="100%" align_items="center">
<div flex_direction="row" gap="4" align_self="end">
<Button id="btn_refresh" tooltip="RELOAD_FROM_DISK" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.BROWSE_ONLINE_CATALOG" sprite_src_builtin="dashboard/download.svg"/>
</div>
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" />
</div>
<div flex_direction="row" gap="4">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.DOWNLOAD_SKYMAPS" sprite_src_builtin="dashboard/download.svg"/>
</div>
</elements>
</layout>

View File

@ -17,9 +17,24 @@
width="256"
align_items="center"
align_self="start">
<image id="image_preview" width="100%" height="128" round="6"/>
<image id="image_preview" width="100%" height="128" round="6">
<!-- new_pass is required, because we need to render rectangles at the top of the image. Sorry. -->
<div new_pass="1" id="resolution_pips" gap="4" margin="6"/>
</image>
<label id="label_title" wrap="1" weight="bold"/>
<label id="label_author" wrap="1"/>
</Button>
</template>
<!--
params:
"color"
"text"
-->
<template name="ResolutionPip">
<rectangle color="${color}" padding_left="4" padding_right="4" padding_top="2" padding_bottom="2" round="3" align_self="start">
<label text="${text}" weight="bold" size="12" shadow="#000000" shadow_x="2" shadow_y="2"/>
</rectangle>
</template>
</layout>

View File

@ -38,6 +38,8 @@
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered",
"BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled",
"BROWSE_ONLINE_CATALOG": "Browse online catalog...",
"BROWSE_SKYMAPS": "Browse skymaps",
"CAPTURE_METHOD": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
"COLOR_KEYING": "Color keying",
@ -53,7 +55,6 @@
"DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d",
"DOUBLE_CURSOR_FIX": "Double cursor fix",
"DOUBLE_CURSOR_FIX_HELP": "Enable this if you see 2 cursors",
"DOWNLOAD_SKYMAPS": "Download skymaps",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",
"HANDSFREE_POINTER": "Handsfree mode",
@ -170,6 +171,7 @@
"PROCESS_LIST": "Process list",
"REFRESH": "Refresh",
"REMOVE": "Remove",
"RELOAD_FROM_DISK": "Reload from disk",
"SETTINGS": "Settings",
"SHOW": "Show",
"TARGET_PATH": "Target path",

View File

@ -80,9 +80,8 @@ enum Task {
struct SettingsMountParams<'a> {
mp: &'a mut MacroParams<'a>,
globals: &'a WguiGlobals,
frontend_tasks: &'a FrontendTasks,
parent_id: WidgetID,
id_parent: WidgetID,
}
trait SettingsTab {
@ -538,8 +537,7 @@ impl<T> TabSettings<T> {
let settings_mount_params = SettingsMountParams {
mp: &mut mp,
globals: &globals,
parent_id: root,
id_parent: root,
frontend_tasks: &self.frontend_tasks,
};

View File

@ -16,7 +16,7 @@ impl State {
if !par.mp.config.autostart_apps.is_empty() {
let c = options_category(
par.mp,
par.parent_id,
par.id_parent,
"APP_SETTINGS.AUTOSTART_APPS",
"dashboard/apps.svg",
)?;
@ -27,6 +27,7 @@ impl State {
options_autostart_app(par.mp, c, &app.name, app_button_ids)?;
}
}
Ok(State {})
}
}

View File

@ -11,7 +11,7 @@ impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(
par.mp,
par.parent_id,
par.id_parent,
"APP_SETTINGS.CONTROLS",
"dashboard/controller.svg",
)?;

View File

@ -9,7 +9,7 @@ impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(par.mp, par.parent_id, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;

View File

@ -11,7 +11,7 @@ impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(
par.mp,
par.parent_id,
par.id_parent,
"APP_SETTINGS.LOOK_AND_FEEL",
"dashboard/palette.svg",
)?;

View File

@ -9,7 +9,7 @@ impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(par.mp, par.parent_id, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
options_dropdown::<wlx_common::config::CaptureMethod>(par.mp, c, &SettingType::CaptureMethod)?;
options_checkbox(par.mp, c, SettingType::XwaylandByDefault)?;
options_checkbox(par.mp, c, SettingType::UprightScreenFix)?;

View File

@ -1,35 +1,69 @@
use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks};
use crate::{
frontend::FrontendTasks,
tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox},
},
views::{ViewTrait, ViewUpdateParams, skymap_list},
util::{popup_manager::PopupHolder, wgui_simple},
views::{self, ViewUpdateParams, skymap_list},
};
#[derive(Clone)]
enum Task {
ShowSkymapList,
}
pub struct State {
skymap_list: skymap_list::View,
popup_skymap_list: PopupHolder<skymap_list::View>,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
}
impl SettingsTab for State {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.skymap_list.update(par)?;
self.popup_skymap_list.update(par)?;
for task in self.tasks.drain() {
match task {
Task::ShowSkymapList => self.show_skymap_list(par.layout),
}
}
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let c = options_category(par.mp, par.parent_id, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
options_checkbox(par.mp, c, SettingType::UseSkybox)?;
options_checkbox(par.mp, c, SettingType::OpaqueBackground)?;
let id_category = options_category(par.mp, par.id_parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
options_checkbox(par.mp, id_category, SettingType::UseSkybox)?;
options_checkbox(par.mp, id_category, SettingType::OpaqueBackground)?;
let skymap_list = skymap_list::View::new(skymap_list::Params {
globals: par.globals.clone(),
let tasks = Tasks::<Task>::new();
// "Browse skymaps" button
wgui_simple::create_button(wgui_simple::CreateButtonParams {
id_parent: id_category,
layout: par.mp.layout,
parent_id: c,
frontend_tasks: par.frontend_tasks,
content: Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
icon_builtin: AssetPath::BuiltIn("dashboard/globe.svg"),
on_click: tasks.get_button_click_callback(Task::ShowSkymapList),
})?;
Ok(Self { skymap_list })
Ok(Self {
popup_skymap_list: Default::default(),
frontend_tasks: par.frontend_tasks.clone(),
tasks,
})
}
fn show_skymap_list(&mut self, layout: &mut Layout) {
views::skymap_list::mount_popup(
self.frontend_tasks.clone(),
layout.state.globals.clone(),
self.popup_skymap_list.clone(),
);
}
}

View File

@ -11,7 +11,7 @@ impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let c = options_category(
par.mp,
par.parent_id,
par.id_parent,
"APP_SETTINGS.TROUBLESHOOTING",
"dashboard/cpu.svg",
)?;

View File

@ -24,6 +24,14 @@ impl SkymapResolution {
SkymapResolution::Res8k => "8K (33 MiB VRAM)",
}
}
pub const fn get_display_str_simple(&self) -> &'static str {
match self {
SkymapResolution::Res2k => "2K",
SkymapResolution::Res4k => "4K",
SkymapResolution::Res8k => "8K",
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]

View File

@ -2,6 +2,7 @@ use glam::{Mat4, Vec2};
use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::{self, button::ButtonClickCallback},
drawing,
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
@ -12,12 +13,39 @@ use wgui::{
},
taffy::{self, prelude::length},
widget::{
ConstructEssentials,
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
};
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
pub struct CreateButtonParams<'a> {
pub id_parent: WidgetID,
pub layout: &'a mut Layout,
pub content: Translation,
pub icon_builtin: AssetPath<'a>,
pub on_click: ButtonClickCallback,
}
pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> {
let (_, button) = components::button::construct(
&mut ConstructEssentials {
layout: par.layout,
parent: par.id_parent,
},
components::button::Params {
text: Some(par.content),
sprite_src: Some(par.icon_builtin),
..Default::default()
},
)?;
button.on_click(par.on_click);
Ok(())
}
pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
@ -29,7 +57,7 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation)
},
);
layout.add_child(parent, label, Default::default())?;
layout.add_child(id_parent, label, Default::default())?;
Ok(())
}

View File

@ -1,5 +1,6 @@
use std::rc::Rc;
use uuid::Uuid;
use wgui::{
assets::AssetPath,
globals::WguiGlobals,
@ -16,7 +17,7 @@ use crate::{
util::{
networking::{
self,
skymap_catalog::{SkymapCatalogEntry, SkymapUuid},
skymap_catalog::{SkymapCatalog, SkymapCatalogEntry, SkymapUuid},
},
popup_manager::{MountPopupOnceParams, PopupHolder},
wgui_simple,
@ -46,6 +47,7 @@ enum Task {
),
),
ShowRemoteSkymapDownloader(SkymapUuid),
RefreshCells,
}
struct MountedCell {
@ -62,11 +64,19 @@ pub struct View {
mounted_cells: Vec<MountedCell>,
executor: AsyncExecutor,
frontend_tasks: FrontendTasks,
catalog: Option<networking::skymap_catalog::SkymapCatalog>,
catalog: Option<SkymapCatalog>,
popup_remote_skymap_downloader: PopupHolder<views::remote_skymap_downloader::View>,
on_updated_library: Rc<dyn Fn()>,
}
fn get_entry_by_uuid(catalog: &SkymapCatalog, skymap_uuid: Uuid) -> Option<&SkymapCatalogEntry> {
let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == skymap_uuid) else {
return None;
};
Some(entry)
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.popup_remote_skymap_downloader.update(par)?;
@ -107,6 +117,9 @@ impl ViewTrait for View {
log::error!("preview image not present, ignoring request");
}
}
Task::RefreshCells => {
self.refresh_cells(par.layout)?;
}
}
}
Ok(())
@ -156,6 +169,21 @@ impl View {
)));
}
fn refresh_cells(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
let Some(catalog) = &self.catalog else {
debug_assert!(false);
return Ok(());
};
for cell in &mut self.mounted_cells {
if let Some(entry) = get_entry_by_uuid(&catalog, cell.skymap_uuid) {
cell.view.refresh_resolution_pips(layout, entry)?;
}
}
Ok(())
}
fn mount_catalog(
&mut self,
layout: &mut Layout,
@ -212,11 +240,20 @@ impl View {
return Ok(());
};
let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == uuid) else {
debug_assert!(false); // impossible
let Some(entry) = get_entry_by_uuid(&catalog, uuid) else {
return Ok(());
};
// call our task before calling underlying on_updated_library callback
let on_updated_library = Rc::new({
let func = self.on_updated_library.clone();
let tasks = self.tasks.clone();
move || {
tasks.push(Task::RefreshCells);
(*func)();
}
});
views::remote_skymap_downloader::mount_popup(
self.frontend_tasks.clone(),
self.executor.clone(),
@ -224,7 +261,7 @@ impl View {
entry.clone(),
preview_image,
preview_image_compressed,
self.on_updated_library.clone(),
on_updated_library,
self.popup_remote_skymap_downloader.clone(),
);
@ -259,7 +296,7 @@ pub fn mount_popup(
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("APP_SETTINGS.DOWNLOAD_SKYMAPS"),
Translation::from_translation_key("APP_SETTINGS.BROWSE_ONLINE_CATALOG"),
Box::new(move |data| {
let view = View::new(Params {
globals: &globals,

View File

@ -12,10 +12,10 @@ use wgui::{
use wlx_common::{async_executor::AsyncExecutor, config_io};
use crate::{
frontend::FrontendTasks,
frontend::{FrontendTask, FrontendTasks},
util::{
networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution},
popup_manager::PopupHolder,
popup_manager::{MountPopupOnceParams, PopupHolder},
wgui_simple,
},
views::{self, ViewTrait, ViewUpdateParams},
@ -182,3 +182,22 @@ impl View {
Ok(())
}
}
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder<View>) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
Box::new(move |data| {
let view = View::new(Params {
globals: globals.clone(),
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
})?;
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -1,7 +1,10 @@
use std::{collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton},
event::EventAlterables,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
@ -9,7 +12,13 @@ use wgui::{
widget::{image::WidgetImage, label::WidgetLabel},
};
use crate::util::{networking, wgui_simple};
use crate::util::{
networking::{
self,
skymap_catalog::{SkymapCatalogEntry, SkymapResolution},
},
wgui_simple,
};
pub struct Params<'a> {
pub id_parent: WidgetID,
@ -23,23 +32,71 @@ pub struct View {
parser_state: ParserState,
id_loading: WidgetID,
id_image_preview: WidgetID,
id_resolution_pips: WidgetID,
image: Option<CustomGlyphData>,
}
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"),
extra: Default::default(),
}
}
fn populate_res_pips(
layout: &mut Layout,
id_parent: WidgetID,
parser_state: &mut ParserState,
entry: &SkymapCatalogEntry,
) -> anyhow::Result<()> {
let globals = layout.state.globals.clone();
layout.remove_children(id_parent);
let mut populate_res_pip = |res: SkymapResolution| -> anyhow::Result<()> {
let mut tpar = HashMap::<Rc<str>, Rc<str>>::new();
let downloaded = entry.is_downloaded(res).unwrap_or(false);
tpar.insert(
Rc::from("color"),
if downloaded {
Rc::from("#116625")
} else {
Rc::from("#444444")
},
);
tpar.insert(Rc::from("text"), res.get_display_str_simple().into());
parser_state.realize_template(&doc_params(&globals), "ResolutionPip", layout, id_parent, tpar)?;
Ok(())
};
populate_res_pip(SkymapResolution::Res2k)?;
if entry.files.size_4k.is_some() {
populate_res_pip(SkymapResolution::Res4k)?;
}
if entry.files.size_8k.is_some() {
populate_res_pip(SkymapResolution::Res8k)?;
}
Ok(())
}
impl View {
pub fn new(par: Params) -> anyhow::Result<Self> {
let globals = par.layout.state.globals.clone();
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.id_parent)?;
let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&globals), par.layout, par.id_parent)?;
let data = parser_state.realize_template(&doc_params, "Cell", par.layout, par.id_parent, Default::default())?;
let data = parser_state.realize_template(
&doc_params(&globals),
"Cell",
par.layout,
par.id_parent,
Default::default(),
)?;
let id_image_preview = data.get_widget_id("image_preview")?;
let id_resolution_pips = data.get_widget_id("resolution_pips")?;
data
.fetch_component_as::<ComponentButton>("button")?
@ -49,7 +106,10 @@ impl View {
let mut label_title = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_title")?;
let mut label_author = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?;
label_title.set_text_simple(&mut globals.get(), Translation::from_raw_text_string(par.entry.name));
label_title.set_text_simple(
&mut globals.get(),
Translation::from_raw_text_string(par.entry.name.clone()),
);
label_author.set_text_simple(
&mut globals.get(),
Translation::from_raw_text_string(format!("by {}", par.entry.author)),
@ -62,14 +122,23 @@ impl View {
with_text: false,
})?;
// Populate resolution pips
populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, &par.entry)?;
Ok(Self {
parser_state,
id_loading,
id_image_preview,
image: None,
id_resolution_pips,
})
}
pub fn refresh_resolution_pips(&mut self, layout: &mut Layout, entry: &SkymapCatalogEntry) -> anyhow::Result<()> {
populate_res_pips(layout, self.id_resolution_pips, &mut self.parser_state, &entry)?;
Ok(())
}
pub fn set_image(&mut self, layout: &mut Layout, content: Option<CustomGlyphData>) -> anyhow::Result<()> {
layout.remove_widget(self.id_loading);
let mut alt = EventAlterables::default();