add <image> widget

This commit is contained in:
galister
2025-12-23 22:10:46 +09:00
parent 17453ae1d9
commit b352269556
19 changed files with 686 additions and 26 deletions

View File

@@ -265,7 +265,7 @@ enum SubcommandPanelModify {
/// Color in HTML hex format (#rrggbb or #rrggbbaa) /// Color in HTML hex format (#rrggbb or #rrggbbaa)
hex_color: String, hex_color: String,
}, },
/// Set the content of a sprite /// Set the content of a <sprite> or <image>. Max size for <sprite> is 256x256.
SetSprite { SetSprite {
/// Absolute path to a svg, gif, png, jpeg or webp image. /// Absolute path to a svg, gif, png, jpeg or webp image.
absolute_path: String, absolute_path: String,

View File

@@ -160,7 +160,17 @@ _2nd gradient color_
### `<sprite>` ### `<sprite>`
### 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 `<image>` 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 #### 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._ _wgui internal image path. Do not use directly unless it's related to the core wgui assets._
## image widget
### `<image>`
### 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 `<sprite>` tag instead.
`<image>` requires a single draw call per widget, while `<sprite>` 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 # Components

View File

@@ -9,7 +9,10 @@ use crate::{
event::EventAlterables, event::EventAlterables,
globals::Globals, globals::Globals,
layout::Widget, layout::Widget,
renderer_vk::text::{TextShadow, custom_glyph::CustomGlyph}, renderer_vk::text::{
custom_glyph::{CustomGlyph, CustomGlyphData},
TextShadow,
},
stack::{self, ScissorBoundary, ScissorStack, TransformStack}, stack::{self, ScissorBoundary, ScissorStack, TransformStack},
widget::{self, ScrollbarInfo, WidgetState}, widget::{self, ScrollbarInfo, WidgetState},
}; };
@@ -175,6 +178,17 @@ pub struct Rectangle {
pub round_units: u8, 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 struct PrimitiveExtent {
pub(super) boundary: Boundary, pub(super) boundary: Boundary,
pub(super) transform: Mat4, pub(super) transform: Mat4,
@@ -185,6 +199,7 @@ pub enum RenderPrimitive {
Rectangle(PrimitiveExtent, Rectangle), Rectangle(PrimitiveExtent, Rectangle),
Text(PrimitiveExtent, Rc<RefCell<Buffer>>, Option<TextShadow>), Text(PrimitiveExtent, Rc<RefCell<Buffer>>, Option<TextShadow>),
Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice
Image(PrimitiveExtent, ImagePrimitive),
ScissorSet(ScissorBoundary), ScissorSet(ScissorBoundary),
} }

View File

@@ -3,6 +3,7 @@ mod component_checkbox;
mod component_slider; mod component_slider;
mod style; mod style;
mod widget_div; mod widget_div;
mod widget_image;
mod widget_label; mod widget_label;
mod widget_rectangle; mod widget_rectangle;
mod widget_sprite; mod widget_sprite;
@@ -15,8 +16,8 @@ use crate::{
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
parser::{ parser::{
component_button::parse_component_button, component_checkbox::parse_component_checkbox, 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, component_slider::parse_component_slider, widget_div::parse_widget_div, widget_image::parse_widget_image,
widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite, widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
}, },
widget::ConstructEssentials, widget::ConstructEssentials,
}; };
@@ -898,6 +899,9 @@ fn parse_child<'a>(
"sprite" => { "sprite" => {
new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs)?); 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" => { "Button" => {
new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?); new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?);
} }

View File

@@ -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<WidgetID> {
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)
}

View File

@@ -1,7 +1,7 @@
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::{CustomGlyphContent, CustomGlyphData},
widget::sprite::{WidgetSprite, WidgetSpriteParams}, widget::sprite::{WidgetSprite, WidgetSpriteParams},
}; };

View File

@@ -2,21 +2,22 @@ use std::{cell::RefCell, rc::Rc, sync::Arc};
use cosmic_text::Buffer; use cosmic_text::Buffer;
use glam::{Mat4, Vec2, Vec3}; use glam::{Mat4, Vec2, Vec3};
use slotmap::{SlotMap, new_key_type}; use slotmap::{new_key_type, SlotMap};
use vulkano::pipeline::graphics::viewport; use vulkano::pipeline::graphics::viewport;
use crate::{ use crate::{
drawing::{self}, drawing::{self},
font_config, font_config,
gfx::{WGfx, cmd::GfxCommandBuffer}, gfx::{cmd::GfxCommandBuffer, WGfx},
renderer_vk::image::{ImagePipeline, ImageRenderer},
}; };
use super::{ use super::{
rect::{RectPipeline, RectRenderer}, rect::{RectPipeline, RectRenderer},
text::{ text::{
DEFAULT_METRICS, SWASH_CACHE, TextArea, TextBounds,
text_atlas::{TextAtlas, TextPipeline}, text_atlas::{TextAtlas, TextPipeline},
text_renderer::TextRenderer, text_renderer::TextRenderer,
TextArea, TextBounds, DEFAULT_METRICS, SWASH_CACHE,
}, },
viewport::Viewport, viewport::Viewport,
}; };
@@ -26,6 +27,7 @@ struct RendererPass<'a> {
text_areas: Vec<TextArea<'a>>, text_areas: Vec<TextArea<'a>>,
text_renderer: TextRenderer, text_renderer: TextRenderer,
rect_renderer: RectRenderer, rect_renderer: RectRenderer,
image_renderer: ImageRenderer,
scissor: Option<drawing::Boundary>, scissor: Option<drawing::Boundary>,
pixel_scale: f32, pixel_scale: f32,
} }
@@ -34,16 +36,19 @@ impl RendererPass<'_> {
fn new( fn new(
text_atlas: &mut TextAtlas, text_atlas: &mut TextAtlas,
rect_pipeline: RectPipeline, rect_pipeline: RectPipeline,
image_pipeline: ImagePipeline,
scissor: Option<drawing::Boundary>, scissor: Option<drawing::Boundary>,
pixel_scale: f32, pixel_scale: f32,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
let text_renderer = TextRenderer::new(text_atlas)?; let text_renderer = TextRenderer::new(text_atlas)?;
let rect_renderer = RectRenderer::new(rect_pipeline)?; let rect_renderer = RectRenderer::new(rect_pipeline)?;
let image_renderer = ImageRenderer::new(image_pipeline)?;
Ok(Self { Ok(Self {
submitted: false, submitted: false,
text_renderer, text_renderer,
rect_renderer, rect_renderer,
image_renderer,
text_areas: Vec::new(), text_areas: Vec::new(),
scissor, scissor,
pixel_scale, pixel_scale,
@@ -90,6 +95,7 @@ impl RendererPass<'_> {
self.submitted = true; self.submitted = true;
self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?; 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(); let mut font_system = font_system.system.lock();
@@ -119,18 +125,21 @@ pub struct SharedContext {
atlas_map: SlotMap<SharedContextKey, SharedAtlas>, atlas_map: SlotMap<SharedContextKey, SharedAtlas>,
rect_pipeline: RectPipeline, rect_pipeline: RectPipeline,
text_pipeline: TextPipeline, text_pipeline: TextPipeline,
image_pipeline: ImagePipeline,
} }
impl SharedContext { impl SharedContext {
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> { pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
let rect_pipeline = RectPipeline::new(gfx.clone(), gfx.surface_format)?; let rect_pipeline = RectPipeline::new(gfx.clone(), gfx.surface_format)?;
let text_pipeline = TextPipeline::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 { Ok(Self {
gfx, gfx,
atlas_map: SlotMap::with_key(), atlas_map: SlotMap::with_key(),
rect_pipeline, rect_pipeline,
text_pipeline, text_pipeline,
image_pipeline,
}) })
} }
@@ -237,6 +246,7 @@ impl Context {
passes.push(RendererPass::new( passes.push(RendererPass::new(
&mut atlas.text_atlas, &mut atlas.text_atlas,
shared.rect_pipeline.clone(), shared.rect_pipeline.clone(),
shared.image_pipeline.clone(),
next_scissor, next_scissor,
self.pixel_scale, self.pixel_scale,
)?); )?);
@@ -293,6 +303,11 @@ impl Context {
transform: extent.transform, transform: extent.transform,
}); });
} }
drawing::RenderPrimitive::Image(extent, image) => {
pass
.image_renderer
.add_image(extent.boundary, image.clone(), &extent.transform);
}
drawing::RenderPrimitive::ScissorSet(boundary) => { drawing::RenderPrimitive::ScissorSet(boundary) => {
next_scissor = Some(boundary.0); next_scissor = Some(boundary.0);
needs_new_pass = true; needs_new_pass = true;

View File

@@ -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<WGfx>,
pub(super) inner: Arc<WGfxPipeline<ImageVertex>>,
}
impl ImagePipeline {
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
let vert = vert_image::load(gfx.device.clone())?;
let frag = frag_image::load(gfx.device.clone())?;
let pipeline = gfx.create_pipeline::<ImageVertex>(
&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<ImageVertex>,
res: [u32; 2],
}
pub struct ImageRenderer {
pipeline: ImagePipeline,
image_verts: Vec<ImageVertexWithContent>,
model_buffer: ModelBuffer,
cached_passes: HashMap<usize, CachedPass>,
}
impl ImageRenderer {
pub fn new(pipeline: ImagePipeline) -> anyhow::Result<Self> {
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<WGfx>,
res: [u32; 2],
img: &ImageVertexWithContent,
) -> anyhow::Result<Option<Arc<ImageView>>> {
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<WGfx>,
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::<ImageVertex>()) 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",
}
}

View File

@@ -1,4 +1,5 @@
pub mod context; pub mod context;
pub mod image;
pub mod model_buffer; pub mod model_buffer;
pub mod rect; pub mod rect;
pub mod text; pub mod text;

View File

@@ -8,7 +8,7 @@ use vulkano::{
use crate::{ use crate::{
gfx, gfx,
renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline}, renderer_vk::{image::ImagePipeline, rect::RectPipeline, text::text_atlas::TextPipeline},
}; };
pub struct ModelBuffer { pub struct ModelBuffer {
@@ -19,6 +19,8 @@ pub struct ModelBuffer {
buffer_capacity_f32: u32, buffer_capacity_f32: u32,
rect_descriptor: Option<Arc<DescriptorSet>>, rect_descriptor: Option<Arc<DescriptorSet>>,
text_descriptor: Option<Arc<DescriptorSet>>,
image_descriptor: Option<Arc<DescriptorSet>>,
} }
impl ModelBuffer { impl ModelBuffer {
@@ -40,6 +42,8 @@ impl ModelBuffer {
buffer, buffer,
buffer_capacity_f32: INITIAL_CAPACITY_F32, buffer_capacity_f32: INITIAL_CAPACITY_F32,
rect_descriptor: None, 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<DescriptorSet> { pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
self self
.rect_descriptor .text_descriptor
.get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap()) .get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap())
.clone() .clone()
} }
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
self
.image_descriptor
.get_or_insert_with(|| pipeline.inner.buffer(1, self.buffer.clone()).unwrap())
.clone()
}
} }

View File

@@ -10,10 +10,10 @@ use vulkano::{
use crate::{ use crate::{
drawing::{Boundary, Rectangle}, drawing::{Boundary, Rectangle},
gfx::{ gfx::{
BLEND_ALPHA, WGfx,
cmd::GfxCommandBuffer, cmd::GfxCommandBuffer,
pass::WGfxPass, pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo}, pipeline::{WGfxPipeline, WPipelineCreateInfo},
WGfx, BLEND_ALPHA,
}, },
renderer_vk::model_buffer::ModelBuffer, renderer_vk::model_buffer::ModelBuffer,
}; };

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -53,8 +53,8 @@ impl CustomGlyphContent {
/// Clone and reuse this to avoid atlasing the same content twice. /// Clone and reuse this to avoid atlasing the same content twice.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CustomGlyphData { pub struct CustomGlyphData {
pub(super) id: usize, pub(crate) id: usize,
pub(super) content: Arc<CustomGlyphContent>, pub(crate) content: Arc<CustomGlyphContent>,
} }
impl CustomGlyphData { impl CustomGlyphData {
@@ -157,7 +157,7 @@ pub struct RasterizedCustomGlyph {
} }
impl RasterizedCustomGlyph { impl RasterizedCustomGlyph {
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> { pub(crate) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> {
match input.data.content.as_ref() { match input.data.content.as_ref() {
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input), CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
CustomGlyphContent::Image(data) => Some(rasterize_image(data)), CustomGlyphContent::Image(data) => Some(rasterize_image(data)),

View File

@@ -5,7 +5,10 @@ use vulkano::{
descriptor_set::DescriptorSet, 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}; use super::{rect::RectPipeline, text::text_atlas::TextPipeline};
@@ -16,6 +19,7 @@ pub struct Viewport {
params_buffer: Subbuffer<[Params]>, params_buffer: Subbuffer<[Params]>,
text_descriptor: Option<Arc<DescriptorSet>>, text_descriptor: Option<Arc<DescriptorSet>>,
rect_descriptor: Option<Arc<DescriptorSet>>, rect_descriptor: Option<Arc<DescriptorSet>>,
image_descriptor: Option<Arc<DescriptorSet>>,
} }
impl Viewport { impl Viewport {
@@ -36,6 +40,7 @@ impl Viewport {
params_buffer, params_buffer,
text_descriptor: None, text_descriptor: None,
rect_descriptor: None, rect_descriptor: None,
image_descriptor: None,
}) })
} }
@@ -57,6 +62,15 @@ impl Viewport {
.clone() .clone()
} }
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
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`. /// 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<()> { pub fn update(&mut self, resolution: [u32; 2], projection: &glam::Mat4, pixel_scale: f32) -> anyhow::Result<()> {
if self.params.screen_resolution == resolution if self.params.screen_resolution == resolution

113
wgui/src/widget/image.rs Normal file
View File

@@ -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<CustomGlyphData>,
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<CustomGlyphData>) {
if self.params.glyph_data == content {
return;
}
self.params.glyph_data = content;
common.mark_widget_dirty(self.id);
}
pub fn get_content(&self) -> Option<CustomGlyphData> {
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<Option<f32>>,
_available_space: taffy::Size<taffy::AvailableSpace>,
) -> taffy::Size<f32> {
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()
}
}

View File

@@ -17,6 +17,7 @@ use crate::{
}; };
pub mod div; pub mod div;
pub mod image;
pub mod label; pub mod label;
pub mod rectangle; pub mod rectangle;
pub mod sprite; pub mod sprite;

View File

@@ -50,6 +50,10 @@ impl WidgetSprite {
} }
pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) { pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) {
if self.params.glyph_data == content {
return;
}
self.params.glyph_data = content; self.params.glyph_data = content;
common.mark_widget_dirty(self.id); common.mark_widget_dirty(self.id);
} }

View File

@@ -9,7 +9,9 @@ use wgui::{
parser::{Fetchable, parse_color_hex}, parser::{Fetchable, parse_color_hex},
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
taffy, taffy,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite}, widget::{
image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite,
},
}; };
use wlx_common::windowing::OverlayWindowState; use wlx_common::windowing::OverlayWindowState;
@@ -111,23 +113,26 @@ fn apply_custom_command(
} }
} }
ModifyPanelCommand::SetSprite(path) => { ModifyPanelCommand::SetSprite(path) => {
let mut widget = panel if let Ok(pair) = panel
.parser_state .parser_state
.fetch_widget_as::<WidgetSprite>(&panel.layout.state, element) .fetch_widget(&panel.layout.state, element)
.context("No <sprite> with such id.")?; {
if path == "none" {
widget.set_content(&mut com, None);
} else {
let content = CustomGlyphContent::from_assets( let content = CustomGlyphContent::from_assets(
&mut app.wgui_globals, &mut 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); let data = CustomGlyphData::new(content);
widget.set_content(&mut com, Some(data)); if let Some(mut sprite) = pair.widget.get_as_mut::<WidgetSprite>() {
sprite.set_content(&mut com, Some(data));
} else if let Some(mut image) = pair.widget.get_as_mut::<WidgetImage>() {
image.set_content(&mut com, Some(data));
} else {
anyhow::bail!("No <sprite> or <image> with such id.");
}
} else {
anyhow::bail!("No <sprite> or <image> with such id.");
} }
} }
ModifyPanelCommand::SetColor(color) => { ModifyPanelCommand::SetColor(color) => {