initial mask impl

This commit is contained in:
2025-11-10 14:45:22 -05:00
parent 5c2022396a
commit 1c49db1b89
15 changed files with 398 additions and 74 deletions

12
src/core/mask.rs Normal file
View File

@@ -0,0 +1,12 @@
use crate::prelude::*;
pub struct Masked {
pub inner: WidgetId,
}
impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) {
painter.set_mask(painter.region());
painter.widget(&self.inner);
}
}

View File

@@ -1,4 +1,5 @@
mod image;
mod mask;
mod position;
mod rect;
mod sense;
@@ -6,6 +7,7 @@ mod text;
mod trait_fns;
pub use image::*;
pub use mask::*;
pub use position::*;
pub use rect::*;
pub use sense::*;

View File

@@ -9,6 +9,7 @@ pub trait CoreWidget<W, Tag> {
fn sized(self, size: impl Into<Vec2>) -> impl WidgetFn<Sized>;
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset>;
fn scroll(self) -> impl WidgetIdFn<Offset>;
fn masked(self) -> impl WidgetFn<Masked>;
}
impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
@@ -53,9 +54,16 @@ impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
}
fn scroll(self) -> impl WidgetIdFn<Offset> {
self.offset(UiVec2::ZERO).edit_on(CursorSense::Scroll, |w, data| {
w.amt += UiVec2::abs(data.scroll_delta * 50.0);
})
self.offset(UiVec2::ZERO)
.edit_on(CursorSense::Scroll, |w, data| {
w.amt += UiVec2::abs(data.scroll_delta * 50.0);
})
}
fn masked(self) -> impl WidgetFn<Masked> {
move |ui| Masked {
inner: self.add(ui).any(),
}
}
}

View File

@@ -1,10 +1,6 @@
use std::ops::{Index, IndexMut};
use crate::{
layout::UiRegion,
render::{Primitive, PrimitiveHandle, Primitives},
util::Id,
};
use crate::render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives};
struct LayerNode {
next: Ptr,
@@ -109,17 +105,11 @@ impl Layers {
LayerIndexIterator::new(&self.vec, self.last)
}
pub fn write<P: Primitive>(
&mut self,
layer: usize,
id: Id,
primitive: P,
region: UiRegion,
) -> PrimitiveHandle {
self[layer].primitives.write(layer, id, primitive, region)
pub fn write<P: Primitive>(&mut self, layer: usize, info: PrimitiveInst<P>) -> PrimitiveHandle {
self[layer].primitives.write(layer, info)
}
pub fn free(&mut self, h: &PrimitiveHandle) {
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self[h.layer].primitives.free(h)
}
}

46
src/layout/mask.rs Normal file
View File

@@ -0,0 +1,46 @@
//! tree structure for masking
use crate::layout::UiRegion;
pub struct Masks {
data: Vec<MaskNode>,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct MaskPtr(u32);
#[repr(C)]
pub struct MaskNode {
/// TODO: this is just a rect for now,
/// but would like to support arbitrary masks
/// at some point; custom shader
/// would probably handle that case
/// bc you'd need to render to a special target
/// anyways
region: UiRegion,
prev: MaskPtr,
}
impl MaskPtr {
const NONE: Self = Self(u32::MAX);
}
impl Masks {
pub fn push(&mut self, parent: MaskPtr, region: UiRegion) -> MaskPtr {
match parent.0 {
_ => {
}
u32::MAX => {
let i = self.data.len();
self.data.push(MaskNode {
region,
prev: parent,
});
MaskPtr(i as u32)
}
}
}
pub fn pop(&mut self, i: usize) {}
}

View File

@@ -3,13 +3,14 @@ use crate::{
Layers, Modules, TextAttrs, TextBuffer, TextData, TextTexture, TextureHandle, Textures,
UiRegion, UiVec2, Vec2, WidgetId, Widgets,
},
render::{Primitive, PrimitiveHandle},
util::{HashMap, HashSet, Id},
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::{HashMap, HashSet, Id, TrackedArena},
};
pub struct Painter<'a, 'c> {
ctx: &'a mut PainterCtx<'c>,
region: UiRegion,
mask: MaskIdx,
textures: Vec<TextureHandle>,
primitives: Vec<PrimitiveHandle>,
children: Vec<Id>,
@@ -25,6 +26,7 @@ pub struct PainterCtx<'a> {
pub active: &'a mut HashMap<Id, WidgetInstance>,
pub layers: &'a mut Layers,
pub textures: &'a mut Textures,
pub masks: &'a mut TrackedArena<Mask, u32>,
pub text: &'a mut TextData,
pub screen_size: Vec2,
pub modules: &'a mut Modules,
@@ -40,9 +42,11 @@ pub struct WidgetInstance {
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<Id>,
pub resize: Option<(Id, UiVec2)>,
pub mask: MaskIdx,
pub layer: usize,
}
#[derive(Default)]
pub struct PainterData {
pub widgets: Widgets,
pub active: HashMap<Id, WidgetInstance>,
@@ -52,21 +56,7 @@ pub struct PainterData {
pub output_size: Vec2,
pub modules: Modules,
pub px_dependent: HashSet<Id>,
}
impl Default for PainterData {
fn default() -> Self {
Self {
widgets: Widgets::new(),
layers: Default::default(),
textures: Textures::new(),
text: TextData::default(),
active: Default::default(),
output_size: Vec2::ZERO,
modules: Modules::default(),
px_dependent: Default::default(),
}
}
pub masks: TrackedArena<Mask, u32>,
}
impl<'a> PainterCtx<'a> {
@@ -80,6 +70,7 @@ impl<'a> PainterCtx<'a> {
screen_size: data.output_size,
modules: &mut data.modules,
px_dependent: &mut data.px_dependent,
masks: &mut data.masks,
draw_started: HashSet::default(),
}
}
@@ -122,6 +113,7 @@ impl<'a> PainterCtx<'a> {
id,
active.region,
active.parent,
active.mask,
Some(active.children),
);
self.active.get_mut(&id).unwrap().resize = active.resize;
@@ -130,7 +122,7 @@ impl<'a> PainterCtx<'a> {
pub fn draw(&mut self, id: Id) {
self.draw_started.clear();
self.layers.clear();
self.draw_inner(0, id, UiRegion::full(), None, None);
self.draw_inner(0, id, UiRegion::full(), None, MaskIdx::NONE, None);
}
fn draw_inner(
@@ -139,6 +131,7 @@ impl<'a> PainterCtx<'a> {
id: Id,
region: UiRegion,
parent: Option<Id>,
mask: MaskIdx,
old_children: Option<Vec<Id>>,
) {
// I have no idea if these checks work lol
@@ -173,6 +166,7 @@ impl<'a> PainterCtx<'a> {
let mut painter = Painter {
region,
mask,
layer,
id,
textures: Vec::new(),
@@ -196,6 +190,7 @@ impl<'a> PainterCtx<'a> {
primitives: painter.primitives,
children: painter.children,
resize,
mask: painter.mask,
layer,
};
for (cid, size) in sized_children {
@@ -238,7 +233,10 @@ impl<'a> PainterCtx<'a> {
let mut inst = self.active.remove(&id);
if let Some(inst) = &mut inst {
for h in &inst.primitives {
self.layers.free(h);
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
self.masks.remove(mask);
}
}
inst.textures.clear();
self.textures.free();
@@ -263,10 +261,19 @@ impl<'a> PainterCtx<'a> {
impl<'a, 'c> Painter<'a, 'c> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self
.ctx
.layers
.write(self.layer, self.id, primitive, region);
let h = self.ctx.layers.write(
self.layer,
PrimitiveInst {
id: self.id,
primitive,
region,
mask_idx: self.mask,
},
);
if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.ctx.masks.push_ref(self.mask);
}
self.primitives.push(h);
}
@@ -279,6 +286,11 @@ impl<'a, 'c> Painter<'a, 'c> {
self.primitive_at(primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE);
self.mask = self.ctx.masks.push(Mask { region });
}
/// Draws a widget within this widget's region.
pub fn widget<W>(&mut self, id: &WidgetId<W>) {
self.widget_at(id, self.region);
@@ -293,7 +305,7 @@ impl<'a, 'c> Painter<'a, 'c> {
fn widget_at<W>(&mut self, id: &WidgetId<W>, region: UiRegion) {
self.children.push(id.id);
self.ctx
.draw_inner(self.layer, id.id, region, Some(self.id), None);
.draw_inner(self.layer, id.id, region, Some(self.id), self.mask, None);
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {

View File

@@ -1,6 +1,5 @@
use wgpu::VertexAttribute;
use crate::layout::UiRegion;
use crate::{layout::UiRegion, util::Id};
use wgpu::*;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
@@ -15,23 +14,37 @@ pub struct PrimitiveInstance {
pub region: UiRegion,
pub binding: u32,
pub idx: u32,
pub mask_idx: MaskIdx,
}
impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 6] = wgpu::vertex_attr_array![
const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
5 => Uint32,
6 => Uint32,
];
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
pub fn desc() -> VertexBufferLayout<'static> {
VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as BufferAddress,
step_mode: VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
pub type MaskIdx = Id<u32>;
impl MaskIdx {
pub const NONE: Self = Self::preset(u32::MAX);
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask {
pub region: UiRegion,
}

View File

@@ -17,6 +17,7 @@ mod primitive;
mod texture;
mod util;
pub use data::{Mask, MaskIdx};
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
@@ -33,6 +34,7 @@ pub struct UiRenderer {
active: Vec<usize>,
window_buffer: Buffer,
textures: GpuTextures,
masks: ArrBuf<Mask>,
}
struct RenderLayer {
@@ -99,7 +101,11 @@ impl UiRenderer {
}
}
if self.textures.update(&mut ui.data.textures) {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures)
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks)
}
if ui.data.masks.changed {
ui.data.masks.changed = false;
self.masks.update(device, queue, &ui.data.masks[..]);
}
}
@@ -132,7 +138,7 @@ impl UiRenderer {
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
@@ -162,8 +168,14 @@ impl UiRenderer {
});
let tex_manager = GpuTextures::new(device, queue);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
@@ -218,6 +230,7 @@ impl UiRenderer {
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
@@ -270,6 +283,16 @@ impl UiRenderer {
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("ui rsc"),
})
@@ -279,6 +302,7 @@ impl UiRenderer {
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
@@ -291,6 +315,10 @@ impl UiRenderer {
binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(),
},
],
label: Some("ui rsc"),
})

View File

@@ -2,7 +2,10 @@ use std::ops::{Deref, DerefMut};
use crate::{
layout::{Color, UiRegion},
render::{ArrBuf, data::PrimitiveInstance},
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
},
util::Id,
};
use bytemuck::Pod;
@@ -95,19 +98,30 @@ macro_rules! primitives {
(@count $t:tt) => { 1 };
}
pub struct PrimitiveInst<P> {
pub id: Id,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
}
impl Primitives {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
id: Id,
data: P,
region: UiRegion,
PrimitiveInst {
id,
primitive,
region,
mask_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
let vec = P::vec(&mut self.data);
let i = vec.add(data);
let i = vec.add(primitive);
let inst = PrimitiveInstance {
region,
idx: i as u32,
mask_idx,
binding: P::BINDING,
};
let inst_i = if let Some(i) = self.free.pop() {
@@ -138,9 +152,10 @@ impl Primitives {
})
}
pub fn free(&mut self, h: &PrimitiveHandle) {
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {

View File

@@ -20,10 +20,22 @@ struct TextureInfo {
sampler_idx: u32,
}
struct Mask {
top_left: UiVec2,
bot_right: UiVec2,
}
struct UiVec2 {
rel: vec2<f32>,
abs: vec2<f32>,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
@group(2) @binding(2)
var<storage> masks: array<Mask>;
struct WindowUniform {
dim: vec2<f32>,
@@ -36,6 +48,7 @@ struct InstanceInput {
@location(3) bottom_right_offset: vec2<f32>,
@location(4) binding: u32,
@location(5) idx: u32,
@location(6) mask_idx: u32,
}
struct VertexOutput {
@@ -44,6 +57,7 @@ struct VertexOutput {
@location(2) uv: vec2<f32>,
@location(3) binding: u32,
@location(4) idx: u32,
@location(5) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
@@ -76,6 +90,7 @@ fn vs_main(
out.idx = in.idx;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
return out;
}
@@ -87,17 +102,27 @@ fn fs_main(
let pos = in.clip_position.xy;
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
var color: vec4<f32>;
switch in.binding {
case RECT: {
return draw_rounded_rect(region, rects[i]);
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
return draw_texture(region, textures[i]);
color = draw_texture(region, textures[i]);
}
default: {
return vec4(1.0, 0.0, 1.0, 1.0);
color = vec4(1.0, 0.0, 1.0, 1.0);
}
}
if in.mask_idx != 4294967295u {
let mask = masks[in.mask_idx];
let top_left = floor(mask.top_left.rel * window.dim) + floor(mask.top_left.abs);
let bot_right = floor(mask.bot_right.rel * window.dim) + floor(mask.bot_right.abs);
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
color *= 0.0;
}
}
return color;
}
// TODO: this seems really inefficient (per frag indexing)?

View File

@@ -129,7 +129,7 @@ impl Client {
.add_static(&mut ui);
let texts = Span::empty(Dir::DOWN).add_static(&mut ui);
let msg_area = (Rect::new(Color::SKY), texts.scroll()).stack();
let msg_area = (Rect::new(Color::SKY), texts.scroll().masked()).stack();
let add_text = text("add")
.editable()
.text_align(Align::Left)

109
src/util/arena.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::ops::Deref;
use crate::util::{Id, IdNum, IdTracker};
pub struct Arena<T, I> {
data: Vec<T>,
tracker: IdTracker<I>,
}
impl<T, I: IdNum> Arena<T, I> {
pub fn new() -> Self {
Self {
data: Vec::new(),
tracker: IdTracker::default(),
}
}
pub fn push(&mut self, value: T) -> Id<I> {
let id = self.tracker.next();
let i = id.idx();
if i == self.data.len() {
self.data.push(value);
} else {
self.data[i] = value;
}
id
}
pub fn remove(&mut self, id: Id<I>) -> T
where
T: Copy,
{
let i = id.idx();
self.tracker.free(id);
self.data[i]
}
}
impl<T, I: IdNum> Default for Arena<T, I> {
fn default() -> Self {
Self::new()
}
}
pub struct TrackedArena<T, I> {
inner: Arena<T, I>,
refs: Vec<u32>,
pub changed: bool,
}
impl<T, I: IdNum> TrackedArena<T, I> {
pub fn new() -> Self {
Self {
inner: Arena::default(),
refs: Vec::new(),
changed: true,
}
}
pub fn push(&mut self, value: T) -> Id<I> {
self.changed = true;
let id = self.inner.push(value);
let i = id.idx();
if i == self.refs.len() {
self.refs.push(0);
}
id
}
pub fn push_ref(&mut self, i: Id<I>) {
self.refs[i.idx()] += 1;
}
pub fn remove(&mut self, id: Id<I>) -> T
where
T: Copy,
{
let i = id.idx();
self.refs[i] -= 1;
if self.refs[i] == 0 {
self.changed = true;
self.inner.remove(id)
} else {
self[i]
}
}
}
impl<T, I: IdNum> Default for TrackedArena<T, I> {
fn default() -> Self {
Self::new()
}
}
impl<T, I> Deref for TrackedArena<T, I> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.inner.data
}
}
impl<T, I> Deref for Arena<T, I> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.data
}
}

View File

@@ -1,25 +1,81 @@
#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy)]
pub struct Id(u64);
#[repr(C)]
#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy, bytemuck::Zeroable)]
pub struct Id<I = u64>(I);
#[derive(Default)]
pub struct IdTracker {
free: Vec<Id>,
cur: u64,
unsafe impl<I: Copy + bytemuck::Zeroable + 'static> bytemuck::Pod for Id<I> {}
pub struct IdTracker<I = u64> {
free: Vec<Id<I>>,
cur: Id<I>,
}
impl IdTracker {
impl<I: IdNum> IdTracker<I> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Id {
pub fn next(&mut self) -> Id<I> {
if let Some(id) = self.free.pop() {
return id;
}
let id = Id(self.cur);
self.cur += 1;
id
let next = self.cur.next();
std::mem::replace(&mut self.cur, next)
}
#[allow(dead_code)]
pub fn free(&mut self, id: Id) {
pub fn free(&mut self, id: Id<I>) {
self.free.push(id);
}
}
impl<I: IdNum> Id<I> {
pub fn idx(&self) -> usize {
self.0.idx()
}
pub fn next(&self) -> Id<I> {
Self(self.0.next())
}
pub const fn preset(value: I) -> Self {
Self(value)
}
}
impl<I: IdNum> Default for IdTracker<I> {
fn default() -> Self {
Self {
free: Vec::new(),
cur: Id(I::first()),
}
}
}
pub trait IdNum {
fn first() -> Self;
fn next(&self) -> Self;
fn idx(&self) -> usize;
}
impl IdNum for u64 {
fn first() -> Self {
0
}
fn next(&self) -> Self {
self + 1
}
fn idx(&self) -> usize {
*self as usize
}
}
impl IdNum for u32 {
fn first() -> Self {
0
}
fn next(&self) -> Self {
self + 1
}
fn idx(&self) -> usize {
*self as usize
}
}

View File

@@ -1,9 +1,11 @@
mod arena;
mod borrow;
mod change;
mod id;
mod math;
mod refcount;
pub(crate) use arena::*;
pub(crate) use borrow::*;
pub use change::*;
pub(crate) use id::*;