added underdeveloped but working image support (no freeing or samplers)

This commit is contained in:
2025-08-22 23:07:31 -04:00
parent bde929b05a
commit 7dbdcbba42
26 changed files with 1256 additions and 155 deletions

View File

@@ -8,7 +8,7 @@ pub struct Regioned {
impl<Ctx: 'static> Widget<Ctx> for Regioned {
fn draw(&self, painter: &mut Painter<Ctx>) {
painter.region.select(&self.region);
painter.draw(&self.inner);
painter.draw_inner(&self.inner);
}
}

63
src/core/image.rs Normal file
View File

@@ -0,0 +1,63 @@
use image::DynamicImage;
use crate::{Color, TextureHandle, Widget, WidgetFnRet, render::RectPrimitive};
pub struct Image {
handle: Option<TextureHandle>,
}
impl<Ctx> Widget<Ctx> for Image {
fn draw(&self, painter: &mut crate::Painter<Ctx>) {
if let Some(handle) = &self.handle {
painter.draw_texture(handle);
} else {
painter.write(RectPrimitive {
color: Color::MAGENTA,
inner_radius: 0.0,
radius: 0.0,
thickness: 0.0,
});
}
}
}
pub fn image<Ctx>(image: impl LoadableImage) -> WidgetFnRet!(Image, Ctx) {
let image = match image.get_image() {
Ok(image) => Some(image),
Err(e) => {
println!("Failed to load image: {e}");
None
}
};
move |ui| Image {
handle: image.map(|image| ui.add_texture(image)),
}
}
pub trait LoadableImage {
fn get_image(self) -> Result<DynamicImage, String>;
}
impl LoadableImage for &str {
fn get_image(self) -> Result<DynamicImage, String> {
image::open(self).map_err(|e| format!("{e:?}"))
}
}
impl LoadableImage for String {
fn get_image(self) -> Result<DynamicImage, String> {
image::open(self).map_err(|e| format!("{e:?}"))
}
}
impl<const LEN: usize> LoadableImage for &[u8; LEN] {
fn get_image(self) -> Result<DynamicImage, String> {
image::load_from_memory(self).map_err(|e| format!("{e:?}"))
}
}
impl LoadableImage for DynamicImage {
fn get_image(self) -> Result<DynamicImage, String> {
Ok(self)
}
}

View File

@@ -1,4 +1,5 @@
mod frame;
mod image;
mod num;
mod rect;
mod sense;
@@ -7,6 +8,7 @@ mod stack;
mod trait_fns;
pub use frame::*;
pub use image::*;
pub use num::*;
pub use rect::*;
pub use sense::*;

View File

@@ -1,4 +1,4 @@
use crate::{primitive::RoundedRectData, Painter, UiNum, UiColor, Widget};
use crate::{Painter, UiColor, UiNum, Widget, render::RectPrimitive};
#[derive(Clone, Copy)]
pub struct Rect {
@@ -29,7 +29,7 @@ impl Rect {
impl<Ctx> Widget<Ctx> for Rect {
fn draw(&self, painter: &mut Painter<Ctx>) {
painter.write(RoundedRectData {
painter.write(RectPrimitive {
color: self.color,
radius: self.radius,
thickness: self.thickness,

View File

@@ -1,4 +1,4 @@
use crate::{Dir, Painter, Sign, UiNum, UiRegion, UIScalar, Widget, WidgetId};
use crate::{Dir, Painter, Sign, UIScalar, UiNum, UiRegion, Widget, WidgetId};
pub struct Span {
pub children: Vec<(WidgetId, SpanLen)>,

View File

@@ -7,7 +7,7 @@ pub struct Stack {
impl<Ctx> Widget<Ctx> for Stack {
fn draw(&self, painter: &mut crate::Painter<Ctx>) {
for child in &self.children {
painter.draw_within(child, painter.region);
painter.draw(child);
}
}
}

View File

@@ -1,19 +1,21 @@
mod color;
mod id;
mod painter;
mod region;
mod sense;
mod texture;
mod ui;
mod vec2;
mod widget;
mod id;
pub use color::*;
pub use id::*;
pub use painter::*;
pub use region::*;
pub use sense::*;
pub use texture::*;
pub use ui::*;
pub use vec2::*;
pub use widget::*;
pub use id::*;
pub type UiColor = Color<u8>;

View File

@@ -1,6 +1,6 @@
use crate::{
ActiveSensors, SensorMap, UiRegion, WidgetId, Widgets,
primitive::{Primitive, Primitives},
ActiveSensors, SensorMap, TextureHandle, UiRegion, WidgetId, Widgets,
render::{Primitive, Primitives},
};
pub struct Painter<'a, Ctx: 'static> {
@@ -32,7 +32,9 @@ impl<'a, Ctx> Painter<'a, Ctx> {
self.primitives.write(data, self.region);
}
pub fn draw<W>(&mut self, id: &WidgetId<W>)
/// Draws a widget but does NOT maintain the region.
/// Useful if you only have a single widget to draw.
pub fn draw_inner<W>(&mut self, id: &WidgetId<W>)
where
Ctx: 'static,
{
@@ -42,16 +44,34 @@ impl<'a, Ctx> Painter<'a, Ctx> {
self.nodes.get_dyn(id).draw(self);
}
pub fn draw_within(&mut self, node: &WidgetId, region: UiRegion)
/// Draws a widget and maintains the original region.
/// Useful if you need to draw multiple overlapping child widgets.
pub fn draw<W>(&mut self, id: &WidgetId<W>)
where
Ctx: 'static,
{
let old = self.region;
self.draw_inner(id);
self.region = old;
}
/// Draws a widget within an inner region and maintains the original region.
/// Useful if you need to draw multiple child widgets in select areas within the original
/// region.
pub fn draw_within(&mut self, id: &WidgetId, region: UiRegion)
where
Ctx: 'static,
{
let old = self.region;
self.region.select(&region);
self.draw(node);
self.draw_inner(id);
self.region = old;
}
pub fn draw_texture(&mut self, handle: &TextureHandle) {
self.write(handle.inner);
}
pub fn finish(self) -> Primitives {
self.primitives
}

48
src/layout/texture.rs Normal file
View File

@@ -0,0 +1,48 @@
use image::DynamicImage;
use crate::render::TexturePrimitive;
/// TODO: proper resource management
pub struct TextureHandle {
pub inner: TexturePrimitive,
}
/// a texture manager for a ui
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
#[derive(Default)]
pub struct Textures {
/// TODO: these are images, not views rn
views: Vec<DynamicImage>,
changed: bool,
}
pub struct TextureUpdates<'a> {
pub images: Option<&'a [DynamicImage]>,
}
impl Textures {
pub fn add(&mut self, image: DynamicImage) -> TextureHandle {
let view_idx = self.views.len() as u32;
self.views.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
self.changed = true;
TextureHandle {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
}
}
pub fn updates(&mut self) -> TextureUpdates<'_> {
if self.changed {
self.changed = false;
TextureUpdates {
images: Some(&self.views),
}
} else {
TextureUpdates { images: None }
}
}
}

View File

@@ -1,7 +1,10 @@
use image::DynamicImage;
use crate::{
ActiveSensors, HashMap, Painter, SensorMap, Widget, WidgetId, WidgetLike,
primitive::Primitives,
util::{IDTracker, Id},
ActiveSensors, HashMap, Painter, SensorMap, TextureHandle, TextureUpdates, Textures, Widget,
WidgetId, WidgetLike,
render::Primitives,
util::{Id, IdTracker},
};
use std::{
any::{Any, TypeId},
@@ -10,20 +13,24 @@ use std::{
};
pub struct Ui<Ctx> {
ids: IDTracker,
base: Option<WidgetId>,
widgets: Widgets<Ctx>,
updates: Vec<WidgetId>,
del_recv: Receiver<Id>,
del_send: Sender<Id>,
primitives: Primitives,
textures: Textures,
full_redraw: bool,
pub(super) active_sensors: ActiveSensors,
pub(super) sensor_map: SensorMap<Ctx>,
primitives: Primitives,
full_redraw: bool,
}
#[derive(Default)]
pub struct Widgets<Ctx>(HashMap<Id, Box<dyn Widget<Ctx>>>);
pub struct Widgets<Ctx> {
ids: IdTracker,
map: HashMap<Id, Box<dyn Widget<Ctx>>>,
}
impl<Ctx> Ui<Ctx> {
pub fn add<W: Widget<Ctx>, Tag>(
@@ -68,7 +75,15 @@ impl<Ctx> Ui<Ctx> {
}
pub fn id<W: Widget<Ctx>>(&mut self) -> WidgetId<W> {
WidgetId::new(self.ids.next(), TypeId::of::<W>(), self.del_send.clone())
WidgetId::new(
self.widgets.reserve(),
TypeId::of::<W>(),
self.del_send.clone(),
)
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.textures.add(image)
}
pub fn redraw_all(&mut self, ctx: &mut Ctx)
@@ -83,12 +98,12 @@ impl<Ctx> Ui<Ctx> {
&mut self.active_sensors,
);
if let Some(base) = &self.base {
painter.draw(base);
painter.draw_inner(base);
}
self.primitives = painter.finish();
}
pub fn update(&mut self, ctx: &mut Ctx) -> Option<&Primitives>
pub fn update(&mut self, ctx: &mut Ctx) -> UiRenderUpdates
where
Ctx: 'static,
{
@@ -99,14 +114,24 @@ impl<Ctx> Ui<Ctx> {
if self.full_redraw {
self.redraw_all(ctx);
self.full_redraw = false;
return Some(&self.primitives);
UiRenderUpdates {
primitives: Some(&self.primitives),
textures: self.textures.updates(),
}
} else if self.updates.is_empty() {
UiRenderUpdates {
primitives: None,
textures: self.textures.updates(),
}
} else {
// TODO: partial updates
self.redraw_all(ctx);
self.updates.drain(..);
UiRenderUpdates {
primitives: Some(&self.primitives),
textures: self.textures.updates(),
}
}
if self.updates.is_empty() {
return None;
}
self.redraw_all(ctx);
self.updates.drain(..);
Some(&self.primitives)
}
pub fn needs_redraw(&self) -> bool {
@@ -135,39 +160,51 @@ impl<W: Widget<Ctx>, Ctx> IndexMut<&WidgetId<W>> for Ui<Ctx> {
impl<Ctx> Widgets<Ctx> {
pub fn new() -> Self {
Self(HashMap::new())
Self {
ids: IdTracker::default(),
map: HashMap::new(),
}
}
pub fn get_dyn<W>(&self, id: &WidgetId<W>) -> &dyn Widget<Ctx> {
self.0.get(&id.id).unwrap().as_ref()
self.map.get(&id.id).unwrap().as_ref()
}
pub fn get<W: Widget<Ctx>>(&self, id: &WidgetId<W>) -> Option<&W> {
self.0.get(&id.id).unwrap().as_any().downcast_ref()
self.map.get(&id.id).unwrap().as_any().downcast_ref()
}
pub fn get_mut<W: Widget<Ctx>>(&mut self, id: &WidgetId<W>) -> Option<&mut W> {
self.0.get_mut(&id.id).unwrap().as_any_mut().downcast_mut()
self.map
.get_mut(&id.id)
.unwrap()
.as_any_mut()
.downcast_mut()
}
pub fn insert(&mut self, id: Id, widget: impl Widget<Ctx>) {
self.0.insert(id, Box::new(widget));
self.map.insert(id, Box::new(widget));
}
pub fn insert_any(&mut self, id: Id, widget: Box<dyn Widget<Ctx>>) {
self.0.insert(id, widget);
self.map.insert(id, widget);
}
pub fn delete(&mut self, id: Id) {
self.0.remove(&id);
self.map.remove(&id);
self.ids.free(id);
}
pub fn reserve(&mut self) -> Id {
self.ids.next()
}
pub fn len(&self) -> usize {
self.0.len()
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
self.map.is_empty()
}
}
@@ -185,11 +222,11 @@ impl<Ctx: 'static> Default for Ui<Ctx> {
fn default() -> Self {
let (del_send, del_recv) = channel();
Self {
ids: Default::default(),
base: Default::default(),
widgets: Widgets::new(),
updates: Default::default(),
primitives: Default::default(),
textures: Textures::default(),
full_redraw: false,
active_sensors: Default::default(),
sensor_map: Default::default(),
@@ -198,3 +235,8 @@ impl<Ctx: 'static> Default for Ui<Ctx> {
}
}
}
pub struct UiRenderUpdates<'a> {
pub primitives: Option<&'a Primitives>,
pub textures: TextureUpdates<'a>,
}

View File

@@ -2,7 +2,7 @@ use crate::{util::{impl_op, F32Util}, UiNum};
use std::ops::*;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
@@ -60,3 +60,9 @@ impl<T: UiNum, U: UiNum> From<(T, U)> for Vec2 {
}
}
}
impl std::fmt::Debug for Vec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}

View File

@@ -41,8 +41,7 @@ pub(crate) use WidgetFnRet;
impl<W: Widget<Ctx>, Ctx, F: FnOnce(&mut Ui<Ctx>) -> W> WidgetLike<Ctx, FnTag> for F {
type Widget = W;
fn add(self, ui: &mut Ui<Ctx>) -> WidgetId<W> {
let w = self(ui);
ui.add(w)
self(ui).add(ui)
}
}

View File

@@ -5,14 +5,13 @@
#![feature(trait_alias)]
#![feature(negative_impls)]
mod layout;
mod render;
mod util;
mod core;
mod layout;
pub mod render;
mod util;
pub use layout::*;
pub use render::*;
pub use core::*;
pub use layout::*;
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
pub type HashSet<K> = std::collections::HashSet<K>;

View File

@@ -1,6 +1,8 @@
use std::num::NonZero;
use crate::{
primitive::{PrimitiveBuffers, Primitives},
render::{data::PrimitiveInstance, util::ArrBuf},
Ui, UiRenderUpdates,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
};
use data::WindowUniform;
use wgpu::{
@@ -10,41 +12,55 @@ use wgpu::{
use winit::dpi::PhysicalSize;
mod data;
pub mod primitive;
mod primitive;
mod texture;
mod util;
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UIRenderNode {
layout0: BindGroupLayout,
group0: BindGroup,
pub struct UiRenderer {
uniform_layout: BindGroupLayout,
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
primitive_group: BindGroup,
rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
pipeline: RenderPipeline,
window_buffer: Buffer,
instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers,
limits: UiLimits,
textures: GpuTextures,
}
impl UIRenderNode {
impl UiRenderer {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
if self.instance.len() != 0 {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.group0, &[]);
pass.set_bind_group(1, &self.primitive_group, &[]);
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
pass.draw(0..4, 0..self.instance.len() as u32);
if self.instance.len() == 0 {
return;
}
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(1, &self.primitive_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
pass.draw(0..4, 0..self.instance.len() as u32);
}
pub fn update(&mut self, device: &Device, queue: &Queue, primitives: Option<&Primitives>) {
if let Some(primitives) = primitives {
pub fn update(&mut self, device: &Device, queue: &Queue, updates: UiRenderUpdates) {
if let Some(primitives) = updates.primitives {
self.instance.update(device, queue, &primitives.instances);
self.primitives.update(device, queue, &primitives.data);
self.primitive_group =
Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers())
}
if self.textures.apply(updates.textures) {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures)
}
}
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
@@ -55,7 +71,12 @@ impl UIRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
pub fn new(
device: &Device,
queue: &Queue,
config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
@@ -75,7 +96,7 @@ impl UIRenderNode {
);
let primitives = PrimitiveBuffers::new(device);
let layout0 = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
@@ -89,7 +110,7 @@ impl UIRenderNode {
label: Some("window"),
});
let group0 = Self::bind_group_0(device, &layout0, &window_buffer);
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
@@ -110,9 +131,13 @@ impl UIRenderNode {
let primitive_group =
Self::primitive_group(device, &primitive_layout, primitives.buffers());
let tex_manager = GpuTextures::new(device, queue);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&layout0, &primitive_layout],
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
@@ -154,18 +179,22 @@ impl UIRenderNode {
});
Self {
layout0,
group0,
uniform_layout,
uniform_group,
primitive_layout,
primitive_group,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
instance,
primitives,
limits,
textures: tex_manager,
}
}
pub fn bind_group_0(
fn bind_group_0(
device: &Device,
layout: &BindGroupLayout,
window_buffer: &Buffer,
@@ -180,18 +209,86 @@ impl UIRenderNode {
})
}
pub fn primitive_group(
fn primitive_group(
device: &Device,
layout: &BindGroupLayout,
buffers: [&Buffer; PrimitiveBuffers::LEN],
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.each_ref().map(|b| BindGroupEntry {
binding: 0,
resource: b.as_entire_binding(),
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
})
}
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: Some(NonZero::new(limits.max_textures).unwrap()),
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
],
label: Some("ui rsc"),
})
}
fn rsc_group(
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(&tex_manager.views()),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
],
label: Some("ui rsc"),
})
}
}
pub struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
self.max_samplers
}
}

View File

@@ -35,9 +35,9 @@ macro_rules! primitives {
impl PrimitiveBuffers {
pub const LEN: usize = primitives!(@count $($name)*);
pub fn buffers(&self) -> [&Buffer; Self::LEN] {
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
[
$(&self.$name.buffer)*
$((<$ty>::BINDING, &self.$name.buffer),)*
]
}
pub fn new(device: &Device) -> Self {
@@ -46,7 +46,7 @@ macro_rules! primitives {
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
))*
),)*
}
}
}
@@ -62,14 +62,10 @@ macro_rules! primitives {
}
)*
};
(@count $t1:tt, $($t:tt),+) => { 1 + gen!(@count $($t),+) };
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t:tt) => { 1 };
}
primitives!(
rects: RoundedRectData => 0,
);
impl Primitives {
pub fn write<P: Primitive>(&mut self, data: P, region: UiRegion) {
let vec = P::vec(&mut self.data);
@@ -83,11 +79,23 @@ impl Primitives {
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
);
#[repr(C)]
#[derive(Copy, Clone)]
pub struct RoundedRectData {
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
pub thickness: f32,
pub inner_radius: f32,
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}

View File

@@ -1,9 +1,29 @@
const RECT: u32 = 0;
const TEXTURE: u32 = 1;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(1) @binding(RECT)
var<storage> rects: array<RoundedRect>;
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
struct WindowUniform {
dim: vec2<f32>,
@@ -18,23 +38,18 @@ struct InstanceInput {
@location(5) idx: u32,
}
struct RoundedRect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct VertexOutput {
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) binding: u32,
@location(3) idx: u32,
@location(2) uv: vec2<f32>,
@location(3) binding: u32,
@location(4) idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
uv: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@@ -50,12 +65,13 @@ fn vs_main(
let bot_right = in.bottom_right_anchor * window.dim + in.bottom_right_offset;
let size = bot_right - top_left;
var pos = top_left + vec2<f32>(
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
) * size;
pos = pos / window.dim * 2.0 - 1.0;
);
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
out.uv = uv;
out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left;
@@ -69,19 +85,27 @@ fn fs_main(
in: VertexOutput
) -> @location(0) vec4<f32> {
let pos = in.clip_position.xy;
let region = Region(pos, in.top_left, in.bot_right);
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
switch in.binding {
case RECT: {
return draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
return draw_texture(region, textures[i]);
}
default: {
return vec4(1.0, 0.0, 1.0, 1.0);
}
}
}
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = read_color(rect.color);
let edge = 0.5;
@@ -106,7 +130,7 @@ fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner:
let p = pixel_pos - rect_center;
// vec from inner rect corner to pixel
let q = abs(p) - (rect_corner - radius);
return length(max(q, vec2<f32>(0.0, 0.0))) - radius;
return length(max(q, vec2(0.0))) - radius;
}
fn read_color(c: u32) -> vec4<f32> {

98
src/render/texture.rs Normal file
View File

@@ -0,0 +1,98 @@
use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *};
use crate::TextureUpdates;
pub struct GpuTextures {
device: Device,
queue: Queue,
views: Vec<TextureView>,
samplers: Vec<Sampler>,
no_views: Vec<TextureView>,
}
impl GpuTextures {
pub fn apply(&mut self, updates: TextureUpdates) -> bool {
if let Some(images) = updates.images {
for img in images {
self.add_view(img);
}
true
} else {
false
}
}
fn add_view(&mut self, image: &DynamicImage) {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: None,
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
);
let view = texture.create_view(&TextureViewDescriptor::default());
self.views.push(view);
}
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
device: device.clone(),
queue: queue.clone(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_texture_view(device)],
}
}
pub fn views(&self) -> Vec<&TextureView> {
if self.views.is_empty() {
&self.no_views
} else {
&self.views
}
.iter()
.by_ref()
.collect()
}
pub fn samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
}
pub fn null_texture_view(device: &Device) -> TextureView {
device
.create_texture(&TextureDescriptor {
label: Some("null"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
}
pub fn default_sampler(device: &Device) -> Sampler {
device.create_sampler(&SamplerDescriptor::default())
}

View File

@@ -32,7 +32,7 @@ impl<T: Pod> ArrBuf<T> {
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
size = size.max(1);
size = size.max(std::mem::size_of::<T>() as u64);
}
device.create_buffer(&BufferDescriptor {
label: Some(label),

View File

@@ -1,4 +1,4 @@
use gui::Ui;
use ui::Ui;
use winit::{
application::ApplicationHandler,
event::WindowEvent,

BIN
src/testing/assets/sungals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,4 +1,4 @@
use gui::{CursorState, Vec2};
use ui::{CursorState, Vec2};
use winit::event::WindowEvent;
use crate::testing::Client;

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use app::App;
use gui::*;
use render::Renderer;
use ui::*;
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
use crate::testing::input::Input;
@@ -76,6 +76,8 @@ impl Client {
let span_add_test = ui.add(Span::empty(Dir::RIGHT).id(&span_add));
let main: WidgetId<Regioned> = ui.id();
let image_test = ui.add(image(include_bytes!("assets/sungals.png")));
fn switch_button<To>(
color: UiColor,
main: &WidgetId<Regioned>,
@@ -104,8 +106,9 @@ impl Client {
switch_button(UiColor::RED, &main, &pad_test),
switch_button(UiColor::GREEN, &main, &span_test),
switch_button(UiColor::BLUE, &main, &span_add_test),
switch_button(UiColor::MAGENTA, &main, &image_test),
)
.span(Dir::RIGHT, [1, 1, 1]),
.span(Dir::RIGHT, [1, 1, 1, 1]),
);
let test_button = Rect::new(Color::PURPLE)
.radius(30)
@@ -156,8 +159,8 @@ impl Client {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
let primitives = ui.update(self);
self.renderer.update(primitives);
let updates = ui.update(self);
self.renderer.update(updates);
self.renderer.draw()
}
WindowEvent::Resized(size) => self.renderer.resize(&size),

View File

@@ -1,48 +1,51 @@
use gui::{UIRenderNode, primitive::Primitives};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::util::StagingBelt;
use ui::{
UiRenderUpdates,
render::{UiLimits, UiRenderer},
};
use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
pub const CLEAR_COLOR: Color = Color::BLACK;
pub struct Renderer {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
encoder: wgpu::CommandEncoder,
surface: Surface<'static>,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
ui_node: UIRenderNode,
ui: UiRenderer,
}
impl Renderer {
pub fn update(&mut self, primitives: Option<&Primitives>) {
self.ui_node.update(&self.device, &self.queue, primitives);
pub fn update(&mut self, updates: UiRenderUpdates) {
self.ui.update(&self.device, &self.queue, updates);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
.create_view(&TextureViewDescriptor::default());
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
{
let render_pass = &mut encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
store: wgpu::StoreOp::Store,
ops: Operations {
load: LoadOp::Clear(CLEAR_COLOR),
store: StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
self.ui_node.draw(render_pass);
self.ui.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
@@ -55,11 +58,11 @@ impl Renderer {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui_node.resize(size, &self.queue);
self.ui.resize(size, &self.queue);
}
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
fn create_encoder(device: &Device) -> CommandEncoder {
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
@@ -67,8 +70,8 @@ impl Renderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
});
@@ -77,18 +80,28 @@ impl Renderer {
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
..Default::default()
},
..Default::default()
})
.block_on()
@@ -102,12 +115,12 @@ impl Renderer {
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
present_mode: PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
@@ -118,7 +131,7 @@ impl Renderer {
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UIRenderNode::new(&device, &config);
let shape_pipeline = UiRenderer::new(&device, &queue, &config, ui_limits);
Self {
surface,
@@ -127,7 +140,7 @@ impl Renderer {
config,
encoder,
staging_belt,
ui_node: shape_pipeline,
ui: shape_pipeline,
window,
}
}

View File

@@ -7,12 +7,12 @@
pub struct Id(u64);
#[derive(Default)]
pub struct IDTracker {
pub struct IdTracker {
free: Vec<Id>,
cur: u64,
}
impl IDTracker {
impl IdTracker {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Id {
if let Some(id) = self.free.pop() {