dash-frontend: games: pagination

This commit is contained in:
Aleksander
2026-01-27 19:49:11 +01:00
parent 19083cfd83
commit d97bbfc796
20 changed files with 224 additions and 130 deletions

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m10 18l-6-6l6-6l1.4 1.45L7.85 11H20v2H7.85l3.55 3.55z" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m14 18l-1.4-1.45L16.15 13H4v-2h12.15L12.6 7.45L14 6l6 6z" />
</svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -66,7 +66,7 @@
<!-- Title bar -->
<div width="100%" align_items="center" justify_content="center">
<rectangle min_width="300" height="100%" consume_mouse_events="1" round="100%" color="~color_top_panel" align_items="center" justify_content="center" gap="8">
<sprite id="sprite_titlebar_icon" width="28" height="28" />
<sprite id="sprite_titlebar_icon" width="24" height="24" />
<label id="label_titlebar_title" weight="bold" size="16" text="Title" />
</rectangle>
</div>
@@ -98,10 +98,7 @@
flex_direction="column"
overflow_x="scroll"
overflow_y="scroll"
padding_top="8"
padding_bottom="8"
padding_left="16"
padding_right="16"
padding="16"
gap="8"
width="100%"
min_height="100%"

View File

@@ -1,8 +1,6 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<rectangle macro="group_box" id="running_games_list_parent" align_self="center" />
<div id="game_list_parent" align_items="center" />
<div id="running_games_list_parent" align_self="center" />
<div id="game_list_parent" align_items="center" flex_direction="column" gap="8" overflow_y="scroll" />
</elements>
</layout>

View File

@@ -1,7 +1,12 @@
<layout>
<elements>
<div flex_direction="column">
<div flex_direction="column" padding_top="8" padding_bottom="8">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</div>
<div align_items="center" justify_content="center" gap="16">
<Button id="btn_prev" sprite_src_builtin="dashboard/arrow_left.svg" width="32" height="32" />
<label id="label_page" text="Page X" weight="bold" />
<Button id="btn_next" sprite_src_builtin="dashboard/arrow_right.svg" width="32" height="32" />
</div>
</elements>
</layout>

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "Langsam, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway",
"NONE": "Keine",
"HMD_PINCH": "HMD + Kneifen",
"EYE_PINCH": "Auge + Kneifen"
"EYE_PINCH": "Auge + Kneifen",
"EYE_ONLY": "Nur Auge",
"HMD_ONLY": "Nur HMD"
},
"AUTOSTART_APPS": "Anwendungen, die beim Start ausgeführt werden sollen",
"HANDSFREE_POINTER": "Freihändige Modus",
"HANDSFREE_POINTER_HELP": "Eingabe, die bei Bewegung\nder Controller verwendet wird, wenn diese nicht verfügbar sind.\nLinkes Kneifen greift, rechtes klickt.",
"UI_GRADIENT_INTENSITY": "UI-Verlaufsintensität"
"UI_GRADIENT_INTENSITY": "UI-Verlaufsintensität",
"RESET_PLAYSPACE": "Spielbereich zurücksetzen",
"RESET_PLAYSPACE_HELP": "Den Abstand des Spielbereichs zurücksetzen."
},
"HELLO": "Hallo!",
"AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden",
"RUNNING_GAMES_LIST": "Liste der laufenden Spiele",
"NO_RUNNING_GAME_FOUND": "Kein laufendes Spiel gefunden"
"RUNNING_GAMES_LIST": "Liste der laufenden Spiele"
},
"TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet",

View File

@@ -132,8 +132,7 @@
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": {
"NO_GAMES_FOUND": "No games found",
"RUNNING_GAMES_LIST": "List of running games",
"NO_RUNNING_GAME_FOUND": "No running game found"
"RUNNING_GAMES_LIST": "List of running games"
},
"GAMES": "Games",
"GENERAL_SETTINGS": "General settings",

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "Lento, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway",
"NONE": "Ninguno",
"HMD_PINCH": "HMD + pellizco",
"EYE_PINCH": "Ojo + pellizco"
"EYE_PINCH": "Ojo + pellizco",
"EYE_ONLY": "Solo ojo",
"HMD_ONLY": "Solo HMD"
},
"AUTOSTART_APPS": "Aplicaciones a ejecutar al inicio",
"HANDSFREE_POINTER": "Modo manos libres",
"HANDSFREE_POINTER_HELP": "Entrada a utilizar cuando no\nestén disponibles los mandos de movimiento.\nPellizco con la izquierda para agarrar, con la derecha para hacer clic.",
"UI_GRADIENT_INTENSITY": "Intensidad del degradado de la IU"
"UI_GRADIENT_INTENSITY": "Intensidad del degradado de la IU",
"RESET_PLAYSPACE": "Restablecer espacio de juego",
"RESET_PLAYSPACE_HELP": "Borrar el desplazamiento del espacio de juego."
},
"HELLO": "¡Hola!",
"AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos",
"RUNNING_GAMES_LIST": "Lista de juegos en ejecución",
"NO_RUNNING_GAME_FOUND": "No se encontró ningún juego en ejecución"
"RUNNING_GAMES_LIST": "Lista de juegos en ejecución"
},
"TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado",

View File

@@ -75,7 +75,9 @@
"SCREENCOPY_HELP": "Lento, nessuna finestra pop-up per la condivisione dello schermo.\nFunziona su: Hyprland, Niri, River, Sway",
"NONE": "Nessuno",
"HMD_PINCH": "HMD + pizzico",
"EYE_PINCH": "Occhio + pizzico"
"EYE_PINCH": "Occhio + pizzico",
"EYE_ONLY": "Solo occhio",
"HMD_ONLY": "Solo HMD"
},
"POINTER_LERP_FACTOR": "Smussamento puntatore",
"RESTART_SOFTWARE": "Riavvia il software",
@@ -103,7 +105,9 @@
"AUTOSTART_APPS": "App da avviare all'avvio",
"HANDSFREE_POINTER": "Modalità a mani libere",
"HANDSFREE_POINTER_HELP": "Input da usare quando i\ncontroller di movimento non sono disponibili.\nPizzico sinistro per afferrare, destro per cliccare.",
"UI_GRADIENT_INTENSITY": "Intensità gradiente dell'interfaccia utente"
"UI_GRADIENT_INTENSITY": "Intensità gradiente dell'interfaccia utente",
"RESET_PLAYSPACE": "Ripristina playspace",
"RESET_PLAYSPACE_HELP": "Cancella l'offset dello spazio di gioco."
},
"APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",
@@ -128,8 +132,7 @@
"GAME_LAUNCHED": "Gioco lanciato",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nessun gioco trovato",
"RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione",
"NO_RUNNING_GAME_FOUND": "Nessun gioco in esecuzione trovato"
"RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione"
},
"GAMES": "Giochi",
"GENERAL_SETTINGS": "Impostazioni generali",

View File

@@ -78,12 +78,16 @@
"SCREENCOPY_HELP": "遅延あり、画面共有ポップアップなし。\n動作する環境: Hyprland, Niri, River, Sway",
"NONE": "なし",
"HMD_PINCH": "HMD + ピンチ",
"EYE_PINCH": "つまんで目を合わせる"
"EYE_PINCH": "つまんで目を合わせる",
"EYE_ONLY": "視野のみ",
"HMD_ONLY": "HMDのみ"
},
"AUTOSTART_APPS": "起動時に実行するアプリ",
"HANDSFREE_POINTER": "ハンズフリーモード",
"HANDSFREE_POINTER_HELP": "モーションコントローラーが利用できない場合の入力方法。\n左手のピンチは掴み、右手のピンチはクリックです。",
"UI_GRADIENT_INTENSITY": "UIグラデーションの強さ"
"UI_GRADIENT_INTENSITY": "UIグラデーションの強さ",
"RESET_PLAYSPACE": "プレイエリアをリセット",
"RESET_PLAYSPACE_HELP": "プレイエリアのオフセットをクリアします。"
},
"HELLO": "こんにちは!",
"AUDIO": {
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした",
"RUNNING_GAMES_LIST": "実行中のゲーム一覧",
"NO_RUNNING_GAME_FOUND": "実行中のゲームが見つかりません"
"RUNNING_GAMES_LIST": "実行中のゲーム一覧"
},
"TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました",

View File

@@ -73,12 +73,16 @@
"SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"NONE": "Brak",
"HMD_PINCH": "HMD + szczyknięcie",
"EYE_PINCH": "Ściśnięcie palcami + oko"
"EYE_PINCH": "Ściśnięcie palcami + oko",
"EYE_ONLY": "Tylko oko",
"HMD_ONLY": "Tylko HMD"
},
"AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie",
"HANDSFREE_POINTER": "Tryb bez użycia rąk",
"HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.",
"UI_GRADIENT_INTENSITY": "Intensywność gradientu UI"
"UI_GRADIENT_INTENSITY": "Intensywność gradientu UI",
"RESET_PLAYSPACE": "Zresetuj przestrzeń gry",
"RESET_PLAYSPACE_HELP": "Wyczyść przesunięcie przestrzeni gry."
},
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje",
@@ -116,8 +120,7 @@
"CLOSE_WINDOW": "Zamknij okno",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier",
"RUNNING_GAMES_LIST": "Lista uruchomionych gier",
"NO_RUNNING_GAME_FOUND": "Nie znaleziono uruchomionej gry"
"RUNNING_GAMES_LIST": "Lista uruchomionych gier"
},
"TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona",

View File

@@ -75,7 +75,9 @@
"SCREENCOPY_HELP": "慢速,无屏幕共享弹窗。\n支持Hyprland, Niri, River, Sway",
"NONE": "无",
"HMD_PINCH": "HMD + 捏合",
"EYE_PINCH": "眼睛 + 捏合"
"EYE_PINCH": "眼睛 + 捏合",
"EYE_ONLY": "仅眼球",
"HMD_ONLY": "仅限头显"
},
"POINTER_LERP_FACTOR": "指针平滑",
"RESTART_SOFTWARE": "重启软件",
@@ -103,7 +105,9 @@
"AUTOSTART_APPS": "开机启动应用",
"HANDSFREE_POINTER": "免提模式",
"HANDSFREE_POINTER_HELP": "当运动控制器不可用时使用的输入。\n左手捏合为抓取右手为点击。",
"UI_GRADIENT_INTENSITY": "UI 渐变强度"
"UI_GRADIENT_INTENSITY": "UI 渐变强度",
"RESET_PLAYSPACE": "重置游戏空间",
"RESET_PLAYSPACE_HELP": "清除舞台空间偏移。"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动",
@@ -128,8 +132,7 @@
"GAME_LAUNCHED": "游戏已启动",
"GAME_LIST": {
"NO_GAMES_FOUND": "未找到游戏",
"RUNNING_GAMES_LIST": "正在运行的游戏列表",
"NO_RUNNING_GAME_FOUND": "未找到正在运行的游戏"
"RUNNING_GAMES_LIST": "正在运行的游戏列表"
},
"GAMES": "游戏",
"GENERAL_SETTINGS": "通用设置",

View File

@@ -1,6 +1,5 @@
use std::{path::PathBuf, rc::Rc};
use anyhow::Context;
use chrono::Timelike;
use glam::Vec2;
use wgui::{

View File

@@ -2,6 +2,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
@@ -29,7 +30,10 @@ enum Task {
AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher,
Refresh,
LoadManifests,
FillPage(u32),
PrevPage,
NextPage,
}
pub struct Params<'a> {
@@ -40,7 +44,9 @@ pub struct Params<'a> {
pub parent_id: WidgetID,
}
pub struct Cell {
const MAX_GAMES_PER_PAGE: u32 = 30;
pub struct GameCoverCell {
view_cover: game_cover::View,
}
@@ -55,10 +61,14 @@ pub struct View {
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
id_list_parent: WidgetID,
cells: HashMap<AppID, Cell>,
game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor,
state: Rc<RefCell<State>>,
mounted_game_covers: HashMap<AppID, GameCoverCell>,
all_manifests: Vec<steam_utils::AppManifest>,
cur_page: u32,
page_count: u32,
id_label_page: WidgetID,
}
impl View {
@@ -71,10 +81,21 @@ impl View {
let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?;
let id_label_page = parser_state.get_widget_id("label_page")?;
let tasks = Tasks::new();
tasks.push(Task::Refresh);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_prev")?,
Task::PrevPage,
);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_next")?,
Task::NextPage,
);
tasks.push(Task::LoadManifests);
Ok(Self {
parser_state,
@@ -82,10 +103,14 @@ impl View {
frontend_tasks: params.frontend_tasks,
globals: params.globals.clone(),
id_list_parent: list_parent.id,
cells: HashMap::new(),
mounted_game_covers: HashMap::new(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor,
all_manifests: Vec::new(),
cur_page: 0,
page_count: 0,
id_label_page,
})
}
@@ -102,10 +127,13 @@ impl View {
}
for task in tasks {
match task {
Task::Refresh => self.refresh(layout, steam_utils, executor)?,
Task::LoadManifests => self.load_manifests(steam_utils),
Task::FillPage(page_idx) => self.fill_page(layout, executor, page_idx)?,
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
Task::PrevPage => self.page_prev(),
Task::NextPage => self.page_next(),
}
}
}
@@ -119,18 +147,14 @@ impl View {
}
}
pub struct Games {
manifests: Vec<steam_utils::AppManifest>,
}
fn fill_game_list(
ess: &mut ConstructEssentials,
executor: &AsyncExecutor,
cells: &mut HashMap<AppID, Cell>,
games: &Games,
mounted_game_covers: &mut HashMap<AppID, GameCoverCell>,
manifests: &[steam_utils::AppManifest],
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
for manifest in &games.manifests {
for manifest in manifests {
let on_loaded = {
let app_id = manifest.app_id.clone();
let tasks = tasks.clone();
@@ -156,49 +180,81 @@ fn fill_game_list(
})
});
cells.insert(manifest.app_id.clone(), Cell { view_cover });
mounted_game_covers.insert(manifest.app_id.clone(), GameCoverCell { view_cover });
}
Ok(())
}
impl View {
fn game_list(&self, steam_utils: &mut SteamUtils) -> anyhow::Result<Games> {
let manifests = steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
Ok(Games { manifests })
fn load_manifests(&mut self, steam_utils: &mut SteamUtils) {
match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) {
Ok(manifests) => {
self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
self.all_manifests = manifests;
self.tasks.push(Task::FillPage(0));
}
Err(e) => {
log::error!("Failed to list installed games: {e:?}");
}
}
}
fn refresh(
&mut self,
layout: &mut Layout,
steam_utils: &mut SteamUtils,
executor: &AsyncExecutor,
) -> anyhow::Result<()> {
fn page_prev(&mut self) {
if self.cur_page == 0 {
return;
}
self.cur_page -= 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn page_next(&mut self) {
if self.cur_page >= self.page_count - 1 {
return;
}
self.cur_page += 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn fill_page(&mut self, layout: &mut Layout, executor: &AsyncExecutor, page_idx: u32) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
self.cells.clear();
self.mounted_game_covers.clear();
let idx_from = (page_idx * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let idx_to = ((page_idx + 1) * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let page_manifests = &self.all_manifests[idx_from as usize..idx_to as usize];
let mut text: Option<Translation> = None;
match self.game_list(steam_utils) {
Ok(list) => {
if list.manifests.is_empty() {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
} else {
fill_game_list(
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
executor,
&mut self.cells,
&list,
&self.tasks,
)?
}
}
Err(e) => text = Some(Translation::from_raw_text(&format!("Error: {:?}", e))),
if page_manifests.is_empty() {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
}
// set page text
let mut c = layout.start_common();
{
let mut common = c.common();
let mut widget = common.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
widget.set_text(
&mut common,
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
);
}
c.finish()?;
fill_game_list(
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
executor,
&mut self.mounted_game_covers,
page_manifests,
&self.tasks,
)?;
if let Some(text) = text.take() {
layout.add_child(
self.id_list_parent,
@@ -217,11 +273,11 @@ impl View {
}
fn set_cover_art(&mut self, layout: &mut Layout, app_id: AppID, cover_art: Rc<CoverArt>) {
let Some(cell) = &mut self.cells.get_mut(&app_id) else {
let Some(cover) = &mut self.mounted_game_covers.get_mut(&app_id) else {
return;
};
if let Err(e) = cell
if let Err(e) = cover
.view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art)
{

View File

@@ -1,20 +1,19 @@
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
event::StyleSetRequest,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::Display,
task::Tasks,
widget::label::WidgetLabel,
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
wgui_simple,
},
util::steam_utils::{self, AppID, AppManifest, GameSortMethod, SteamUtils},
};
#[derive(Clone)]
@@ -39,6 +38,7 @@ pub struct View {
id_list_parent: WidgetID,
installed_games: Vec<AppManifest>,
frontend_tasks: FrontendTasks,
parent_id: WidgetID,
}
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
@@ -58,7 +58,7 @@ impl View {
let installed_games = params
.steam_utils
.list_installed_games(GameSortMethod::None)
.unwrap_or(Vec::new());
.unwrap_or_default();
let tasks = Tasks::<Task>::new();
@@ -72,6 +72,7 @@ impl View {
id_list_parent,
installed_games,
frontend_tasks: params.frontend_tasks,
parent_id: params.parent_id,
})
}
@@ -98,7 +99,7 @@ impl View {
Ok(())
}
fn extract_name_from_appid<'a>(app_id: &AppID, manifests: &[AppManifest]) -> String {
fn extract_name_from_appid(app_id: &AppID, manifests: &[AppManifest]) -> String {
for manifest in manifests {
if manifest.app_id == *app_id {
return manifest.name.clone();
@@ -110,14 +111,19 @@ impl View {
fn fill_list(&mut self, layout: &mut Layout, games: Vec<steam_utils::RunningGame>) -> anyhow::Result<()> {
if games.is_empty() {
wgui_simple::create_label(
layout,
self.id_list_parent,
Translation::from_translation_key("GAME_LIST.NO_RUNNING_GAME_FOUND"),
)?;
// hide self
layout.tasks.push(LayoutTask::SetWidgetStyle(
self.parent_id,
StyleSetRequest::Display(Display::None),
));
return Ok(());
}
layout.tasks.push(LayoutTask::SetWidgetStyle(
self.parent_id,
StyleSetRequest::Display(Display::DEFAULT),
));
for game in games {
let game_name = View::extract_name_from_appid(&game.app_id, &self.installed_games);

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "llm_translator",

View File

@@ -103,7 +103,8 @@ pub(super) fn setup_custom_label<S: 'static>(
layout
.state
.widgets
.cast_as::<WidgetLabel>(attribs.widget_id)?
.cast_as::<WidgetLabel>(attribs.widget_id)
.unwrap()
.set_text_simple(&mut globals, Translation::from_raw_text(pretty_tz));
// does not need to be dynamic

View File

@@ -195,7 +195,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
color: TOOLTIP_COLOR,
border_color: TOOLTIP_BORDER_COLOR,
border: 2.0,
round: WLength::Percent(1.0),
round: WLength::Units(24.0),
..Default::default()
}),
taffy::Style {

View File

@@ -138,6 +138,7 @@ pub type ModifyLayoutStateFunc = Box<dyn FnOnce(ModifyLayoutStateData) -> anyhow
pub enum LayoutTask {
RemoveWidget(WidgetID),
SetWidgetStyle(WidgetID, event::StyleSetRequest),
ModifyLayoutState(ModifyLayoutStateFunc),
PlaySound(WguiSoundType),
Dispatch(Box<dyn FnOnce(&mut CallbackDataCommon) -> anyhow::Result<()>>),
@@ -706,12 +707,51 @@ impl Layout {
func(&mut c.common())?;
c.finish()?;
}
LayoutTask::SetWidgetStyle(widget_id, style_request) => {
self.set_style_request(widget_id, style_request);
}
}
}
Ok(())
}
fn set_style_request(&mut self, widget_id: WidgetID, style_request: event::StyleSetRequest) {
let Some(node_id) = self.state.nodes.get(widget_id) else {
return;
};
// taffy requires us to copy this whole 536-byte style struct.
// we can't get `&mut Style` directly from taffy unfortunately
let mut cur_style = self.state.tree.style(*node_id).unwrap().clone() /* always safe */;
match style_request {
event::StyleSetRequest::Display(display) => {
// refresh the component in case if visibility/display mode has changed
if cur_style.display != display
&& let Some(component) = self.registered_components_to_refresh.get(node_id)
{
self.components_to_refresh_once.insert(component.clone());
}
cur_style.display = display;
}
event::StyleSetRequest::Margin(margin) => {
cur_style.margin = margin;
}
event::StyleSetRequest::Width(val) => {
cur_style.size.width = val;
}
event::StyleSetRequest::Height(val) => {
cur_style.size.height = val;
}
}
if let Err(e) = self.state.tree.set_style(*node_id, cur_style) {
log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}");
}
}
pub fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> {
for task in alterables.tasks {
self.tasks.push(task);
@@ -747,39 +787,7 @@ impl Layout {
}
for (widget_id, style_request) in alterables.style_set_requests {
let Some(node_id) = self.state.nodes.get(widget_id) else {
continue;
};
// taffy requires us to copy this whole 536-byte style struct.
// we can't get `&mut Style` directly from taffy unfortunately
let mut cur_style = self.state.tree.style(*node_id).unwrap().clone() /* always safe */;
match style_request {
event::StyleSetRequest::Display(display) => {
// refresh the component in case if visibility/display mode has changed
if cur_style.display != display
&& let Some(component) = self.registered_components_to_refresh.get(node_id)
{
self.components_to_refresh_once.insert(component.clone());
}
cur_style.display = display;
}
event::StyleSetRequest::Margin(margin) => {
cur_style.margin = margin;
}
event::StyleSetRequest::Width(val) => {
cur_style.size.width = val;
}
event::StyleSetRequest::Height(val) => {
cur_style.size.height = val;
}
}
if let Err(e) = self.state.tree.set_style(*node_id, cur_style) {
log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}");
}
self.set_style_request(widget_id, style_request);
}
Ok(())

View File

@@ -47,7 +47,7 @@ impl WidgetObj for WidgetRectangle {
let boundary = drawing::Boundary::construct_relative(state.transform_stack);
let round_units = match self.params.round {
WLength::Units(units) => units as u8,
WLength::Units(units) => (f32::min(boundary.size.x, boundary.size.y) as u8 / 2).min(units as u8),
WLength::Percent(percent) => (f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8,
};