cached image loading

This commit is contained in:
galister
2025-12-27 22:06:28 +09:00
parent f4545241df
commit 8747487beb
8 changed files with 126 additions and 41 deletions

View File

@@ -4,7 +4,7 @@ use wgui::{
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, renderer_vk::text::custom_glyph::CustomGlyphData,
taffy::{self, prelude::length}, taffy::{self, prelude::length},
widget::{ widget::{
label::{WidgetLabel, WidgetLabelParams}, label::{WidgetLabel, WidgetLabelParams},
@@ -62,7 +62,7 @@ pub fn mount_simple_sprite_square(
layout.add_child( layout.add_child(
parent_id, parent_id,
WidgetSprite::create(WidgetSpriteParams { WidgetSprite::create(WidgetSpriteParams {
glyph_data: Some(CustomGlyphData::new(CustomGlyphContent::from_assets(globals, path)?)), glyph_data: Some(CustomGlyphData::from_assets(globals, path)?),
..Default::default() ..Default::default()
}), }),
taffy::Style { taffy::Style {

View File

@@ -14,7 +14,7 @@ use wgui::{
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::{ renderer_vk::text::{
FontWeight, HorizontalAlign, TextShadow, TextStyle, FontWeight, HorizontalAlign, TextShadow, TextStyle,
custom_glyph::{CustomGlyphContent, CustomGlyphData}, custom_glyph::{ CustomGlyphData},
}, },
taffy::{ taffy::{
self, AlignItems, AlignSelf, JustifyContent, JustifySelf, self, AlignItems, AlignSelf, JustifyContent, JustifySelf,
@@ -413,10 +413,10 @@ impl View {
fn get_placeholder_image(&mut self) -> anyhow::Result<&CustomGlyphData> { fn get_placeholder_image(&mut self) -> anyhow::Result<&CustomGlyphData> {
if self.img_placeholder.is_none() { if self.img_placeholder.is_none() {
let c = CustomGlyphData::new(CustomGlyphContent::from_assets( let c = CustomGlyphData::from_assets(
&self.globals, &self.globals,
AssetPath::BuiltIn("dashboard/placeholder_cover.png"), AssetPath::BuiltIn("dashboard/placeholder_cover.png"),
)?); )?;
self.img_placeholder = Some(c); self.img_placeholder = Some(c);
} }
@@ -515,7 +515,8 @@ impl View {
return Ok(()); return Ok(());
}; };
let glyph = match CustomGlyphContent::from_bin_raster(&cover_art.compressed_image_data) { let path = format!("app:{app_id:?}");
let glyph = match CustomGlyphData::from_bytes_raster(&self.globals, &path ,&cover_art.compressed_image_data) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
log::warn!( log::warn!(
@@ -526,7 +527,7 @@ impl View {
return Ok(()); return Ok(());
} }
}; };
View::mount_image(layout, cell, &CustomGlyphData::new(glyph))?; View::mount_image(layout, cell, &glyph)?;
} }
Ok(()) Ok(())

View File

@@ -1,24 +1,22 @@
use crate::{ use crate::{
animation::{Animation, AnimationEasing}, animation::{Animation, AnimationEasing},
assets::AssetPath, assets::AssetPath,
components::{self, Component, ComponentBase, ComponentTrait, RefreshData, tooltip::ComponentTooltip}, components::{self, tooltip::ComponentTooltip, Component, ComponentBase, ComponentTrait, RefreshData},
drawing::{self, Boundary, Color}, drawing::{self, Boundary, Color},
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind}, event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation, i18n::Translation,
layout::{LayoutTask, WidgetID, WidgetPair}, layout::{LayoutTask, WidgetID, WidgetPair},
renderer_vk::{ renderer_vk::{
text::{ text::{custom_glyph::CustomGlyphData, FontWeight, TextStyle},
FontWeight, TextStyle,
custom_glyph::{CustomGlyphContent, CustomGlyphData},
},
util::centered_matrix, util::centered_matrix,
}, },
widget::{ widget::{
self, ConstructEssentials, EventResult, WidgetData, self,
label::{WidgetLabel, WidgetLabelParams}, label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams}, rectangle::{WidgetRectangle, WidgetRectangleParams},
sprite::{WidgetSprite, WidgetSpriteParams}, sprite::{WidgetSprite, WidgetSpriteParams},
util::WLength, util::WLength,
ConstructEssentials, EventResult, WidgetData,
}, },
}; };
use glam::{Mat4, Vec3}; use glam::{Mat4, Vec3};
@@ -27,7 +25,7 @@ use std::{
rc::Rc, rc::Rc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use taffy::{AlignItems, JustifyContent, prelude::length}; use taffy::{prelude::length, AlignItems, JustifyContent};
pub struct Params<'a> { pub struct Params<'a> {
pub text: Option<Translation>, // if unset, label will not be populated pub text: Option<Translation>, // if unset, label will not be populated
@@ -461,10 +459,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
if let Some(sprite_path) = params.sprite_src { if let Some(sprite_path) = params.sprite_src {
let sprite = WidgetSprite::create(WidgetSpriteParams { let sprite = WidgetSprite::create(WidgetSpriteParams {
glyph_data: Some(CustomGlyphData::new(CustomGlyphContent::from_assets( glyph_data: Some(CustomGlyphData::from_assets(&globals, sprite_path)?),
&globals,
sprite_path,
)?)),
..Default::default() ..Default::default()
}); });

View File

@@ -12,6 +12,7 @@ use crate::{
assets_internal, drawing, assets_internal, drawing,
font_config::{WguiFontConfig, WguiFontSystem}, font_config::{WguiFontConfig, WguiFontSystem},
i18n::I18n, i18n::I18n,
renderer_vk::text::custom_glyph::CustomGlyphCache,
}; };
#[derive(Clone)] #[derive(Clone)]
@@ -52,6 +53,7 @@ pub struct Globals {
pub i18n_builtin: I18n, pub i18n_builtin: I18n,
pub defaults: Defaults, pub defaults: Defaults,
pub font_system: WguiFontSystem, pub font_system: WguiFontSystem,
pub custom_glyph_cache: CustomGlyphCache,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -74,6 +76,7 @@ impl WguiGlobals {
defaults, defaults,
asset_folder, asset_folder,
font_system: WguiFontSystem::new(font_config), font_system: WguiFontSystem::new(font_config),
custom_glyph_cache: CustomGlyphCache::new(),
})))) }))))
} }

View File

@@ -2,10 +2,11 @@ use crate::{
assets::AssetPath, assets::AssetPath,
layout::WidgetID, layout::WidgetID,
parser::{ parser::{
AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, print_invalid_attrib, parse_children, parse_widget_universal, print_invalid_attrib,
style::{parse_color, parse_round, parse_style}, style::{parse_color, parse_round, parse_style},
AttribPair, ParserContext, ParserFile,
}, },
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, renderer_vk::text::custom_glyph::CustomGlyphData,
widget::image::{WidgetImage, WidgetImageParams}, widget::image::{WidgetImage, WidgetImageParams},
}; };
@@ -33,7 +34,7 @@ pub fn parse_widget_image<'a>(
}; };
if !value.is_empty() { if !value.is_empty() {
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals, asset_path) { glyph = match CustomGlyphData::from_assets(&mut ctx.layout.state.globals, asset_path) {
Ok(glyph) => Some(glyph), Ok(glyph) => Some(glyph),
Err(e) => { Err(e) => {
log::warn!("failed to load {value}: {e}"); log::warn!("failed to load {value}: {e}");
@@ -63,7 +64,7 @@ pub fn parse_widget_image<'a>(
} }
if let Some(glyph) = glyph { if let Some(glyph) = glyph {
params.glyph_data = Some(CustomGlyphData::new(glyph)); params.glyph_data = Some(glyph);
} else { } else {
log::warn!("No source for image node!"); log::warn!("No source for image node!");
} }

View File

@@ -1,8 +1,8 @@
use crate::{ use crate::{
assets::AssetPath, assets::AssetPath,
layout::WidgetID, layout::WidgetID,
parser::{AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, style::parse_style}, parser::{parse_children, parse_widget_universal, style::parse_style, AttribPair, ParserContext, ParserFile},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, renderer_vk::text::custom_glyph::CustomGlyphData,
widget::sprite::{WidgetSprite, WidgetSpriteParams}, widget::sprite::{WidgetSprite, WidgetSpriteParams},
}; };
@@ -32,7 +32,7 @@ pub fn parse_widget_sprite<'a>(
}; };
if !value.is_empty() { if !value.is_empty() {
glyph = match CustomGlyphContent::from_assets(&ctx.layout.state.globals, asset_path) { glyph = match CustomGlyphData::from_assets(&ctx.layout.state.globals, asset_path) {
Ok(glyph) => Some(glyph), Ok(glyph) => Some(glyph),
Err(e) => { Err(e) => {
log::warn!("failed to load {value}: {e}"); log::warn!("failed to load {value}: {e}");
@@ -53,7 +53,7 @@ pub fn parse_widget_sprite<'a>(
} }
if let Some(glyph) = glyph { if let Some(glyph) = glyph {
params.glyph_data = Some(CustomGlyphData::new(glyph)); params.glyph_data = Some(glyph);
} else { } else {
log::warn!("No source for sprite node!"); log::warn!("No source for sprite node!");
} }

View File

@@ -1,8 +1,10 @@
use std::{ use std::{
collections::HashMap,
f32, f32,
hash::{DefaultHasher, Hasher},
sync::{ sync::{
Arc,
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
Arc, Weak,
}, },
}; };
@@ -14,15 +16,53 @@ use crate::{assets::AssetPath, globals::WguiGlobals};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0); static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
#[derive(Hash, PartialEq, Eq)]
pub struct HashedAsset {
path: String,
hash: u64,
}
pub struct CustomGlyphCache {
inner: HashMap<HashedAsset, CustomGlyphDataWeak>,
}
impl CustomGlyphCache {
pub(crate) fn new() -> Self {
Self { inner: HashMap::new() }
}
fn get(&self, path: &str, bytes: &[u8]) -> Result<CustomGlyphData, HashedAsset> {
let mut hasher = DefaultHasher::new();
hasher.write(bytes);
let hash = hasher.finish();
let hashed_asset = HashedAsset {
path: path.to_string(),
hash,
};
self
.inner
.get(&hashed_asset)
.and_then(|a| a.upgrade())
.inspect(|_| log::debug!("Glyph cache hit on: '{path}'"))
.ok_or(hashed_asset)
}
fn insert(&mut self, hashed_asset: HashedAsset, data: &CustomGlyphData) {
self.inner.insert(hashed_asset, data.clone_weak());
}
}
/// The raw content of a glyph. /// The raw content of a glyph.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CustomGlyphContent { pub(crate) enum CustomGlyphContent {
Svg(Box<Tree>), Svg(Box<Tree>),
Image(RgbaImage), Image(RgbaImage),
} }
impl CustomGlyphContent { impl CustomGlyphContent {
pub fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> { fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> {
let options = Options { let options = Options {
style_sheet: Some("svg { color: white }".into()), style_sheet: Some("svg { color: white }".into()),
..Options::default() ..Options::default()
@@ -32,19 +72,23 @@ impl CustomGlyphContent {
Ok(Self::Svg(Box::new(tree))) Ok(Self::Svg(Box::new(tree)))
} }
pub fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> { fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> {
let image = image::load_from_memory(data)?.into_rgba8(); let image = image::load_from_memory(data)?.into_rgba8();
Ok(Self::Image(image)) Ok(Self::Image(image))
} }
}
#[allow(clippy::case_sensitive_file_extension_comparisons)] struct CustomGlyphDataWeak {
pub fn from_assets(globals: &WguiGlobals, path: AssetPath) -> anyhow::Result<Self> { id: usize,
let path_str = path.get_str(); content: Weak<CustomGlyphContent>,
let data = globals.get_asset(path)?; }
if path_str.ends_with(".svg") || path_str.ends_with(".svgz") {
Ok(Self::from_bin_svg(&data)?) impl CustomGlyphDataWeak {
fn upgrade(&self) -> Option<CustomGlyphData> {
if let Some(content) = self.content.upgrade() {
Some(CustomGlyphData { id: self.id, content })
} else { } else {
Ok(Self::from_bin_raster(&data)?) None
} }
} }
} }
@@ -58,13 +102,20 @@ pub struct CustomGlyphData {
} }
impl CustomGlyphData { impl CustomGlyphData {
pub fn new(content: CustomGlyphContent) -> Self { fn new(content: CustomGlyphContent) -> Self {
Self { Self {
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed), id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
content: Arc::new(content), content: Arc::new(content),
} }
} }
fn clone_weak(&self) -> CustomGlyphDataWeak {
CustomGlyphDataWeak {
id: self.id,
content: Arc::downgrade(&self.content),
}
}
pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) { pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) {
const MAX_RASTER_DIM: u16 = 256; const MAX_RASTER_DIM: u16 = 256;
match self.content.as_ref() { match self.content.as_ref() {
@@ -75,6 +126,41 @@ impl CustomGlyphData {
CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _), CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _),
} }
} }
pub fn from_assets(globals: &WguiGlobals, path: AssetPath) -> anyhow::Result<Self> {
let path_str = path.get_str();
let data = globals.get_asset(path)?;
if path_str.ends_with(".svg") || path_str.ends_with(".svgz") {
Self::from_bytes_svg(globals, path_str, &data)
} else {
Self::from_bytes_raster(globals, path_str, &data)
}
}
pub fn from_bytes_raster(globals: &WguiGlobals, path: &str, data: &[u8]) -> anyhow::Result<Self> {
let globals_borrow = &mut globals.get();
match globals_borrow.custom_glyph_cache.get(path, data) {
Ok(data) => return Ok(data),
Err(hashed_asset) => {
let data = Self::new(CustomGlyphContent::from_bin_raster(data)?);
globals_borrow.custom_glyph_cache.insert(hashed_asset, &data);
Ok(data)
}
}
}
pub fn from_bytes_svg(globals: &WguiGlobals, path: &str, data: &[u8]) -> anyhow::Result<Self> {
let globals_borrow = &mut globals.get();
match globals_borrow.custom_glyph_cache.get(path, data) {
Ok(data) => return Ok(data),
Err(hashed_asset) => {
let data = Self::new(CustomGlyphContent::from_bin_svg(data)?);
globals_borrow.custom_glyph_cache.insert(hashed_asset, &data);
Ok(data)
}
}
}
} }
impl PartialEq for CustomGlyphData { impl PartialEq for CustomGlyphData {

View File

@@ -7,7 +7,7 @@ use wgui::{
event::{CallbackDataCommon, EventAlterables}, event::{CallbackDataCommon, EventAlterables},
i18n::Translation, i18n::Translation,
parser::{Fetchable, parse_color_hex}, parser::{Fetchable, parse_color_hex},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, renderer_vk::text::custom_glyph::CustomGlyphData,
taffy, taffy,
widget::{ widget::{
image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite, image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite,
@@ -117,12 +117,11 @@ fn apply_custom_command(
.parser_state .parser_state
.fetch_widget(&panel.layout.state, element) .fetch_widget(&panel.layout.state, element)
{ {
let content = CustomGlyphContent::from_assets( let data = CustomGlyphData::from_assets(
&app.wgui_globals, &app.wgui_globals,
wgui::assets::AssetPath::File(path), wgui::assets::AssetPath::File(path),
) )
.context("Could not load content from supplied path.")?; .context("Could not load content from supplied path.")?;
let data = CustomGlyphData::new(content);
if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() { if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() {
sprite.set_content(&mut com, Some(data)); sprite.set_content(&mut com, Some(data));