From b3522695564ae1d5dd939b5ab2e76d49a99c5127 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:10:46 +0900 Subject: [PATCH] add widget --- wayvrctl/src/main.rs | 2 +- wgui/doc/widgets.md | 50 ++++- wgui/src/drawing.rs | 17 +- wgui/src/parser/mod.rs | 8 +- wgui/src/parser/widget_image.rs | 78 +++++++ wgui/src/parser/widget_sprite.rs | 2 +- wgui/src/renderer_vk/context.rs | 21 +- wgui/src/renderer_vk/image.rs | 244 ++++++++++++++++++++++ wgui/src/renderer_vk/mod.rs | 1 + wgui/src/renderer_vk/model_buffer.rs | 15 +- wgui/src/renderer_vk/rect.rs | 2 +- wgui/src/renderer_vk/shaders/image.frag | 55 +++++ wgui/src/renderer_vk/shaders/image.vert | 52 +++++ wgui/src/renderer_vk/text/custom_glyph.rs | 6 +- wgui/src/renderer_vk/viewport.rs | 16 +- wgui/src/widget/image.rs | 113 ++++++++++ wgui/src/widget/mod.rs | 1 + wgui/src/widget/sprite.rs | 4 + wlx-overlay-s/src/overlays/custom.rs | 25 ++- 19 files changed, 686 insertions(+), 26 deletions(-) create mode 100644 wgui/src/parser/widget_image.rs create mode 100644 wgui/src/renderer_vk/image.rs create mode 100644 wgui/src/renderer_vk/shaders/image.frag create mode 100644 wgui/src/renderer_vk/shaders/image.vert create mode 100644 wgui/src/widget/image.rs diff --git a/wayvrctl/src/main.rs b/wayvrctl/src/main.rs index d29583f..7ecf454 100644 --- a/wayvrctl/src/main.rs +++ b/wayvrctl/src/main.rs @@ -265,7 +265,7 @@ enum SubcommandPanelModify { /// Color in HTML hex format (#rrggbb or #rrggbbaa) hex_color: String, }, - /// Set the content of a sprite + /// Set the content of a or . Max size for is 256x256. SetSprite { /// Absolute path to a svg, gif, png, jpeg or webp image. absolute_path: String, diff --git a/wgui/doc/widgets.md b/wgui/doc/widgets.md index 2c10d3c..f9e8ef7 100644 --- a/wgui/doc/widgets.md +++ b/wgui/doc/widgets.md @@ -160,7 +160,17 @@ _2nd gradient color_ ### `` -### Image widget, supports raster and svg vector +### Image widget for small images, + +Supported formats: svg, png, jpeg, gif, webp + +Maximum image size: 256x256 pixels + +For large or frequently changing images (e.g. album art), or if borders/rounding is desired, consider the `` tag instead. + +Sprite images are atlassed, making them very efficient to render. + +Adding large sprites permanently increases the atlas size (and thus VRAM usage) for the session. (Atlas space can be re-used, but the atlas won't shrink back.) #### Parameters @@ -180,6 +190,44 @@ _Internal (assets) image path_ _wgui internal image path. Do not use directly unless it's related to the core wgui assets._ +## image widget + +### `` + +### Image widget for large images + +Supported formats: svg, png, jpeg, gif, webp + +Maximum image size: Max texture size for the GPU (usually 8K+) + +For small images such as icons, consider using the `` tag instead. + +`` requires a single draw call per widget, while `` widgets all share a single draw call per panel. + +#### Parameters + +`src`: **string** + +_External (filesystem) image path. Falls back to Internal (assets) if not found._ + +`src_ext`: **string** + +_External (filesystem) image path_ + +`src_builtin`: **string** + +_Internal (assets) image path_ + +`src_internal`: **string** + +_wgui internal image path. Do not use directly unless it's related to the core wgui assets._ + +`round`: **float** (default: 0) | **percent** (0-100%) + +`border`: **float** + +`border_color`: #FFAABB | #FFAABBCC + --- # Components diff --git a/wgui/src/drawing.rs b/wgui/src/drawing.rs index 5d0c900..e439d8c 100644 --- a/wgui/src/drawing.rs +++ b/wgui/src/drawing.rs @@ -9,7 +9,10 @@ use crate::{ event::EventAlterables, globals::Globals, layout::Widget, - renderer_vk::text::{TextShadow, custom_glyph::CustomGlyph}, + renderer_vk::text::{ + custom_glyph::{CustomGlyph, CustomGlyphData}, + TextShadow, + }, stack::{self, ScissorBoundary, ScissorStack, TransformStack}, widget::{self, ScrollbarInfo, WidgetState}, }; @@ -175,6 +178,17 @@ pub struct Rectangle { pub round_units: u8, } +#[derive(Clone)] +pub struct ImagePrimitive { + pub content: CustomGlyphData, + pub content_key: usize, + + pub border: f32, // width in pixels + pub border_color: Color, + + pub round_units: u8, +} + pub struct PrimitiveExtent { pub(super) boundary: Boundary, pub(super) transform: Mat4, @@ -185,6 +199,7 @@ pub enum RenderPrimitive { Rectangle(PrimitiveExtent, Rectangle), Text(PrimitiveExtent, Rc>, Option), Sprite(PrimitiveExtent, Option), //option because we want as_slice + Image(PrimitiveExtent, ImagePrimitive), ScissorSet(ScissorBoundary), } diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index 92fb89f..2d1220c 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -3,6 +3,7 @@ mod component_checkbox; mod component_slider; mod style; mod widget_div; +mod widget_image; mod widget_label; mod widget_rectangle; mod widget_sprite; @@ -15,8 +16,8 @@ use crate::{ layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, parser::{ component_button::parse_component_button, component_checkbox::parse_component_checkbox, - component_slider::parse_component_slider, widget_div::parse_widget_div, widget_label::parse_widget_label, - widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite, + component_slider::parse_component_slider, widget_div::parse_widget_div, widget_image::parse_widget_image, + widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite, }, widget::ConstructEssentials, }; @@ -898,6 +899,9 @@ fn parse_child<'a>( "sprite" => { new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs)?); } + "image" => { + new_widget_id = Some(parse_widget_image(file, ctx, child_node, parent_id, &attribs)?); + } "Button" => { new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?); } diff --git a/wgui/src/parser/widget_image.rs b/wgui/src/parser/widget_image.rs new file mode 100644 index 0000000..0b4b44a --- /dev/null +++ b/wgui/src/parser/widget_image.rs @@ -0,0 +1,78 @@ +use crate::{ + assets::AssetPath, + layout::WidgetID, + parser::{ + parse_children, parse_widget_universal, print_invalid_attrib, + style::{parse_color, parse_round, parse_style}, + AttribPair, ParserContext, ParserFile, + }, + renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, + widget::image::{WidgetImage, WidgetImageParams}, +}; + +pub fn parse_widget_image<'a>( + file: &ParserFile, + ctx: &mut ParserContext, + node: roxmltree::Node<'a, 'a>, + parent_id: WidgetID, + attribs: &[AttribPair], +) -> anyhow::Result { + let mut params = WidgetImageParams::default(); + let style = parse_style(attribs); + let mut glyph = None; + + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { + "src" | "src_ext" | "src_builtin" | "src_internal" => { + let asset_path = match key { + "src" => AssetPath::FileOrBuiltIn(value), + "src_ext" => AssetPath::File(value), + "src_builtin" => AssetPath::BuiltIn(value), + "src_internal" => AssetPath::WguiInternal(value), + _ => unreachable!(), + }; + + if !value.is_empty() { + glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals, asset_path) { + Ok(glyph) => Some(glyph), + Err(e) => { + log::warn!("failed to load {value}: {e}"); + None + } + } + } + } + "round" => { + parse_round( + value, + &mut params.round, + ctx.doc_params.globals.get().defaults.rounding_mult, + ); + } + "border" => { + params.border = value.parse().unwrap_or_else(|_| { + print_invalid_attrib(key, value); + 0.0 + }); + } + "border_color" => { + parse_color(value, &mut params.border_color); + } + _ => {} + } + } + + if let Some(glyph) = glyph { + params.glyph_data = Some(CustomGlyphData::new(glyph)); + } else { + log::warn!("No source for image node!"); + } + + let (widget, _) = ctx.layout.add_child(parent_id, WidgetImage::create(params), style)?; + + parse_widget_universal(ctx, &widget, attribs); + parse_children(file, ctx, node, widget.id)?; + + Ok(widget.id) +} diff --git a/wgui/src/parser/widget_sprite.rs b/wgui/src/parser/widget_sprite.rs index fc86f16..8c3735e 100644 --- a/wgui/src/parser/widget_sprite.rs +++ b/wgui/src/parser/widget_sprite.rs @@ -1,7 +1,7 @@ use crate::{ assets::AssetPath, 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}, widget::sprite::{WidgetSprite, WidgetSpriteParams}, }; diff --git a/wgui/src/renderer_vk/context.rs b/wgui/src/renderer_vk/context.rs index 3f3247b..895f06d 100644 --- a/wgui/src/renderer_vk/context.rs +++ b/wgui/src/renderer_vk/context.rs @@ -2,21 +2,22 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; use cosmic_text::Buffer; use glam::{Mat4, Vec2, Vec3}; -use slotmap::{SlotMap, new_key_type}; +use slotmap::{new_key_type, SlotMap}; use vulkano::pipeline::graphics::viewport; use crate::{ drawing::{self}, font_config, - gfx::{WGfx, cmd::GfxCommandBuffer}, + gfx::{cmd::GfxCommandBuffer, WGfx}, + renderer_vk::image::{ImagePipeline, ImageRenderer}, }; use super::{ rect::{RectPipeline, RectRenderer}, text::{ - DEFAULT_METRICS, SWASH_CACHE, TextArea, TextBounds, text_atlas::{TextAtlas, TextPipeline}, text_renderer::TextRenderer, + TextArea, TextBounds, DEFAULT_METRICS, SWASH_CACHE, }, viewport::Viewport, }; @@ -26,6 +27,7 @@ struct RendererPass<'a> { text_areas: Vec>, text_renderer: TextRenderer, rect_renderer: RectRenderer, + image_renderer: ImageRenderer, scissor: Option, pixel_scale: f32, } @@ -34,16 +36,19 @@ impl RendererPass<'_> { fn new( text_atlas: &mut TextAtlas, rect_pipeline: RectPipeline, + image_pipeline: ImagePipeline, scissor: Option, pixel_scale: f32, ) -> anyhow::Result { let text_renderer = TextRenderer::new(text_atlas)?; let rect_renderer = RectRenderer::new(rect_pipeline)?; + let image_renderer = ImageRenderer::new(image_pipeline)?; Ok(Self { submitted: false, text_renderer, rect_renderer, + image_renderer, text_areas: Vec::new(), scissor, pixel_scale, @@ -90,6 +95,7 @@ impl RendererPass<'_> { self.submitted = true; self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?; + self.image_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?; { let mut font_system = font_system.system.lock(); @@ -119,18 +125,21 @@ pub struct SharedContext { atlas_map: SlotMap, rect_pipeline: RectPipeline, text_pipeline: TextPipeline, + image_pipeline: ImagePipeline, } impl SharedContext { pub fn new(gfx: Arc) -> anyhow::Result { let rect_pipeline = RectPipeline::new(gfx.clone(), gfx.surface_format)?; let text_pipeline = TextPipeline::new(gfx.clone(), gfx.surface_format)?; + let image_pipeline = ImagePipeline::new(gfx.clone(), gfx.surface_format)?; Ok(Self { gfx, atlas_map: SlotMap::with_key(), rect_pipeline, text_pipeline, + image_pipeline, }) } @@ -237,6 +246,7 @@ impl Context { passes.push(RendererPass::new( &mut atlas.text_atlas, shared.rect_pipeline.clone(), + shared.image_pipeline.clone(), next_scissor, self.pixel_scale, )?); @@ -293,6 +303,11 @@ impl Context { transform: extent.transform, }); } + drawing::RenderPrimitive::Image(extent, image) => { + pass + .image_renderer + .add_image(extent.boundary, image.clone(), &extent.transform); + } drawing::RenderPrimitive::ScissorSet(boundary) => { next_scissor = Some(boundary.0); needs_new_pass = true; diff --git a/wgui/src/renderer_vk/image.rs b/wgui/src/renderer_vk/image.rs new file mode 100644 index 0000000..9b8e0aa --- /dev/null +++ b/wgui/src/renderer_vk/image.rs @@ -0,0 +1,244 @@ +use std::{collections::HashMap, sync::Arc}; + +use cosmic_text::SubpixelBin; +use glam::Mat4; +use smallvec::smallvec; +use vulkano::{ + buffer::{BufferContents, BufferUsage, Subbuffer}, + command_buffer::CommandBufferUsage, + format::Format, + image::view::ImageView, + pipeline::graphics::{self, vertex_input::Vertex}, +}; + +use crate::{ + drawing::{Boundary, ImagePrimitive}, + gfx::{ + cmd::GfxCommandBuffer, + pass::WGfxPass, + pipeline::{WGfxPipeline, WPipelineCreateInfo}, + WGfx, BLEND_ALPHA, + }, + renderer_vk::{ + model_buffer::ModelBuffer, + text::custom_glyph::{CustomGlyphData, RasterizeCustomGlyphRequest, RasterizedCustomGlyph}, + }, +}; + +use super::viewport::Viewport; + +#[repr(C)] +#[derive(BufferContents, Vertex, Copy, Clone, Debug)] +pub struct ImageVertex { + #[format(R32_UINT)] + pub in_model_idx: u32, + #[format(R32_UINT)] + pub in_rect_dim: [u16; 2], + #[format(R32_UINT)] + pub in_border_color: u32, + #[format(R32_UINT)] + pub round_border: [u8; 4], +} + +/// Cloneable pipeline & shaders to be shared between `RectRenderer` instances. +#[derive(Clone)] +pub struct ImagePipeline { + gfx: Arc, + pub(super) inner: Arc>, +} + +impl ImagePipeline { + pub fn new(gfx: Arc, format: Format) -> anyhow::Result { + let vert = vert_image::load(gfx.device.clone())?; + let frag = frag_image::load(gfx.device.clone())?; + + let pipeline = gfx.create_pipeline::( + &vert, + &frag, + WPipelineCreateInfo::new(format) + .use_blend(BLEND_ALPHA) + .use_instanced() + .use_updatable_descriptors(smallvec![2]), + )?; + + Ok(Self { gfx, inner: pipeline }) + } +} + +struct ImageVertexWithContent { + vert: ImageVertex, + content: CustomGlyphData, + content_key: usize, // identifies an image tag. +} + +struct CachedPass { + content_id: usize, + vert_buffer: Subbuffer<[ImageVertex]>, + inner: WGfxPass, + res: [u32; 2], +} + +pub struct ImageRenderer { + pipeline: ImagePipeline, + image_verts: Vec, + model_buffer: ModelBuffer, + cached_passes: HashMap, +} + +impl ImageRenderer { + pub fn new(pipeline: ImagePipeline) -> anyhow::Result { + Ok(Self { + model_buffer: ModelBuffer::new(&pipeline.gfx)?, + pipeline, + image_verts: vec![], + cached_passes: HashMap::new(), + }) + } + + pub fn begin(&mut self) { + self.image_verts.clear(); + self.model_buffer.begin(); + } + + pub fn add_image(&mut self, boundary: Boundary, image: ImagePrimitive, transform: &Mat4) { + let in_model_idx = self + .model_buffer + .register_pos_size(&boundary.pos, &boundary.size, transform); + + self.image_verts.push(ImageVertexWithContent { + vert: ImageVertex { + in_model_idx, + in_rect_dim: [boundary.size.x as u16, boundary.size.y as u16], + in_border_color: cosmic_text::Color::from(image.border_color).0, + round_border: [ + image.round_units, + (image.border) as u8, + 0, // unused + 0, + ], + }, + content: image.content, + content_key: image.content_key, + }); + } + + fn upload_image( + gfx: Arc, + res: [u32; 2], + img: &ImageVertexWithContent, + ) -> anyhow::Result>> { + let raster = match RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest { + data: img.content.clone(), + width: res[0] as _, + height: res[1] as _, + x_bin: SubpixelBin::Zero, + y_bin: SubpixelBin::Zero, + scale: 1.0, // unused + }) { + Some(x) => x, + None => { + log::error!("Unable to rasterize custom image"); + return Ok(None); + } + }; + let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?; + + let image = cmd_buf.upload_image( + raster.width as _, + raster.height as _, + Format::R8G8B8A8_UNORM, + &raster.data, + )?; + let image_view = ImageView::new_default(image)?; + + cmd_buf.build_and_execute_now()?; + + Ok(Some(image_view)) + } + + pub fn render( + &mut self, + gfx: &Arc, + viewport: &mut Viewport, + vk_scissor: &graphics::viewport::Scissor, + cmd_buf: &mut GfxCommandBuffer, + ) -> anyhow::Result<()> { + let res = viewport.resolution(); + self.model_buffer.upload(gfx)?; + + for img in self.image_verts.iter() { + let pass = match self.cached_passes.get_mut(&img.content_key) { + Some(x) => { + if x.content_id != img.content.id || x.res != res { + // image changed + let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else { + continue; + }; + + x.inner + .update_sampler(2, image_view, self.pipeline.gfx.texture_filter)?; + } + + x + } + None => { + let vert_buffer = self.pipeline.gfx.empty_buffer( + BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST, + (std::mem::size_of::()) as _, + )?; + + let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else { + continue; + }; + + let set0 = viewport.get_image_descriptor(&self.pipeline); + let set1 = self.model_buffer.get_image_descriptor(&self.pipeline); + let set2 = self + .pipeline + .inner + .uniform_sampler(2, image_view, self.pipeline.gfx.texture_filter)?; + + let pass = self.pipeline.inner.create_pass( + [res[0] as _, res[1] as _], + vert_buffer.clone(), + 0..4, + 0..self.image_verts.len() as _, + vec![set0, set1, set2], + vk_scissor, + )?; + + self.cached_passes.insert( + img.content_key, + CachedPass { + content_id: img.content.id, + vert_buffer, + inner: pass, + res, + }, + ); + self.cached_passes.get_mut(&img.content_key).unwrap() + } + }; + + pass.vert_buffer.write()?[0..1].clone_from_slice(&[img.vert]); + + cmd_buf.run_ref(&pass.inner)?; + } + + Ok(()) + } +} + +pub mod vert_image { + vulkano_shaders::shader! { + ty: "vertex", + path: "src/renderer_vk/shaders/image.vert", + } +} + +pub mod frag_image { + vulkano_shaders::shader! { + ty: "fragment", + path: "src/renderer_vk/shaders/image.frag", + } +} diff --git a/wgui/src/renderer_vk/mod.rs b/wgui/src/renderer_vk/mod.rs index d6f00a5..75b514d 100644 --- a/wgui/src/renderer_vk/mod.rs +++ b/wgui/src/renderer_vk/mod.rs @@ -1,4 +1,5 @@ pub mod context; +pub mod image; pub mod model_buffer; pub mod rect; pub mod text; diff --git a/wgui/src/renderer_vk/model_buffer.rs b/wgui/src/renderer_vk/model_buffer.rs index 1567ab6..931122d 100644 --- a/wgui/src/renderer_vk/model_buffer.rs +++ b/wgui/src/renderer_vk/model_buffer.rs @@ -8,7 +8,7 @@ use vulkano::{ use crate::{ gfx, - renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline}, + renderer_vk::{image::ImagePipeline, rect::RectPipeline, text::text_atlas::TextPipeline}, }; pub struct ModelBuffer { @@ -19,6 +19,8 @@ pub struct ModelBuffer { buffer_capacity_f32: u32, rect_descriptor: Option>, + text_descriptor: Option>, + image_descriptor: Option>, } impl ModelBuffer { @@ -40,6 +42,8 @@ impl ModelBuffer { buffer, buffer_capacity_f32: INITIAL_CAPACITY_F32, rect_descriptor: None, + text_descriptor: None, + image_descriptor: None, }) } @@ -109,8 +113,15 @@ impl ModelBuffer { pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc { self - .rect_descriptor + .text_descriptor .get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap()) .clone() } + + pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc { + self + .image_descriptor + .get_or_insert_with(|| pipeline.inner.buffer(1, self.buffer.clone()).unwrap()) + .clone() + } } diff --git a/wgui/src/renderer_vk/rect.rs b/wgui/src/renderer_vk/rect.rs index 54eb908..b1e52e7 100644 --- a/wgui/src/renderer_vk/rect.rs +++ b/wgui/src/renderer_vk/rect.rs @@ -10,10 +10,10 @@ use vulkano::{ use crate::{ drawing::{Boundary, Rectangle}, gfx::{ - BLEND_ALPHA, WGfx, cmd::GfxCommandBuffer, pass::WGfxPass, pipeline::{WGfxPipeline, WPipelineCreateInfo}, + WGfx, BLEND_ALPHA, }, renderer_vk::model_buffer::ModelBuffer, }; diff --git a/wgui/src/renderer_vk/shaders/image.frag b/wgui/src/renderer_vk/shaders/image.frag new file mode 100644 index 0000000..02767d4 --- /dev/null +++ b/wgui/src/renderer_vk/shaders/image.frag @@ -0,0 +1,55 @@ + +#version 450 +#extension GL_GOOGLE_include_directive : enable + +precision highp float; + +layout(location = 0) in vec2 in_uv; +layout(location = 1) in vec4 in_border_color; +layout(location = 2) in float in_border_size; // in units +layout(location = 3) in float in_radius; // in units +layout(location = 4) in vec2 in_rect_size; +layout(location = 0) out vec4 out_color; + +#define UNIFORM_PARAMS_SET 0 +#include "uniform.glsl" + +layout(set = 2, binding = 0) uniform sampler2D image; + +void main() { + float rect_aspect = in_rect_size.x / in_rect_size.y; + + vec2 center = in_rect_size / 2.0; + vec2 coords = in_uv * in_rect_size; + + + float radius = in_radius; + + vec2 sdf_rect_dim = center - vec2(radius); + float sdf = length(max(abs(coords - center), sdf_rect_dim) - sdf_rect_dim) - + in_radius; + + vec4 color = texture(image, in_uv); + + float pixel_size = 1.0 / uniforms.pixel_scale; + + if (in_border_size < in_radius) { + // rounded border + float f = in_border_size > 0.0 ? smoothstep(in_border_size + pixel_size, + in_border_size, -sdf) * + in_border_color.a + : 0.0; + out_color = mix(color, in_border_color, f); + } else { + // square border + vec2 a = abs(coords - center); + float aa = center.x - in_border_size; + float bb = center.y - in_border_size; + out_color = (a.x > aa || a.y > bb) ? in_border_color : color; + } + + if (in_radius > 0.0) { + // rounding cutout alpha + out_color.a *= 1.0 - smoothstep(-pixel_size, 0.0, sdf); + } +} diff --git a/wgui/src/renderer_vk/shaders/image.vert b/wgui/src/renderer_vk/shaders/image.vert new file mode 100644 index 0000000..2208dc1 --- /dev/null +++ b/wgui/src/renderer_vk/shaders/image.vert @@ -0,0 +1,52 @@ +#version 450 +#extension GL_GOOGLE_include_directive : enable + +precision highp float; + +layout(location = 0) in uint in_model_idx; +layout(location = 1) in uint in_rect_dim; +layout(location = 2) in uint in_border_color; +layout(location = 3) in uint round_border; + +layout(location = 0) out vec2 out_uv; +layout(location = 1) out vec4 out_border_color; +layout(location = 2) out float out_border_size; +layout(location = 3) out float out_radius; +layout(location = 4) out vec2 out_rect_size; + +#define UNIFORM_PARAMS_SET 0 +#define MODEL_BUFFER_SET 1 + +#include "model_buffer.glsl" +#include "uniform.glsl" + +void main() { + uint v = uint(gl_VertexIndex); // 0-3 + uint rect_width = in_rect_dim & 0xffffu; + uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u; + vec2 rect_size = vec2(float(rect_width), float(rect_height)); + float rect_aspect = rect_size.x / rect_size.y; + + // 0.0 - 1.0 normalized + uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u); + vec2 corner_pos = vec2(corner_pos_u); + out_uv = corner_pos; + + mat4 model_matrix = model_buffer.models[in_model_idx]; + + out_rect_size = rect_size; + + gl_Position = uniforms.projection * model_matrix * vec4(corner_pos, 0.0, 1.0); + + out_border_color = + vec4(float((in_border_color & 0x00ff0000u) >> 16u) / 255.0, + float((in_border_color & 0x0000ff00u) >> 8u) / 255.0, + float(in_border_color & 0x000000ffu) / 255.0, + float((in_border_color & 0xff000000u) >> 24u) / 255.0); + + float radius = float(round_border & 0xffu); + out_radius = radius; + + float border_size = float((round_border & 0xff00u) >> 8); + out_border_size = border_size; +} diff --git a/wgui/src/renderer_vk/text/custom_glyph.rs b/wgui/src/renderer_vk/text/custom_glyph.rs index ed4fd57..66ac8f7 100644 --- a/wgui/src/renderer_vk/text/custom_glyph.rs +++ b/wgui/src/renderer_vk/text/custom_glyph.rs @@ -53,8 +53,8 @@ impl CustomGlyphContent { /// Clone and reuse this to avoid atlasing the same content twice. #[derive(Debug, Clone)] pub struct CustomGlyphData { - pub(super) id: usize, - pub(super) content: Arc, + pub(crate) id: usize, + pub(crate) content: Arc, } impl CustomGlyphData { @@ -157,7 +157,7 @@ pub struct RasterizedCustomGlyph { } impl RasterizedCustomGlyph { - pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option { + pub(crate) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option { match input.data.content.as_ref() { CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input), CustomGlyphContent::Image(data) => Some(rasterize_image(data)), diff --git a/wgui/src/renderer_vk/viewport.rs b/wgui/src/renderer_vk/viewport.rs index 05b8c35..ee6b0b4 100644 --- a/wgui/src/renderer_vk/viewport.rs +++ b/wgui/src/renderer_vk/viewport.rs @@ -5,7 +5,10 @@ use vulkano::{ descriptor_set::DescriptorSet, }; -use crate::{gfx::WGfx, renderer_vk::util::WMat4}; +use crate::{ + gfx::WGfx, + renderer_vk::{image::ImagePipeline, util::WMat4}, +}; use super::{rect::RectPipeline, text::text_atlas::TextPipeline}; @@ -16,6 +19,7 @@ pub struct Viewport { params_buffer: Subbuffer<[Params]>, text_descriptor: Option>, rect_descriptor: Option>, + image_descriptor: Option>, } impl Viewport { @@ -36,6 +40,7 @@ impl Viewport { params_buffer, text_descriptor: None, rect_descriptor: None, + image_descriptor: None, }) } @@ -57,6 +62,15 @@ impl Viewport { .clone() } + pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc { + self + .image_descriptor + .get_or_insert_with(|| { + pipeline.inner.buffer(0, self.params_buffer.clone()).unwrap() // safe unwrap + }) + .clone() + } + /// Updates the `Viewport` with the given `resolution` and `projection`. pub fn update(&mut self, resolution: [u32; 2], projection: &glam::Mat4, pixel_scale: f32) -> anyhow::Result<()> { if self.params.screen_resolution == resolution diff --git a/wgui/src/widget/image.rs b/wgui/src/widget/image.rs new file mode 100644 index 0000000..f02aaeb --- /dev/null +++ b/wgui/src/widget/image.rs @@ -0,0 +1,113 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use slotmap::Key; + +use crate::{ + drawing::{self, ImagePrimitive, PrimitiveExtent}, + event::CallbackDataCommon, + globals::Globals, + layout::WidgetID, + renderer_vk::text::custom_glyph::CustomGlyphData, + widget::{util::WLength, WidgetStateFlags}, +}; + +use super::{WidgetObj, WidgetState}; + +static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Default)] +pub struct WidgetImageParams { + pub glyph_data: Option, + + pub border: f32, + pub border_color: drawing::Color, + + pub round: WLength, +} + +#[derive(Debug, Default)] +pub struct WidgetImage { + params: WidgetImageParams, + id: WidgetID, + content_key: usize, +} + +impl WidgetImage { + pub fn create(params: WidgetImageParams) -> WidgetState { + WidgetState::new( + WidgetStateFlags::default(), + Box::new(Self { + params, + id: WidgetID::null(), + content_key: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed), + }), + ) + } + + pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + if self.params.glyph_data == content { + return; + } + + self.params.glyph_data = content; + common.mark_widget_dirty(self.id); + } + + pub fn get_content(&self) -> Option { + self.params.glyph_data.clone() + } +} + +impl WidgetObj for WidgetImage { + fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) { + let boundary = drawing::Boundary::construct_relative(state.transform_stack); + + let Some(content) = self.params.glyph_data.clone() else { + return; + }; + + let round_units = match self.params.round { + WLength::Units(units) => units as u8, + WLength::Percent(percent) => (f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8, + }; + + state.primitives.push(drawing::RenderPrimitive::Image( + PrimitiveExtent { + boundary, + transform: state.transform_stack.get().transform, + }, + ImagePrimitive { + content, + content_key: self.content_key, + border: self.params.border, + border_color: self.params.border_color, + round_units, + }, + )); + } + + fn measure( + &mut self, + _globals: &Globals, + _known_dimensions: taffy::Size>, + _available_space: taffy::Size, + ) -> taffy::Size { + taffy::Size::ZERO + } + + fn get_id(&self) -> WidgetID { + self.id + } + + fn set_id(&mut self, id: WidgetID) { + self.id = id; + } + + fn get_type(&self) -> super::WidgetType { + super::WidgetType::Sprite + } + + fn debug_print(&self) -> String { + String::default() + } +} diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index f0712a4..912436d 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -17,6 +17,7 @@ use crate::{ }; pub mod div; +pub mod image; pub mod label; pub mod rectangle; pub mod sprite; diff --git a/wgui/src/widget/sprite.rs b/wgui/src/widget/sprite.rs index f64f947..50e9c81 100644 --- a/wgui/src/widget/sprite.rs +++ b/wgui/src/widget/sprite.rs @@ -50,6 +50,10 @@ impl WidgetSprite { } pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + if self.params.glyph_data == content { + return; + } + self.params.glyph_data = content; common.mark_widget_dirty(self.id); } diff --git a/wlx-overlay-s/src/overlays/custom.rs b/wlx-overlay-s/src/overlays/custom.rs index 3637fe4..9626c72 100644 --- a/wlx-overlay-s/src/overlays/custom.rs +++ b/wlx-overlay-s/src/overlays/custom.rs @@ -9,7 +9,9 @@ use wgui::{ parser::{Fetchable, parse_color_hex}, renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, taffy, - widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite}, + widget::{ + image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite, + }, }; use wlx_common::windowing::OverlayWindowState; @@ -111,23 +113,26 @@ fn apply_custom_command( } } ModifyPanelCommand::SetSprite(path) => { - let mut widget = panel + if let Ok(pair) = panel .parser_state - .fetch_widget_as::(&panel.layout.state, element) - .context("No with such id.")?; - - if path == "none" { - widget.set_content(&mut com, None); - } else { + .fetch_widget(&panel.layout.state, element) + { let content = CustomGlyphContent::from_assets( &mut app.wgui_globals, wgui::assets::AssetPath::File(&path), ) .context("Could not load content from supplied path.")?; - let data = CustomGlyphData::new(content); - widget.set_content(&mut com, Some(data)); + if let Some(mut sprite) = pair.widget.get_as_mut::() { + sprite.set_content(&mut com, Some(data)); + } else if let Some(mut image) = pair.widget.get_as_mut::() { + image.set_content(&mut com, Some(data)); + } else { + anyhow::bail!("No or with such id."); + } + } else { + anyhow::bail!("No or with such id."); } } ModifyPanelCommand::SetColor(color) => {