Take a primitive's kind from its type, and keep images out of the rest
A `Primitive` now carries its own WGSL, and `PrimitiveRegistry` keys ids by `TypeId`, so `Painter::primitive` takes only the value and the `RECT`, `GLYPH` and `TEXTURE` constants are gone. Registering is what a first draw does; the built-ins are seeded up front so first-draw order cannot decide anything about them. What a primitive samples is no longer something every registration states. The glyph atlas and the one sampler moved into the shared group, which is where a mask texture would go too, so a rect's pipeline has no texture in its layout at all. Only a primitive whose type sets `TEXTURE` gets an image group, and that is also what records the slot at write time -- so nothing reads a `u32` back out of the instance payload. Verified on the headless rig: the tabs example, two images added at runtime, an image alone in a layer, and text spanning a four-layer atlas after the array grew twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b318e3271
commit
29d390da52
11 files changed
+268
-333
No files matched your search
+103
-122
@@ -30,7 +30,7 @@ const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
pub struct UiRenderNode {
|
||||
shared_layout: BindGroupLayout,
|
||||
shared_group: BindGroup,
|
||||
texture_layout: BindGroupLayout,
|
||||
image_layout: BindGroupLayout,
|
||||
format: TextureFormat,
|
||||
|
||||
/// One per registered primitive, in id order.
|
||||
@@ -46,26 +46,22 @@ pub struct UiRenderNode {
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
/// One per registered primitive, in id order, matching `LayerDraws` --
|
||||
/// `None` where this layer draws none, so a primitive nobody uses costs no
|
||||
/// buffers per layer.
|
||||
/// One per registered primitive, `None` where this layer draws none.
|
||||
primitives: Vec<Option<ListBuffers>>,
|
||||
}
|
||||
|
||||
/// What draws one registered primitive. The group 1 layout is its own rather
|
||||
/// than shared, so its entry size is the minimum only for it.
|
||||
/// What draws one registered primitive.
|
||||
struct PrimitivePipeline {
|
||||
data_layout: BindGroupLayout,
|
||||
pipeline: RenderPipeline,
|
||||
texture: PrimitiveTexture,
|
||||
}
|
||||
|
||||
/// One list's vertex buffer and the data its shader reads at group 1.
|
||||
/// One list's vertex buffer and the data its shader reads.
|
||||
struct ListBuffers {
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
data: ArrBuf<u8>,
|
||||
group: Option<BindGroup>,
|
||||
/// For a `PerInstance` primitive, the texture each instance binds.
|
||||
/// The image each instance binds. Empty unless the primitive is textured.
|
||||
slots: Vec<u32>,
|
||||
}
|
||||
|
||||
@@ -74,37 +70,24 @@ impl UiRenderNode {
|
||||
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||
for i in &self.active {
|
||||
let layer = &self.layers[i];
|
||||
// Types run in registration order, so a rect is under a glyph is
|
||||
// under a texture. Ordering beyond that is what `Layers` is for --
|
||||
// freeing an instance swaps another into its place.
|
||||
for (id, list) in layer.primitives.iter().enumerate() {
|
||||
let Some(list) = list else {
|
||||
continue;
|
||||
};
|
||||
let Some(group) = &list.group else {
|
||||
continue;
|
||||
};
|
||||
let primitive = &self.primitives[id];
|
||||
pass.set_pipeline(&primitive.pipeline);
|
||||
// Both groups after the pipeline: each primitive has its own
|
||||
// pipeline layout, and a change drops the groups from where
|
||||
// the two layouts differ.
|
||||
let Some(list) = list else { continue };
|
||||
let Some(group) = &list.group else { continue };
|
||||
pass.set_pipeline(&self.primitives[id].pipeline);
|
||||
// After the pipeline: a change drops the groups from where two
|
||||
// pipeline layouts differ, and each primitive has its own.
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||
match primitive.texture {
|
||||
PrimitiveTexture::Atlas => {
|
||||
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||
pass.draw(0..4, 0..list.instance.len() as u32);
|
||||
}
|
||||
PrimitiveTexture::PerInstance => {
|
||||
for (i, &slot) in list.slots.iter().enumerate() {
|
||||
let Some(texture) = self.textures.group(slot) else {
|
||||
continue;
|
||||
};
|
||||
pass.set_bind_group(2, texture, &[]);
|
||||
pass.draw(0..4, i as u32..i as u32 + 1);
|
||||
}
|
||||
}
|
||||
if list.slots.is_empty() {
|
||||
pass.draw(0..4, 0..list.instance.len() as u32);
|
||||
continue;
|
||||
}
|
||||
for (i, &slot) in list.slots.iter().enumerate() {
|
||||
let Some(image) = self.textures.group(slot) else {
|
||||
continue;
|
||||
};
|
||||
pass.set_bind_group(2, image, &[]);
|
||||
pass.draw(0..4, i as u32..i as u32 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,9 +100,7 @@ impl UiRenderNode {
|
||||
ui: &mut UiData,
|
||||
ui_render: &mut UiRenderState,
|
||||
) {
|
||||
// Before the layers: a list is given the bind group layout of the
|
||||
// pipeline that will draw it, so every registered primitive needs one
|
||||
// by the time a layer is reached.
|
||||
// Before the layers: each list is given its pipeline's data layout.
|
||||
self.build_pipelines(device, &ui.primitives);
|
||||
self.active.clear();
|
||||
for (i, draws) in ui_render.layers.iter_mut() {
|
||||
@@ -150,35 +131,30 @@ impl UiRenderNode {
|
||||
};
|
||||
// Indexed, not zipped: a missing pipeline should say so
|
||||
// rather than quietly leave the list unbuilt.
|
||||
let primitive = &self.primitives[id];
|
||||
let layout = &self.primitives[id].data_layout;
|
||||
buffers
|
||||
.get_or_insert_with(|| ListBuffers::new(device))
|
||||
.update(
|
||||
device,
|
||||
queue,
|
||||
primitive.texture,
|
||||
&primitive.data_layout,
|
||||
list,
|
||||
);
|
||||
.update(device, queue, layout, list);
|
||||
}
|
||||
draws.updated = false;
|
||||
}
|
||||
}
|
||||
let mut shared_stale = self.pages.update(&mut ui.text.atlas);
|
||||
if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||
self.shared_group = Self::shared_group(
|
||||
device,
|
||||
&self.shared_layout,
|
||||
&self.window_buffer,
|
||||
&self.masks,
|
||||
);
|
||||
}
|
||||
shared_stale |= self.masks.update(device, queue, &ui.masks[..]);
|
||||
}
|
||||
self.pages
|
||||
.update(&mut ui.text.atlas, &self.texture_layout, &self.sampler);
|
||||
self.textures
|
||||
.update(&mut ui.textures, &self.texture_layout, &self.sampler);
|
||||
if shared_stale {
|
||||
self.shared_group = Self::shared_group(
|
||||
device,
|
||||
&self.shared_layout,
|
||||
&self.window_buffer,
|
||||
&self.masks,
|
||||
&self.pages,
|
||||
&self.sampler,
|
||||
);
|
||||
}
|
||||
self.textures.update(&mut ui.textures, &self.image_layout);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
@@ -202,23 +178,30 @@ impl UiRenderNode {
|
||||
});
|
||||
|
||||
let shared_layout = Self::shared_layout(device);
|
||||
let texture_layout = Self::texture_layout(device);
|
||||
let image_layout = Self::image_layout(device);
|
||||
|
||||
let masks = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||
|
||||
let sampler = default_sampler(device);
|
||||
let pages = GpuPages::new(device, queue, &texture_layout, &sampler);
|
||||
let pages = GpuPages::new(device, queue);
|
||||
let textures = GpuTextures::new(device, queue);
|
||||
let shared_group = Self::shared_group(
|
||||
device,
|
||||
&shared_layout,
|
||||
&window_buffer,
|
||||
&masks,
|
||||
&pages,
|
||||
&sampler,
|
||||
);
|
||||
|
||||
Self {
|
||||
shared_layout,
|
||||
shared_group,
|
||||
texture_layout,
|
||||
image_layout,
|
||||
format: config.format,
|
||||
primitives: Vec::new(),
|
||||
window_buffer,
|
||||
@@ -236,34 +219,23 @@ impl UiRenderNode {
|
||||
fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) {
|
||||
for source in ®istry.sources()[self.primitives.len()..] {
|
||||
let data_layout = Self::data_layout(device, source.stride);
|
||||
let layout = Self::pipeline_layout(
|
||||
device,
|
||||
&self.shared_layout,
|
||||
&data_layout,
|
||||
&self.texture_layout,
|
||||
);
|
||||
let mut groups = vec![&self.shared_layout, &data_layout];
|
||||
if source.textured {
|
||||
groups.push(&self.image_layout);
|
||||
}
|
||||
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some(source.label),
|
||||
bind_group_layouts: &groups,
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||
self.primitives.push(PrimitivePipeline {
|
||||
data_layout,
|
||||
pipeline,
|
||||
texture: source.texture,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pipeline_layout(
|
||||
device: &Device,
|
||||
shared: &BindGroupLayout,
|
||||
data: &BindGroupLayout,
|
||||
texture: &BindGroupLayout,
|
||||
) -> PipelineLayout {
|
||||
device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("ui"),
|
||||
bind_group_layouts: &[shared, data, texture],
|
||||
immediate_size: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
@@ -314,7 +286,8 @@ impl UiRenderNode {
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 0: what every draw in the ui shares.
|
||||
/// What every draw in the ui is given, whether or not its shader reads it:
|
||||
/// the window, the masks, the glyph atlas and the one sampler.
|
||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
@@ -338,6 +311,22 @@ impl UiRenderNode {
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
@@ -348,6 +337,8 @@ impl UiRenderNode {
|
||||
layout: &BindGroupLayout,
|
||||
window: &Buffer,
|
||||
masks: &ArrBuf<Mask>,
|
||||
pages: &GpuPages,
|
||||
sampler: &Sampler,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
@@ -360,16 +351,22 @@ impl UiRenderNode {
|
||||
binding: 1,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::TextureView(pages.view()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 1: one list's per-instance data, whatever its shader reads it as.
|
||||
///
|
||||
/// One per primitive with `stride` stated, because a `None` minimum is
|
||||
/// filled in from the first pipeline built against the layout -- sharing
|
||||
/// one would hold every primitive to the largest.
|
||||
/// One list's per-instance data. Per primitive with `stride` stated: a
|
||||
/// `None` minimum is filled in from the first pipeline built against the
|
||||
/// layout, so a shared one would hold every primitive to the largest.
|
||||
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
@@ -386,29 +383,21 @@ impl UiRenderNode {
|
||||
})
|
||||
}
|
||||
|
||||
/// Group 2: the texture this draw samples, and the sampler. No `count` on
|
||||
/// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs.
|
||||
fn texture_layout(device: &Device) -> BindGroupLayout {
|
||||
/// The one image an instance samples. The sampler is shared, so there is
|
||||
/// nothing else in here.
|
||||
fn image_layout(device: &Device) -> 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::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui texture"),
|
||||
count: None,
|
||||
}],
|
||||
label: Some("ui image"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -447,22 +436,14 @@ impl ListBuffers {
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
texture: PrimitiveTexture,
|
||||
layout: &BindGroupLayout,
|
||||
list: &InstanceList,
|
||||
) {
|
||||
if texture == PrimitiveTexture::PerInstance {
|
||||
self.slots.clear();
|
||||
// Read rather than cast: the payload is a byte vec, so it carries
|
||||
// no alignment a `u32` slice could borrow.
|
||||
let slots = list.data().as_chunks::<4>().0;
|
||||
self.slots
|
||||
.extend(slots.iter().copied().map(u32::from_ne_bytes));
|
||||
}
|
||||
self.slots.clear();
|
||||
self.slots.extend_from_slice(list.slots());
|
||||
self.instance.update(device, queue, list.instances());
|
||||
let resized = self.data.update(device, queue, list.data());
|
||||
// An empty list has no buffer big enough for one entry, and nothing
|
||||
// draws it, so it has no bind group either.
|
||||
// An empty list has no buffer big enough to bind, and nothing to draw.
|
||||
if list.instances().is_empty() {
|
||||
self.group = None;
|
||||
} else if resized || self.group.is_none() {
|
||||
|
||||
+21
-21
@@ -2,10 +2,7 @@ use wgpu::*;
|
||||
|
||||
use crate::GlyphAtlas;
|
||||
|
||||
use super::{
|
||||
atlas::PAGE,
|
||||
texture::{array_view, texture_group, write_region},
|
||||
};
|
||||
use super::{atlas::PAGE, texture::write_region};
|
||||
|
||||
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||
/// `GlyphAtlas` packs.
|
||||
@@ -17,29 +14,25 @@ pub struct GpuPages {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
view: TextureView,
|
||||
}
|
||||
|
||||
impl GpuPages {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> Self {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let texture = create_array(device, 1);
|
||||
let group = texture_group(device, layout, &array_view(&texture), sampler);
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
view: array_view(&texture),
|
||||
texture,
|
||||
group,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
||||
self.grow(atlas.page_count(), layout, sampler);
|
||||
/// Returns whether the array was replaced, which stales the view.
|
||||
pub fn update(&mut self, atlas: &mut GlyphAtlas) -> bool {
|
||||
let grew = atlas.page_count() > self.texture.depth_or_array_layers();
|
||||
if grew {
|
||||
self.grow(atlas.page_count());
|
||||
}
|
||||
for (upload, page) in atlas.uploads() {
|
||||
let dst = TexelCopyTextureInfo {
|
||||
@@ -54,15 +47,15 @@ impl GpuPages {
|
||||
};
|
||||
write_region(&self.queue, dst, page, upload.rect);
|
||||
}
|
||||
grew
|
||||
}
|
||||
|
||||
pub fn group(&self) -> &BindGroup {
|
||||
&self.group
|
||||
pub fn view(&self) -> &TextureView {
|
||||
&self.view
|
||||
}
|
||||
|
||||
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||
/// The new view invalidates the old group, so that is rebuilt here.
|
||||
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
fn grow(&mut self, needed: u32) {
|
||||
let old = self.texture.depth_or_array_layers();
|
||||
let mut layers = old;
|
||||
while layers < needed {
|
||||
@@ -84,11 +77,18 @@ impl GpuPages {
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.group = texture_group(&self.device, layout, &array_view(&texture), sampler);
|
||||
self.view = array_view(&texture);
|
||||
self.texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
fn array_view(texture: &Texture) -> TextureView {
|
||||
texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas"),
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
|
||||
use crate::{
|
||||
Color, UiRegion, WidgetId,
|
||||
render::data::{MaskIdx, PrimitiveInstance},
|
||||
util::Vec2,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
|
||||
/// One instance of a registered primitive, laid out as the struct that
|
||||
/// primitive's shader reads at `@group(1) @binding(0)`.
|
||||
/// One instance of a primitive, laid out as the struct its shader reads.
|
||||
///
|
||||
/// A `PrimitiveKind<P>` is only minted by `register::<P>`, and `write` takes
|
||||
/// the kind and the value together, so holding one is the proof that `P` has a
|
||||
/// list of its own to go in and a write needs no check.
|
||||
pub trait Primitive: Pod {}
|
||||
/// The type carries its own shader, so drawing one is all the wiring it needs:
|
||||
/// its list, free list, buffers and pipeline follow from being registered.
|
||||
pub trait Primitive: Pod + 'static {
|
||||
/// Compiled after `prelude.wgsl`, which states what it declares and what
|
||||
/// it is given.
|
||||
const WGSL: &'static str;
|
||||
/// Reads the image an instance samples, for a primitive that draws one.
|
||||
/// Each instance is then a draw of its own, bound for it alone.
|
||||
const TEXTURE: Option<fn(&Self) -> u32> = None;
|
||||
}
|
||||
|
||||
/// Which registered primitive an instance is, and so which list it lives in
|
||||
/// and which pipeline draws it. The type ties a `write` to what its shader reads.
|
||||
/// Which registered primitive an instance is. Only `PrimitiveRegistry::kind`
|
||||
/// mints one, so holding it is the proof that `P` has a list to go in.
|
||||
pub struct PrimitiveKind<P> {
|
||||
id: u32,
|
||||
_p: PhantomData<fn(P)>,
|
||||
}
|
||||
|
||||
impl<P> PrimitiveKind<P> {
|
||||
const fn new(id: u32) -> Self {
|
||||
fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Clone for PrimitiveKind<P> {
|
||||
@@ -43,80 +44,50 @@ impl<P> Clone for PrimitiveKind<P> {
|
||||
|
||||
impl<P> Copy for PrimitiveKind<P> {}
|
||||
|
||||
pub const RECT: PrimitiveKind<RectPrimitive> = PrimitiveKind::new(0);
|
||||
pub const GLYPH: PrimitiveKind<GlyphPrimitive> = PrimitiveKind::new(1);
|
||||
pub const TEXTURE: PrimitiveKind<TexturePrimitive> = PrimitiveKind::new(2);
|
||||
|
||||
/// Every primitive a ui can draw, in id order. Registering one is all the
|
||||
/// wiring it needs: its list, buffers, free list and pipeline follow, and
|
||||
/// nothing here knows its type.
|
||||
///
|
||||
/// A source is compiled after `prelude.wgsl` and supplies its data at
|
||||
/// `@group(1) @binding(0)` and an `fs_main` shading one instance.
|
||||
///
|
||||
/// Order is draw order within a layer, so a primitive registered later is
|
||||
/// drawn over one registered earlier.
|
||||
/// Every primitive a ui can draw, in the order they were first drawn.
|
||||
pub struct PrimitiveRegistry {
|
||||
kinds: Vec<PrimitiveSource>,
|
||||
ids: HashMap<TypeId, u32>,
|
||||
}
|
||||
|
||||
pub struct PrimitiveSource {
|
||||
pub wgsl: &'static str,
|
||||
pub label: &'static str,
|
||||
/// Size of one instance's entry, which the renderer states as the group 1
|
||||
/// binding's minimum rather than leaving it to be inferred.
|
||||
/// Size of one instance's entry, stated as the data binding's minimum.
|
||||
pub stride: u64,
|
||||
pub texture: PrimitiveTexture,
|
||||
}
|
||||
|
||||
/// What a primitive samples at group 2, which is the whole of why some of them
|
||||
/// cannot share one instanced draw.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PrimitiveTexture {
|
||||
/// The shared glyph atlas, bound once for the layer.
|
||||
Atlas,
|
||||
/// The `Textures` slot in the first four bytes of its own data, bound for
|
||||
/// that instance alone -- so one draw call each.
|
||||
PerInstance,
|
||||
/// Whether an instance binds a texture of its own, from `P::TEXTURE`.
|
||||
pub textured: bool,
|
||||
}
|
||||
|
||||
impl Default for PrimitiveRegistry {
|
||||
/// The built-ins are registered up front rather than on first use, so that
|
||||
/// what a ui happens to draw first cannot decide anything about them.
|
||||
fn default() -> Self {
|
||||
use PrimitiveTexture::*;
|
||||
let mut registry = Self { kinds: Vec::new() };
|
||||
let rect =
|
||||
registry.register::<RectPrimitive>(include_str!("shader/rect.wgsl"), "rect", Atlas);
|
||||
let glyph =
|
||||
registry.register::<GlyphPrimitive>(include_str!("shader/glyph.wgsl"), "glyph", Atlas);
|
||||
let texture = registry.register::<TexturePrimitive>(
|
||||
include_str!("shader/texture.wgsl"),
|
||||
"texture",
|
||||
PerInstance,
|
||||
);
|
||||
// The built-ins have constant ids so a widget can name one without the
|
||||
// registry; registering them first is what makes those constants true.
|
||||
assert_eq!(
|
||||
(rect.id(), glyph.id(), texture.id()),
|
||||
(RECT.id(), GLYPH.id(), TEXTURE.id())
|
||||
);
|
||||
let mut registry = Self {
|
||||
kinds: Vec::new(),
|
||||
ids: HashMap::default(),
|
||||
};
|
||||
registry.kind::<RectPrimitive>();
|
||||
registry.kind::<GlyphPrimitive>();
|
||||
registry.kind::<TexturePrimitive>();
|
||||
registry
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveRegistry {
|
||||
pub fn register<P: Primitive>(
|
||||
&mut self,
|
||||
wgsl: &'static str,
|
||||
label: &'static str,
|
||||
texture: PrimitiveTexture,
|
||||
) -> PrimitiveKind<P> {
|
||||
self.kinds.push(PrimitiveSource {
|
||||
wgsl,
|
||||
label,
|
||||
stride: size_of::<P>() as u64,
|
||||
texture,
|
||||
/// Registers `P` if this is the first time it has been drawn.
|
||||
pub fn kind<P: Primitive>(&mut self) -> PrimitiveKind<P> {
|
||||
let Self { kinds, ids } = self;
|
||||
let id = *ids.entry(TypeId::of::<P>()).or_insert_with(|| {
|
||||
kinds.push(PrimitiveSource {
|
||||
wgsl: P::WGSL,
|
||||
label: std::any::type_name::<P>(),
|
||||
stride: size_of::<P>() as u64,
|
||||
textured: P::TEXTURE.is_some(),
|
||||
});
|
||||
kinds.len() as u32 - 1
|
||||
});
|
||||
PrimitiveKind::new(self.kinds.len() as u32 - 1)
|
||||
PrimitiveKind::new(id)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[PrimitiveSource] {
|
||||
@@ -124,18 +95,18 @@ impl PrimitiveRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// One registered primitive's instances in one layer: the instances, the
|
||||
/// widget each belongs to, the slots waiting to be reused, and `stride` bytes
|
||||
/// of that primitive's data per instance at the same index.
|
||||
///
|
||||
/// `stride` comes from the type the list was made for, so a write is never
|
||||
/// checked against it. The data rides here rather than beside the list so the
|
||||
/// two stay in step through a `swap_remove`.
|
||||
/// One registered primitive's instances in one layer. Everything per-instance
|
||||
/// rides here, so it stays in step through a `swap_remove`.
|
||||
pub struct InstanceList {
|
||||
instances: Vec<PrimitiveInstance>,
|
||||
/// The widget each instance belongs to, for renumbering its handles.
|
||||
assoc: Vec<WidgetId>,
|
||||
free: Vec<usize>,
|
||||
/// `stride` bytes of the primitive's own data per instance.
|
||||
data: Vec<u8>,
|
||||
/// The image each instance samples, from `P::TEXTURE`. Empty without it.
|
||||
slots: Vec<u32>,
|
||||
/// From the type the list was made for, so a write is never checked.
|
||||
stride: usize,
|
||||
}
|
||||
|
||||
@@ -146,6 +117,7 @@ impl InstanceList {
|
||||
assoc: Vec::new(),
|
||||
free: Vec::new(),
|
||||
data: Vec::new(),
|
||||
slots: Vec::new(),
|
||||
stride: size_of::<P>(),
|
||||
}
|
||||
}
|
||||
@@ -158,17 +130,31 @@ impl InstanceList {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
|
||||
pub fn slots(&self) -> &[u32] {
|
||||
&self.slots
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
inst: PrimitiveInstance,
|
||||
data: &[u8],
|
||||
slot: Option<u32>,
|
||||
) -> usize {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
|
||||
if let Some(slot) = slot {
|
||||
self.slots[i] = slot;
|
||||
}
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
self.data.extend_from_slice(data);
|
||||
self.slots.extend(slot);
|
||||
i
|
||||
}
|
||||
}
|
||||
@@ -183,10 +169,14 @@ impl InstanceList {
|
||||
let instances = &mut self.instances;
|
||||
let assoc = &mut self.assoc;
|
||||
let data = &mut self.data;
|
||||
let slots = &mut self.slots;
|
||||
let stride = self.stride;
|
||||
self.free.drain(..).filter_map(move |i| {
|
||||
instances.swap_remove(i);
|
||||
assoc.swap_remove(i);
|
||||
if !slots.is_empty() {
|
||||
slots.swap_remove(i);
|
||||
}
|
||||
let last = instances.len();
|
||||
data.copy_within(last * stride..(last + 1) * stride, i * stride);
|
||||
data.truncate(last * stride);
|
||||
@@ -204,9 +194,7 @@ impl InstanceList {
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything one layer draws, one list per registered primitive. They index
|
||||
/// independently, so a handle or a renumbering naming only a position would be
|
||||
/// ambiguous between them.
|
||||
/// Everything one layer draws, one list per registered primitive.
|
||||
pub struct LayerDraws {
|
||||
/// `None` until this layer draws that primitive, because only the write
|
||||
/// knows the type the list is for.
|
||||
@@ -247,6 +235,7 @@ impl LayerDraws {
|
||||
id,
|
||||
PrimitiveInstance { region, mask_idx },
|
||||
bytemuck::bytes_of(&primitive),
|
||||
P::TEXTURE.map(|slot| slot(&primitive)),
|
||||
);
|
||||
PrimitiveHandle {
|
||||
layer,
|
||||
@@ -309,7 +298,7 @@ pub struct PrimitiveHandle {
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct RectPrimitive {
|
||||
pub color: Color<u8>,
|
||||
pub radius: f32,
|
||||
@@ -317,9 +306,9 @@ pub struct RectPrimitive {
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for RectPrimitive {}
|
||||
unsafe impl bytemuck::Zeroable for RectPrimitive {}
|
||||
impl Primitive for RectPrimitive {}
|
||||
impl Primitive for RectPrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/rect.wgsl");
|
||||
}
|
||||
|
||||
impl RectPrimitive {
|
||||
pub fn color(color: Color<u8>) -> Self {
|
||||
@@ -345,18 +334,23 @@ pub struct GlyphPrimitive {
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
||||
// is how WGSL lays the struct out.
|
||||
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||
impl Primitive for GlyphPrimitive {}
|
||||
impl Primitive for GlyphPrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
|
||||
}
|
||||
|
||||
/// One drawn image. Its shader reads nothing: the slot names the texture bound
|
||||
/// for this instance alone, which is what `PrimitiveTexture::PerInstance` does.
|
||||
/// One drawn image. Its shader reads nothing per instance; the slot names the
|
||||
/// texture to bind for it.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct TexturePrimitive {
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for TexturePrimitive {}
|
||||
unsafe impl bytemuck::Zeroable for TexturePrimitive {}
|
||||
impl Primitive for TexturePrimitive {}
|
||||
impl Primitive for TexturePrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/texture.wgsl");
|
||||
const TEXTURE: Option<fn(&Self) -> u32> = Some(|texture| texture.slot);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Matches `GlyphEntry::IS_COLORED`.
|
||||
const COLORED: u32 = 1u;
|
||||
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
@@ -14,8 +17,8 @@ var<storage> glyphs: array<GlyphInfo>;
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let g = glyphs[in.idx];
|
||||
let uv = mix(g.uv_min, g.uv_max, in.uv);
|
||||
let texel = textureSample(tex, samp, uv, i32(g.layer));
|
||||
if (g.flags & 1u) != 0u {
|
||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||
if (g.flags & COLORED) != 0u {
|
||||
return masked(in, texel);
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Prepended to every primitive's shader, which supplies only its own data
|
||||
// struct at group 1 and an `fs_main` that shades one instance.
|
||||
// Prepended to every primitive's shader, which declares its own instance data
|
||||
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
||||
// shading one instance of it. A primitive that samples an image of its own
|
||||
// takes it at group 2 binding 0; see texture.wgsl.
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(0) @binding(1)
|
||||
var<storage> masks: array<Mask>;
|
||||
|
||||
// The texture this draw samples: the glyph atlas, whose layers are its pages,
|
||||
// or one standalone image as an array of one.
|
||||
@group(2) @binding(0)
|
||||
var tex: texture_2d_array<f32>;
|
||||
@group(2) @binding(1)
|
||||
// The glyph atlas, whose array layers are its pages.
|
||||
@group(0) @binding(2)
|
||||
var atlas: texture_2d_array<f32>;
|
||||
@group(0) @binding(3)
|
||||
var samp: sampler;
|
||||
|
||||
struct WindowUniform {
|
||||
@@ -45,7 +45,6 @@ struct VertexOutput {
|
||||
@location(1) bot_right: vec2<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) @interpolate(flat) mask_idx: u32,
|
||||
// The instance's own index, which is also where its data sits in group 1.
|
||||
@location(4) @interpolate(flat) idx: u32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// No group 1: the texture is bound at group 2 for this instance alone, so
|
||||
// there is nothing per-instance left to look up.
|
||||
// The image this instance draws, bound for it alone.
|
||||
@group(2) @binding(0)
|
||||
var image: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return masked(in, textureSample(tex, samp, in.uv, 0));
|
||||
return masked(in, textureSample(image, samp, in.uv));
|
||||
}
|
||||
+15
-47
@@ -3,7 +3,7 @@ use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{PatchRect, TextureUpdate, Textures};
|
||||
|
||||
/// The standalone images a ui draws, each its own texture and group 2 --
|
||||
/// The standalone images a ui draws, each its own texture and bind group --
|
||||
/// unlike the glyph atlas in `super::page`, which is one array they share.
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
@@ -26,15 +26,15 @@ impl GpuTextures {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout) {
|
||||
for update in textures.updates() {
|
||||
match update {
|
||||
TextureUpdate::Push(image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
let image = self.create(image, layout);
|
||||
self.slots.push(Some(image));
|
||||
}
|
||||
TextureUpdate::Set(i, image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
let image = self.create(image, layout);
|
||||
self.slots[i as usize] = Some(image);
|
||||
}
|
||||
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||
@@ -45,8 +45,6 @@ impl GpuTextures {
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` once freed. A drawn instance holds a `TextureHandle`, so its
|
||||
/// slot outlives it.
|
||||
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||
}
|
||||
@@ -55,12 +53,7 @@ impl GpuTextures {
|
||||
self.slots.iter().flatten().count()
|
||||
}
|
||||
|
||||
fn create(
|
||||
&self,
|
||||
image: &DynamicImage,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> ImageGpu {
|
||||
fn create(&self, image: &DynamicImage, layout: &BindGroupLayout) -> ImageGpu {
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
let texture = self.device.create_texture_with_data(
|
||||
@@ -82,7 +75,16 @@ impl GpuTextures {
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
rgba.as_bytes(),
|
||||
);
|
||||
let group = texture_group(&self.device, layout, &array_view(&texture), sampler);
|
||||
let group = self.device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(
|
||||
&texture.create_view(&TextureViewDescriptor::default()),
|
||||
),
|
||||
}],
|
||||
label: Some("ui image"),
|
||||
});
|
||||
ImageGpu { texture, group }
|
||||
}
|
||||
|
||||
@@ -114,8 +116,6 @@ impl GpuTextures {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads `rect` of `src` without copying it out first: `write_texture` takes
|
||||
/// a row stride, so a region can be addressed where it already is.
|
||||
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
@@ -137,38 +137,6 @@ pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, r
|
||||
);
|
||||
}
|
||||
|
||||
/// One array texture and a sampler, so the atlas and a standalone image share a
|
||||
/// layout and the shader samples whichever is bound -- an image being a
|
||||
/// single-layer texture viewed as an array of one.
|
||||
pub fn texture_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
label: Some("ui texture"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn array_view(texture: &Texture) -> TextureView {
|
||||
texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn default_sampler(device: &Device) -> Sampler {
|
||||
device.create_sampler(&SamplerDescriptor::default())
|
||||
}
|
||||
@@ -39,11 +39,8 @@ 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) {
|
||||
// An empty storage buffer is still bound, and a binding has to be
|
||||
// non-empty and a multiple of four however small `T` is.
|
||||
size = size
|
||||
.max(std::mem::size_of::<T>() as u64)
|
||||
.next_multiple_of(4);
|
||||
// A binding cannot be empty or under the layout's minimum.
|
||||
size = size.max(std::mem::size_of::<T>() as u64);
|
||||
}
|
||||
device.create_buffer(&BufferDescriptor {
|
||||
label: Some(label),
|
||||
|
||||
+17
-20
@@ -2,8 +2,8 @@ use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{
|
||||
GLYPH, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||
PrimitiveKind, TEXTURE, TexturePrimitive,
|
||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
util::Vec2,
|
||||
};
|
||||
@@ -23,12 +23,14 @@ pub struct Painter<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
fn primitive_at<P: Primitive>(
|
||||
&mut self,
|
||||
kind: PrimitiveKind<P>,
|
||||
primitive: P,
|
||||
region: UiRegion,
|
||||
) {
|
||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
||||
self.write(kind, primitive, region);
|
||||
}
|
||||
|
||||
/// For a caller with many of one primitive to write, since looking the kind
|
||||
/// up is per type rather than per instance.
|
||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||
let h = self.state.layers.write(
|
||||
self.layer,
|
||||
PrimitiveInst {
|
||||
@@ -51,17 +53,12 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
/// Writes a primitive to be rendered
|
||||
pub fn primitive<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P) {
|
||||
self.primitive_at(kind, primitive, self.region)
|
||||
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
pub fn primitive_within<P: Primitive>(
|
||||
&mut self,
|
||||
kind: PrimitiveKind<P>,
|
||||
primitive: P,
|
||||
region: UiRegion,
|
||||
) {
|
||||
self.primitive_at(kind, primitive, region.within(&self.region));
|
||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.primitive_at(primitive, region.within(&self.region));
|
||||
}
|
||||
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
@@ -104,7 +101,6 @@ impl<'a> Painter<'a> {
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.primitive_at(
|
||||
TEXTURE,
|
||||
TexturePrimitive {
|
||||
slot: handle.slot(),
|
||||
},
|
||||
@@ -123,6 +119,7 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
@@ -130,8 +127,8 @@ impl<'a> Painter<'a> {
|
||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||
self.primitive_at(
|
||||
GLYPH,
|
||||
self.write(
|
||||
kind,
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
|
||||
+6
-9
@@ -29,15 +29,12 @@ impl Rect {
|
||||
|
||||
impl Widget for Rect {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(
|
||||
RECT,
|
||||
RectPrimitive {
|
||||
color: self.color,
|
||||
radius: self.radius,
|
||||
thickness: self.thickness,
|
||||
inner_radius: self.inner_radius,
|
||||
},
|
||||
);
|
||||
painter.primitive(RectPrimitive {
|
||||
color: self.color,
|
||||
radius: self.radius,
|
||||
thickness: self.thickness,
|
||||
inner_radius: self.inner_radius,
|
||||
});
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
|
||||
@@ -73,7 +73,6 @@ impl Widget for TextEdit {
|
||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RECT,
|
||||
RectPrimitive::color(Color::SKY),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
@@ -83,7 +82,6 @@ impl Widget for TextEdit {
|
||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RECT,
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user