Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa771a99eb | ||
|
|
e817bc83af | ||
|
|
a648c62aa2 | ||
|
|
c118bb446b | ||
|
|
1102dc7338 | ||
|
|
1aadef0e7e | ||
|
|
426ff0adfc | ||
|
|
dab6cf298a | ||
|
|
38d896d44d |
No files matched your search
Generated
+612
-766
File diff suppressed because it is too large.
Load diff
+7
-7
@@ -8,7 +8,8 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
iris-core = { workspace = true }
|
||||
iris-macro = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
cosmic-text = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
winit = { workspace = true }
|
||||
arboard = { workspace = true, features = ["wayland-data-control"] }
|
||||
pollster = { workspace = true }
|
||||
@@ -27,15 +28,14 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
pollster = "0.4.0"
|
||||
pollster = "1.0.1"
|
||||
winit = "0.30.12"
|
||||
wgpu = "30.0.1"
|
||||
wgpu = "30.0.0"
|
||||
bytemuck = "1.23.1"
|
||||
image = "0.25.10"
|
||||
parley = "0.11.1"
|
||||
swash = "0.2.10"
|
||||
image = "0.25.6"
|
||||
cosmic-text = "0.16.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
fxhash = "0.2.1"
|
||||
log = "0.4.29"
|
||||
arboard = "3.6.1"
|
||||
iris-core = { path = "core" }
|
||||
iris-macro = { path = "macro" }
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
images
|
||||
settings (sampler)
|
||||
consider typed TextureHandle<T> variants for distinct texture uses
|
||||
|
||||
text
|
||||
figure out ways to speed up / what costs the most
|
||||
resizing (per frame) is really slow (assuming painter isn't griefing)
|
||||
j is weird / fix x offset
|
||||
|
||||
masks r just made to bare minimum work
|
||||
|
||||
scaling
|
||||
could be just a simple scaling factor that multiplies abs
|
||||
and need to ensure text uses raw abs and not scaled abs
|
||||
naming? (pt, px)
|
||||
want to keep (drawn) regions using px? or should I add another field to UiScalar/Vec
|
||||
field could be best solution so redrawing stuff isn't needed & you can specify both as user
|
||||
|
||||
WidgetRef<W> or smth instead of Id
|
||||
enum that's either an Id or an actual concrete instance of W
|
||||
@@ -11,6 +24,17 @@ WidgetRef<W> or smth instead of Id
|
||||
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
|
||||
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
|
||||
|
||||
really weird limitation:
|
||||
I don't think you can currently remove an element from a parent and put it in a child of the same parent
|
||||
because it removes the unused children after the entire parent redraw
|
||||
but the child gets drawn during that, so it will think the child is still active !!!
|
||||
or something like that idk, maybe I need a special enum for parent that includes a undecided state where it may or may not get redrawn by the parent
|
||||
or just do ref counting and ensure all drawn things == 1 afterwards (seems like best way)
|
||||
ok so I'm removing the limit for now
|
||||
|
||||
don't forget I'm streaming
|
||||
|
||||
tags
|
||||
vecs for each widget type?
|
||||
|
||||
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
||||
+2
-3
@@ -4,10 +4,9 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
winit = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
cosmic-text = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
log = { workspace = true }
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
|
||||
use crate::{UiRsc, WidgetIdFn, WidgetLike, WeakWidget};
|
||||
|
||||
pub trait WidgetAttr<Rsc, W: ?Sized> {
|
||||
type Input;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_destruct)]
|
||||
#![feature(portable_simd)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
|
||||
@@ -10,12 +10,6 @@ pub struct Color<T> {
|
||||
pub a: T,
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Default for Color<T> {
|
||||
fn default() -> Self {
|
||||
Self::BLACK
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Color<T> {
|
||||
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
||||
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{
|
||||
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
|
||||
util::to_mut,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ struct Child {
|
||||
tail: usize,
|
||||
}
|
||||
|
||||
pub type DrawLayers = Layers<LayerDraws>;
|
||||
pub type PrimitiveLayers = Layers<Primitives>;
|
||||
|
||||
impl<T: Default> Layers<T> {
|
||||
pub fn new() -> Layers<T> {
|
||||
@@ -119,7 +119,7 @@ impl<T: Default> Layers<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawLayers {
|
||||
impl PrimitiveLayers {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: LayerId,
|
||||
|
||||
+146
-237
@@ -1,66 +1,60 @@
|
||||
use crate::{
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||
};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use swash::{
|
||||
FontRef,
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
zeno::{Format, Vector},
|
||||
use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
|
||||
SwashContent,
|
||||
};
|
||||
use image::{DynamicImage, GenericImageView, RgbaImage};
|
||||
use std::simd::{Simd, num::SimdUint};
|
||||
|
||||
/// TODO: properly wrap this
|
||||
pub mod text_lib {
|
||||
pub use cosmic_text::*;
|
||||
}
|
||||
|
||||
pub struct TextData {
|
||||
pub font_ctx: FontContext,
|
||||
pub layout_ctx: LayoutContext<UiColor>,
|
||||
scale_ctx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
pub font_system: FontSystem,
|
||||
pub swash_cache: SwashCache,
|
||||
glyph_cache: Vec<(Placement, CacheKey, Color)>,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_ctx: FontContext::new(),
|
||||
layout_ctx: LayoutContext::new(),
|
||||
scale_ctx: ScaleContext::new(),
|
||||
atlas: GlyphAtlas::default(),
|
||||
font_system: FontSystem::new(),
|
||||
swash_cache: SwashCache::new(),
|
||||
glyph_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Family {
|
||||
fn family(&self) -> FontFamily<'_> {
|
||||
let name = match self {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
pub family: Family<'static>,
|
||||
pub wrap: bool,
|
||||
/// inner alignment of text region (within where it's drawn)
|
||||
pub align: RegionAlign,
|
||||
}
|
||||
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
impl TextAttrs {
|
||||
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
|
||||
buf.set_metrics_and_size(
|
||||
font_system,
|
||||
Metrics::new(self.font_size, self.line_height),
|
||||
width,
|
||||
None,
|
||||
);
|
||||
let attrs = Attrs::new().family(self.family);
|
||||
let list = AttrsList::new(&attrs);
|
||||
for line in &mut buf.lines {
|
||||
line.set_attrs_list(list.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type TextBuffer = Buffer;
|
||||
|
||||
impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
@@ -76,207 +70,122 @@ impl Default for TextAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps text and its corresponding layout from getting out of sync.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
layout_key: Option<LayoutKey>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct LayoutKey {
|
||||
attrs: TextAttrs,
|
||||
max_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl TextBuffer {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
layout: Layout::new(),
|
||||
layout_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
self.layout_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidates the layout and returns the underlying string for editing.
|
||||
pub fn edit(&mut self) -> &mut String {
|
||||
self.layout_key = None;
|
||||
&mut self.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
||||
let layout_key = LayoutKey {
|
||||
attrs: attrs.clone(),
|
||||
max_width: width,
|
||||
};
|
||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||
return;
|
||||
}
|
||||
let mut builder = data
|
||||
.layout_ctx
|
||||
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
builder.build_into(&mut self.layout, &self.text);
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.layout_key = Some(layout_key);
|
||||
}
|
||||
}
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl TextData {
|
||||
pub fn place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
||||
continue;
|
||||
};
|
||||
let font = run.run().font();
|
||||
let font_size = run.run().font_size();
|
||||
let coords = run.run().normalized_coords();
|
||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
let font_id = font.data.id();
|
||||
|
||||
for glyph in run.positioned_glyphs() {
|
||||
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
|
||||
let key = GlyphKey {
|
||||
font: font_id,
|
||||
glyph: glyph.id,
|
||||
size: glyph_size_key(font_size),
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||
key,
|
||||
font: font_ref,
|
||||
font_size,
|
||||
coords,
|
||||
subpixel,
|
||||
glyph_id: glyph.id,
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
placed.push(PlacedGlyph {
|
||||
entry,
|
||||
offset: Vec2::new(
|
||||
glyph.x.floor() + entry.left as f32,
|
||||
glyph.y.floor() - entry.top as f32,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
placed
|
||||
}
|
||||
|
||||
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
let mut scaler = self
|
||||
.scale_ctx
|
||||
.builder(glyph.font)
|
||||
.size(glyph.font_size)
|
||||
.hint(true)
|
||||
.normalized_coords(glyph.coords)
|
||||
.build();
|
||||
let image = Render::new(&[
|
||||
Source::ColorOutline(0),
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
Source::Outline,
|
||||
])
|
||||
.format(Format::Alpha)
|
||||
.offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0))
|
||||
.render(&mut scaler, glyph.glyph_id as u16);
|
||||
|
||||
if let Some(image) = image {
|
||||
self.atlas.insert(glyph.key, &image)
|
||||
} else {
|
||||
self.atlas.insert_empty(glyph.key);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GlyphRaster<'a> {
|
||||
key: GlyphKey,
|
||||
font: FontRef<'a>,
|
||||
font_size: f32,
|
||||
coords: &'a [i16],
|
||||
subpixel: u8,
|
||||
glyph_id: u32,
|
||||
}
|
||||
|
||||
fn hash_coords(coords: &[i16]) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
coords.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0;
|
||||
|
||||
fn glyph_size_key(font_size: f32) -> u32 {
|
||||
(font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32
|
||||
}
|
||||
|
||||
pub struct RenderedText {
|
||||
pub glyphs: Vec<PlacedGlyph>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn render(
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer);
|
||||
// TODO: either this or the layout stuff (or both) is super slow,
|
||||
// should probably do texture packing and things if possible.
|
||||
// very visible if you add just a couple of wrapping texts and resize window
|
||||
// should also be timed to figure out exactly what points need to be sped up
|
||||
// let mut pixels = HashMap::<_, [u8; 4]>::default();
|
||||
let mut min_x = 0;
|
||||
let mut min_y = 0;
|
||||
let mut max_x = 0;
|
||||
let mut max_y = 0;
|
||||
let text_color = {
|
||||
let c = attrs.color;
|
||||
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
|
||||
};
|
||||
let mut max_width = 0.0f32;
|
||||
let mut height = 0.0;
|
||||
|
||||
for run in buffer.layout_runs() {
|
||||
for glyph in run.glyphs.iter() {
|
||||
let physical_glyph = glyph.physical((0., 0.), 1.0);
|
||||
|
||||
let glyph_color = match glyph.color_opt {
|
||||
Some(some) => some,
|
||||
None => text_color,
|
||||
};
|
||||
|
||||
if let Some(img) = self
|
||||
.swash_cache
|
||||
.get_image(&mut self.font_system, physical_glyph.cache_key)
|
||||
{
|
||||
let mut pos = img.placement;
|
||||
pos.left += physical_glyph.x;
|
||||
pos.top = physical_glyph.y + run.line_y as i32 - pos.top;
|
||||
min_x = min_x.min(pos.left);
|
||||
min_y = min_y.min(pos.top);
|
||||
max_x = max_x.max(pos.left + pos.width as i32);
|
||||
max_y = max_y.max(pos.top + pos.height as i32);
|
||||
self.glyph_cache
|
||||
.push((pos, physical_glyph.cache_key, glyph_color));
|
||||
}
|
||||
}
|
||||
max_width = max_width.max(run.line_w);
|
||||
height += run.line_height;
|
||||
}
|
||||
let img_width = (max_x - min_x + 1) as u32;
|
||||
let img_height = (max_y - min_y + 1) as u32;
|
||||
let mut image = RgbaImage::new(img_width, img_height);
|
||||
|
||||
for (pos, key, color) in self.glyph_cache.drain(..) {
|
||||
let img = self
|
||||
.swash_cache
|
||||
.get_image(&mut self.font_system, key)
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let mut merge = |i, color: [u8; 4]| {
|
||||
let i = i as i32;
|
||||
let x = (i % pos.width as i32 + pos.left - min_x) as u32;
|
||||
let y = (i / pos.width as i32 + pos.top - min_y) as u32;
|
||||
let pixel = &mut image[(x, y)].0;
|
||||
// TODO: no clue if proper alpha blending should be done
|
||||
*pixel = Simd::from(color).saturating_add(Simd::from(*pixel)).into();
|
||||
};
|
||||
|
||||
match img.content {
|
||||
SwashContent::Mask => {
|
||||
for (i, a) in img.data.iter().enumerate() {
|
||||
let mut color = color.as_rgba();
|
||||
color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8;
|
||||
merge(i, color);
|
||||
}
|
||||
}
|
||||
SwashContent::SubpixelMask => todo!("subpixel mask text rendering"),
|
||||
SwashContent::Color => {
|
||||
let (colors, _) = img.data.as_chunks::<4>();
|
||||
for (i, color) in colors.iter().enumerate() {
|
||||
merge(i, *color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let max_dim = 8192;
|
||||
if image.width() > max_dim || image.height() > max_dim {
|
||||
let width = image.width().min(max_dim);
|
||||
let height = image.height().min(max_dim);
|
||||
eprintln!(
|
||||
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
|
||||
image.dimensions(),
|
||||
(width, height)
|
||||
);
|
||||
image = image.view(0, 0, width, height).to_image();
|
||||
}
|
||||
|
||||
RenderedText {
|
||||
glyphs,
|
||||
size: buffer.size(),
|
||||
color: attrs.color,
|
||||
handle: textures.add(image),
|
||||
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
|
||||
size: Vec2::new(max_width, height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub handle: TextureHandle,
|
||||
pub top_left_offset: Vec2,
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
pub trait HasTextures {
|
||||
fn add_texture(&mut self, image: DynamicImage) -> TextureHandle;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::util::{RefCounter, Vec2};
|
||||
use crate::{
|
||||
render::TexturePrimitive,
|
||||
util::{RefCounter, Vec2},
|
||||
};
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use std::{
|
||||
ops::Index,
|
||||
@@ -7,7 +10,7 @@ use std::{
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureHandle {
|
||||
slot: u32,
|
||||
inner: TexturePrimitive,
|
||||
size: Vec2,
|
||||
counter: RefCounter,
|
||||
send: Sender<u32>,
|
||||
@@ -26,26 +29,14 @@ pub struct Textures {
|
||||
pub enum TextureUpdate<'a> {
|
||||
Push(&'a DynamicImage),
|
||||
Set(u32, &'a DynamicImage),
|
||||
Patch(u32, PatchRect, &'a DynamicImage),
|
||||
Free(u32),
|
||||
/// Added and freed before the renderer drained either update. It still has
|
||||
/// to push a slot to stay lined up with `images`; `Free` then empties it.
|
||||
PushFree,
|
||||
SetFree,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatchRect {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(u32),
|
||||
Set(u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
|
||||
@@ -63,8 +54,14 @@ impl Textures {
|
||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
let view_idx = self.push(image);
|
||||
// 0 == default in renderer; TODO: actually create samplers here
|
||||
let sampler_idx = 0;
|
||||
TextureHandle {
|
||||
slot: self.push(image),
|
||||
inner: TexturePrimitive {
|
||||
view_idx,
|
||||
sampler_idx,
|
||||
},
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
@@ -84,23 +81,6 @@ impl Textures {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
||||
self.images[handle.slot as usize]
|
||||
.as_mut()
|
||||
.expect("texture was freed while still held")
|
||||
}
|
||||
|
||||
/// Queue an upload of just `rect`, after writing it with `image_mut`.
|
||||
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
||||
self.updates.push(Update::Patch(handle.slot, rect));
|
||||
}
|
||||
|
||||
/// How many textures are live, which is what a ui can ask; the renderer's
|
||||
/// copies follow from the updates it drains.
|
||||
pub fn count(&self) -> usize {
|
||||
self.images.iter().flatten().count()
|
||||
}
|
||||
|
||||
pub fn free(&mut self) {
|
||||
for idx in self.recv.try_iter() {
|
||||
self.images[idx as usize] = None;
|
||||
@@ -119,19 +99,14 @@ impl Textures {
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Patch(i, rect, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Free(i) => TextureUpdate::Free(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureHandle {
|
||||
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||
pub fn slot(&self) -> u32 {
|
||||
self.slot
|
||||
pub fn primitive(&self) -> TexturePrimitive {
|
||||
self.inner
|
||||
}
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.size
|
||||
@@ -141,7 +116,7 @@ impl TextureHandle {
|
||||
impl Drop for TextureHandle {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.slot);
|
||||
let _ = self.send.send(self.inner.view_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +125,7 @@ impl Index<&TextureHandle> for Textures {
|
||||
type Output = DynamicImage;
|
||||
|
||||
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
||||
self.images[index.slot as usize].as_ref().unwrap()
|
||||
self.images[index.inner.view_idx as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
use crate::{
|
||||
PatchRect,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||
pub(crate) const PAGE: u32 = 1024;
|
||||
|
||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||
/// pick up its neighbour along a shared edge.
|
||||
const PAD: u32 = 1;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphKey {
|
||||
pub font: u64,
|
||||
pub glyph: u32,
|
||||
/// Font size in 1/16 px, so sizes that round to the same pixels share a
|
||||
/// raster instead of filling the atlas with near-duplicates.
|
||||
pub size: u32,
|
||||
/// Horizontal subpixel phase, in 1/4 px.
|
||||
pub subpixel: u8,
|
||||
/// Hash of the variation coordinates; a variable font at two weights is two
|
||||
/// different sets of pixels from one glyph id.
|
||||
pub coords: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GlyphEntry {
|
||||
pub uv_min: Vec2,
|
||||
pub uv_max: Vec2,
|
||||
/// Offset from the glyph's pen position to the top-left of its pixels.
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_colored: bool,
|
||||
/// Which atlas array layer this glyph is on.
|
||||
pub layer: u32,
|
||||
}
|
||||
|
||||
impl GlyphEntry {
|
||||
const IS_COLORED: u32 = 1;
|
||||
|
||||
pub(crate) fn flags(&self) -> u32 {
|
||||
if self.is_colored { Self::IS_COLORED } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct Page {
|
||||
image: RgbaImage,
|
||||
x: u32,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
/// A rectangle of one page the renderer has not uploaded yet.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PageUpload {
|
||||
pub layer: u32,
|
||||
pub rect: PatchRect,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
uploads: Vec<PageUpload>,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.entries.get(key).copied()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
log::warn!(
|
||||
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
|
||||
key.glyph,
|
||||
key.font,
|
||||
);
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
|
||||
log::warn!(
|
||||
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
|
||||
key.glyph,
|
||||
key.font,
|
||||
);
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
|
||||
let upload = self.allocate(w, h);
|
||||
let PatchRect { x, y, .. } = upload.rect;
|
||||
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
|
||||
self.uploads.push(upload);
|
||||
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
|
||||
left: image.placement.left,
|
||||
top: image.placement.top,
|
||||
width: w,
|
||||
height: h,
|
||||
is_colored: matches!(image.content, Content::Color),
|
||||
layer: upload.layer,
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
|
||||
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
|
||||
let rect = |x, y| PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
if let Some((i, (x, y))) = self
|
||||
.pages
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
||||
{
|
||||
return PageUpload {
|
||||
layer: i as u32,
|
||||
rect: rect(x, y),
|
||||
};
|
||||
}
|
||||
|
||||
self.pages.push(Page {
|
||||
image: RgbaImage::new(PAGE, PAGE),
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
PageUpload {
|
||||
layer: self.pages.len() as u32 - 1,
|
||||
rect: rect(PAD, PAD),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains what has been written since the last call, for the renderer to
|
||||
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
|
||||
/// layer transparent, which is what an atlas wants.
|
||||
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
||||
let pages = &self.pages;
|
||||
self.uploads
|
||||
.drain(..)
|
||||
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
||||
}
|
||||
|
||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
|
||||
let need_w = w + PAD;
|
||||
let need_h = h + PAD;
|
||||
if self.x + need_w > PAGE {
|
||||
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
|
||||
return None;
|
||||
}
|
||||
self.y += self.shelf_height;
|
||||
self.x = PAD;
|
||||
self.shelf_height = 0;
|
||||
} else if self.y + need_h > PAGE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let position = (self.x, self.y);
|
||||
self.x += need_w;
|
||||
self.shelf_height = self.shelf_height.max(need_h);
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
|
||||
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||
let width = image.placement.width as usize;
|
||||
let height = image.placement.height as usize;
|
||||
let page_stride = page.width() as usize * 4;
|
||||
let x = x as usize * 4;
|
||||
let y = y as usize;
|
||||
let page = page.as_mut();
|
||||
|
||||
for row in 0..height {
|
||||
let start = (y + row) * page_stride + x;
|
||||
let target = &mut page[start..start + width * 4];
|
||||
match image.content {
|
||||
Content::Color => {
|
||||
let start = row * width * 4;
|
||||
target.copy_from_slice(&image.data[start..start + width * 4]);
|
||||
}
|
||||
Content::Mask => {
|
||||
let start = row * width;
|
||||
for (target, &alpha) in target
|
||||
.as_chunks_mut::<4>()
|
||||
.0
|
||||
.iter_mut()
|
||||
.zip(&image.data[start..start + width])
|
||||
{
|
||||
target.copy_from_slice(&[255, 255, 255, alpha]);
|
||||
}
|
||||
}
|
||||
Content::SubpixelMask => {
|
||||
let start = row * width * 4;
|
||||
for (target, source) in target
|
||||
.as_chunks_mut::<4>()
|
||||
.0
|
||||
.iter_mut()
|
||||
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
|
||||
{
|
||||
target.copy_from_slice(&[255, 255, 255, source[1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PlacedGlyph {
|
||||
pub entry: GlyphEntry,
|
||||
pub offset: Vec2,
|
||||
}
|
||||
@@ -12,16 +12,20 @@ pub struct WindowUniform {
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UiRegion,
|
||||
pub binding: u32,
|
||||
pub idx: u32,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 5] = 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() -> VertexBufferLayout<'static> {
|
||||
|
||||
+198
-212
@@ -1,82 +1,61 @@
|
||||
use std::num::NonZero;
|
||||
|
||||
use crate::{
|
||||
UiData, UiRenderState,
|
||||
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||
util::{HashMap, Vec2},
|
||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
||||
util::HashMap,
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod page;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
pub struct UiRenderNode {
|
||||
shared_layout: BindGroupLayout,
|
||||
shared_group: BindGroup,
|
||||
format: TextureFormat,
|
||||
uniform_group: BindGroup,
|
||||
primitive_layout: BindGroupLayout,
|
||||
rsc_layout: BindGroupLayout,
|
||||
rsc_group: BindGroup,
|
||||
|
||||
/// One per registered primitive, in id order.
|
||||
primitives: Vec<PrimitivePipeline>,
|
||||
pipeline: RenderPipeline,
|
||||
|
||||
layers: HashMap<usize, RenderLayer>,
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
textures: GpuTextures,
|
||||
masks: ArrBuf<Mask>,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
/// One per registered primitive, `None` where this layer draws none.
|
||||
primitives: Vec<Option<ListBuffers>>,
|
||||
}
|
||||
|
||||
/// What draws one registered primitive.
|
||||
struct PrimitivePipeline {
|
||||
data_layout: BindGroupLayout,
|
||||
pipeline: RenderPipeline,
|
||||
render: Box<dyn PrimitiveRender>,
|
||||
}
|
||||
|
||||
/// One list's vertex buffer and the data its shader reads.
|
||||
struct ListBuffers {
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
data: ArrBuf<u8>,
|
||||
group: Option<BindGroup>,
|
||||
/// What the primitive asked to keep per instance, if anything.
|
||||
bindings: Vec<u32>,
|
||||
primitives: PrimitiveBuffers,
|
||||
primitive_group: BindGroup,
|
||||
}
|
||||
|
||||
impl UiRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
||||
for i in &self.active {
|
||||
let layer = &self.layers[i];
|
||||
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);
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||
primitive.render.draw(
|
||||
pass,
|
||||
ListDraw {
|
||||
instances: list.instance.len() as u32,
|
||||
bindings: &list.bindings,
|
||||
},
|
||||
);
|
||||
if layer.instance.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
pass.set_bind_group(1, &layer.primitive_group, &[]);
|
||||
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
|
||||
pass.draw(0..4, 0..layer.instance.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,146 +66,145 @@ impl UiRenderNode {
|
||||
ui: &mut UiData,
|
||||
ui_render: &mut UiRenderState,
|
||||
) {
|
||||
// Before the layers: each list is given its pipeline's data layout.
|
||||
self.build_pipelines(device, queue, &ui.primitives);
|
||||
self.active.clear();
|
||||
for (i, draws) in ui_render.layers.iter_mut() {
|
||||
for (i, primitives) in ui_render.layers.iter_mut() {
|
||||
self.active.push(i);
|
||||
for change in draws.apply_free() {
|
||||
for change in primitives.apply_free() {
|
||||
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||
for h in &mut inst.primitives {
|
||||
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||
if h.layer == i && h.inst_idx == change.old {
|
||||
h.inst_idx = change.new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
||||
if draws.updated {
|
||||
let lists = draws.primitives();
|
||||
// The zip would otherwise skip a list with no pipeline.
|
||||
assert!(lists.len() <= self.primitives.len());
|
||||
rlayer.primitives.resize_with(lists.len(), || None);
|
||||
for ((buffers, list), primitive) in rlayer
|
||||
.primitives
|
||||
.iter_mut()
|
||||
.zip(lists)
|
||||
.zip(&self.primitives)
|
||||
{
|
||||
let Some(list) = list else {
|
||||
continue;
|
||||
};
|
||||
buffers
|
||||
.get_or_insert_with(|| ListBuffers::new(device))
|
||||
.update(device, queue, primitive, list);
|
||||
let rlayer = self.layers.entry(i).or_insert_with(|| {
|
||||
let primitives = PrimitiveBuffers::new(device);
|
||||
let primitive_group =
|
||||
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
|
||||
RenderLayer {
|
||||
instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"instance",
|
||||
),
|
||||
primitives,
|
||||
primitive_group,
|
||||
}
|
||||
draws.updated = false;
|
||||
});
|
||||
if primitives.updated {
|
||||
rlayer
|
||||
.instance
|
||||
.update(device, queue, primitives.instances());
|
||||
rlayer.primitives.update(device, queue, primitives.data());
|
||||
rlayer.primitive_group = Self::primitive_group(
|
||||
device,
|
||||
&self.primitive_layout,
|
||||
rlayer.primitives.buffers(),
|
||||
);
|
||||
primitives.updated = false;
|
||||
}
|
||||
}
|
||||
for primitive in &mut self.primitives {
|
||||
primitive.render.update(ui);
|
||||
}
|
||||
let mut changed = false;
|
||||
changed |= self.textures.update(&mut ui.textures);
|
||||
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,
|
||||
);
|
||||
}
|
||||
self.masks.update(device, queue, &ui.masks[..]);
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
let size = size.into();
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
|
||||
let slice = &[WindowUniform {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
width: size.width as f32,
|
||||
height: size.height as f32,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||
let window_uniform = WindowUniform {
|
||||
width: config.width as f32,
|
||||
height: config.height as f32,
|
||||
};
|
||||
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()),
|
||||
});
|
||||
|
||||
let window_uniform = WindowUniform::default();
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("window"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let shared_layout = Self::shared_layout(device);
|
||||
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: Some("window"),
|
||||
});
|
||||
|
||||
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| {
|
||||
BindGroupLayoutEntry {
|
||||
binding: i as u32,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}
|
||||
}),
|
||||
label: Some("primitive"),
|
||||
});
|
||||
|
||||
let tex_manager = GpuTextures::new(device, queue);
|
||||
let masks = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||
|
||||
Self {
|
||||
shared_layout,
|
||||
shared_group,
|
||||
format: config.format,
|
||||
primitives: Vec::new(),
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
masks,
|
||||
}
|
||||
}
|
||||
let rsc_layout = Self::rsc_layout(device, &limits);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
|
||||
|
||||
/// Compiles a pipeline for every primitive registered since the last call.
|
||||
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
|
||||
for source in ®istry.sources()[self.primitives.len()..] {
|
||||
let render = (source.render)(device, queue);
|
||||
let data_layout = Self::data_layout(device, source.stride);
|
||||
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
||||
groups.extend(render.layout().map(Some));
|
||||
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,
|
||||
render,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
format: TextureFormat,
|
||||
wgsl: &str,
|
||||
label: &str,
|
||||
) -> RenderPipeline {
|
||||
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout].map(Some),
|
||||
immediate_size: 0,
|
||||
});
|
||||
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &module,
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(PrimitiveInstance::desc())],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &module,
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format,
|
||||
format: config.format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
@@ -249,42 +227,90 @@ impl UiRenderNode {
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
uniform_group,
|
||||
primitive_layout,
|
||||
rsc_layout,
|
||||
rsc_group,
|
||||
pipeline,
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
textures: tex_manager,
|
||||
masks,
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_group_0(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window_buffer: &Buffer,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window_buffer.as_entire_binding(),
|
||||
}],
|
||||
label: Some("ui window"),
|
||||
})
|
||||
}
|
||||
|
||||
/// What every draw in the ui is given: the window and the masks.
|
||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||
fn primitive_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
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::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Texture {
|
||||
sample_type: TextureSampleType::Float { filterable: false },
|
||||
view_dimension: TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
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()),
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
}
|
||||
|
||||
fn shared_group(
|
||||
fn rsc_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window: &Buffer,
|
||||
tex_manager: &GpuTextures,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
@@ -292,85 +318,45 @@ impl UiRenderNode {
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window.as_entire_binding(),
|
||||
resource: BindingResource::TextureViewArray(&tex_manager.views()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
label: Some("ui rsc"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Layout for a list of one primitive's data. Every size in the ui is
|
||||
/// stated, so "is the buffer big enough for one entry?" is answered when
|
||||
/// the bind group is made; a `None` size is wgpu's to check on every draw.
|
||||
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
|
||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(stride),
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
label: Some("ui primitive data"),
|
||||
})
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.textures.view_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderLayer {
|
||||
fn new() -> Self {
|
||||
pub struct UiLimits {
|
||||
max_textures: u32,
|
||||
max_samplers: u32,
|
||||
}
|
||||
|
||||
impl Default for UiLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
max_textures: 100000,
|
||||
max_samplers: 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListBuffers {
|
||||
fn new(device: &Device) -> Self {
|
||||
Self {
|
||||
instance: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"instance",
|
||||
),
|
||||
data: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"primitive data",
|
||||
),
|
||||
group: None,
|
||||
bindings: Vec::new(),
|
||||
}
|
||||
impl UiLimits {
|
||||
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
|
||||
self.max_textures + self.max_samplers
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
primitive: &PrimitivePipeline,
|
||||
list: &InstanceList,
|
||||
) {
|
||||
self.bindings.clear();
|
||||
primitive.render.instance_bindings(list, &mut self.bindings);
|
||||
self.instance.update(device, queue, list.instances());
|
||||
let resized = self.data.update(device, queue, list.data());
|
||||
if list.instances().is_empty() {
|
||||
self.group = None;
|
||||
} else if resized || self.group.is_none() {
|
||||
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: &primitive.data_layout,
|
||||
entries: &[BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self.data.buffer.as_entire_binding(),
|
||||
}],
|
||||
label: Some("ui primitive data"),
|
||||
}));
|
||||
}
|
||||
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
|
||||
self.max_samplers
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use wgpu::*;
|
||||
|
||||
use crate::{GlyphAtlas, UiData};
|
||||
|
||||
use super::{
|
||||
atlas::PAGE,
|
||||
primitive::{ListDraw, PrimitiveRender},
|
||||
texture::{default_sampler, sampled_group, sampled_layout, write_region},
|
||||
};
|
||||
|
||||
/// Draws glyphs from the atlas, which it owns: one array texture bound once
|
||||
/// for a whole list, since every glyph in it reads the same pages.
|
||||
pub struct GlyphRender {
|
||||
pages: GpuPages,
|
||||
layout: BindGroupLayout,
|
||||
sampler: Sampler,
|
||||
}
|
||||
|
||||
impl GlyphRender {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
|
||||
let sampler = default_sampler(device);
|
||||
Self {
|
||||
pages: GpuPages::new(device, queue, &layout, &sampler),
|
||||
layout,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveRender for GlyphRender {
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
Some(&self.layout)
|
||||
}
|
||||
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
self.pages
|
||||
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||
pass.draw(0..4, 0..list.instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||
/// `GlyphAtlas` packs.
|
||||
///
|
||||
/// One array rather than a texture per page because a layer index is ordinary
|
||||
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
|
||||
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
||||
pub struct GpuPages {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
}
|
||||
|
||||
impl GpuPages {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> Self {
|
||||
let texture = create_array(device, 1);
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
group: atlas_group(device, layout, &texture, sampler),
|
||||
texture,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
for (upload, page) in atlas.uploads() {
|
||||
let dst = TexelCopyTextureInfo {
|
||||
texture: &self.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: upload.rect.x,
|
||||
y: upload.rect.y,
|
||||
z: upload.layer,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
write_region(&self.queue, dst, page, upload.rect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group(&self) -> &BindGroup {
|
||||
&self.group
|
||||
}
|
||||
|
||||
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||
/// The new texture stales the group, so that is rebuilt here.
|
||||
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
let old = self.texture.depth_or_array_layers();
|
||||
let mut layers = old;
|
||||
while layers < needed {
|
||||
layers *= 2;
|
||||
}
|
||||
let texture = create_array(&self.device, layers);
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("atlas grow"),
|
||||
});
|
||||
encoder.copy_texture_to_texture(
|
||||
self.texture.as_image_copy(),
|
||||
texture.as_image_copy(),
|
||||
Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: old,
|
||||
},
|
||||
);
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.group = atlas_group(&self.device, layout, &texture, sampler);
|
||||
self.texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
fn atlas_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
texture: &Texture,
|
||||
sampler: &Sampler,
|
||||
) -> BindGroup {
|
||||
let view = texture.create_view(&TextureViewDescriptor {
|
||||
dimension: Some(TextureViewDimension::D2Array),
|
||||
..Default::default()
|
||||
});
|
||||
sampled_group(device, layout, &view, sampler, "ui atlas")
|
||||
}
|
||||
|
||||
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||
device.create_texture(&TextureDescriptor {
|
||||
label: Some("glyph atlas"),
|
||||
size: Extent3d {
|
||||
width: PAGE,
|
||||
height: PAGE,
|
||||
depth_or_array_layers: layers,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
+195
-308
@@ -1,247 +1,114 @@
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{
|
||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||
Color, UiRegion, WidgetId,
|
||||
render::{
|
||||
ArrBuf,
|
||||
data::{MaskIdx, PrimitiveInstance},
|
||||
page::GlyphRender,
|
||||
texture::ImageRender,
|
||||
},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
|
||||
use wgpu::*;
|
||||
|
||||
/// One instance of a primitive, laid out as the struct its shader reads.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// Made once, the first time the renderer sees this primitive. It owns
|
||||
/// whatever the shader samples and records the primitive's own draws; the
|
||||
/// default owns nothing and draws every instance in one call.
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let _ = (device, queue);
|
||||
Box::new(Instanced)
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer's half of a primitive: what it samples, what it uploads, and
|
||||
/// what draws it records.
|
||||
///
|
||||
/// Everything a draw shares -- the pipeline, the window and masks, the list's
|
||||
/// own data and instance buffer -- is set before this is called. What is left
|
||||
/// is what only this primitive knows: its group 2, and how many draws its
|
||||
/// instances are.
|
||||
pub trait PrimitiveRender {
|
||||
/// The layout its shader reads at group 2. `None` for a primitive whose
|
||||
/// shader samples nothing, whose pipeline then has no group 2 at all.
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Uploads whatever this primitive owns, once a frame, before any draw.
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
let _ = ui;
|
||||
}
|
||||
|
||||
/// Keeps what the primitive needs per instance at draw time, read from
|
||||
/// the list's own data. A primitive that binds nothing per instance --
|
||||
/// most of them -- leaves this empty and draws in one call.
|
||||
fn instance_bindings(&self, list: &InstanceList, out: &mut Vec<u32>) {
|
||||
let _ = (list, out);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>);
|
||||
}
|
||||
|
||||
/// What a `PrimitiveRender` draws: this list's instances, and whatever
|
||||
/// `instance_bindings` kept for them.
|
||||
pub struct ListDraw<'a> {
|
||||
pub instances: u32,
|
||||
pub bindings: &'a [u32],
|
||||
}
|
||||
|
||||
/// The default: nothing sampled, every instance in one call.
|
||||
pub struct Instanced;
|
||||
|
||||
impl PrimitiveRender for Instanced {
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
pass.draw(0..4, 0..list.instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which registered primitive an instance is.
|
||||
pub struct PrimitiveKind<P> {
|
||||
id: u32,
|
||||
_p: PhantomData<fn(P)>,
|
||||
}
|
||||
|
||||
impl<P> PrimitiveKind<P> {
|
||||
fn new(id: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Clone for PrimitiveKind<P> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Copy for PrimitiveKind<P> {}
|
||||
|
||||
/// Every primitive a ui can draw, in the order they were first drawn.
|
||||
#[derive(Default)]
|
||||
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, stated as the data binding's minimum.
|
||||
pub stride: u64,
|
||||
pub render: fn(&Device, &Queue) -> Box<dyn PrimitiveRender>,
|
||||
}
|
||||
|
||||
impl PrimitiveRegistry {
|
||||
/// 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,
|
||||
render: P::render,
|
||||
});
|
||||
kinds.len() as u32 - 1
|
||||
});
|
||||
PrimitiveKind::new(id)
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[PrimitiveSource] {
|
||||
&self.kinds
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub struct Primitives {
|
||||
instances: Vec<PrimitiveInstance>,
|
||||
/// The widget each instance belongs to, for renumbering its handles.
|
||||
assoc: Vec<WidgetId>,
|
||||
data: PrimitiveData,
|
||||
free: Vec<usize>,
|
||||
/// `stride` bytes of the primitive's own data per instance.
|
||||
data: Vec<u8>,
|
||||
/// From the type the list was made for, so a write is never checked.
|
||||
stride: usize,
|
||||
}
|
||||
|
||||
impl InstanceList {
|
||||
fn new<P: Primitive>() -> Self {
|
||||
Self {
|
||||
instances: Vec::new(),
|
||||
assoc: Vec::new(),
|
||||
free: Vec::new(),
|
||||
data: Vec::new(),
|
||||
stride: size_of::<P>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> &[PrimitiveInstance] {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn stride(&self) -> usize {
|
||||
self.stride
|
||||
}
|
||||
|
||||
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> 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);
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
self.data.extend_from_slice(data);
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
fn free(&mut self, i: usize) -> MaskIdx {
|
||||
self.free.push(i);
|
||||
self.instances[i].mask_idx
|
||||
}
|
||||
|
||||
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.free.sort_by(|a, b| b.cmp(a));
|
||||
let instances = &mut self.instances;
|
||||
let assoc = &mut self.assoc;
|
||||
let data = &mut self.data;
|
||||
let stride = self.stride;
|
||||
self.free.drain(..).filter_map(move |i| {
|
||||
instances.swap_remove(i);
|
||||
assoc.swap_remove(i);
|
||||
let last = instances.len();
|
||||
data.copy_within(last * stride..(last + 1) * stride, i * stride);
|
||||
data.truncate(last * stride);
|
||||
if i == last {
|
||||
return None;
|
||||
}
|
||||
let id = assoc[i];
|
||||
Some(PrimitiveChange {
|
||||
id,
|
||||
kind,
|
||||
old: last,
|
||||
new: i,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
primitives: Vec<Option<InstanceList>>,
|
||||
pub updated: bool,
|
||||
}
|
||||
|
||||
impl Default for LayerDraws {
|
||||
impl Default for Primitives {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
instances: Default::default(),
|
||||
assoc: Default::default(),
|
||||
data: Default::default(),
|
||||
free: Vec::new(),
|
||||
updated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerDraws {
|
||||
pub trait Primitive: Pod {
|
||||
const BINDING: u32;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
|
||||
}
|
||||
|
||||
macro_rules! primitives {
|
||||
($($name:ident: $ty:ty => $binding:expr,)*) => {
|
||||
#[derive(Default)]
|
||||
pub struct PrimitiveData {
|
||||
$(pub(crate) $name: PrimitiveVec<$ty>,)*
|
||||
}
|
||||
|
||||
pub struct PrimitiveBuffers {
|
||||
$($name: ArrBuf<$ty>,)*
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
|
||||
$(self.$name.update(device, queue, &data.$name);)*
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
pub const LEN: usize = primitives!(@count $($name)*);
|
||||
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
||||
[
|
||||
$((<$ty>::BINDING, &self.$name.buffer),)*
|
||||
]
|
||||
}
|
||||
pub fn new(device: &Device) -> Self {
|
||||
Self {
|
||||
$($name: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
stringify!($name),
|
||||
),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveData {
|
||||
pub fn clear(&mut self) {
|
||||
$(self.$name.clear();)*
|
||||
}
|
||||
pub fn free(&mut self, binding: u32, idx: usize) {
|
||||
match binding {
|
||||
$(<$ty>::BINDING => self.$name.free(idx),)*
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
unsafe impl bytemuck::Pod for $ty {}
|
||||
unsafe impl bytemuck::Zeroable for $ty {}
|
||||
impl Primitive for $ty {
|
||||
const BINDING: u32 = $binding;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
|
||||
&mut data.$name
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
|
||||
(@count $t:tt) => { 1 };
|
||||
}
|
||||
|
||||
pub struct PrimitiveInst<P> {
|
||||
pub id: WidgetId,
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
impl Primitives {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id,
|
||||
primitive,
|
||||
region,
|
||||
@@ -249,67 +116,65 @@ impl LayerDraws {
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
// Grown on first use rather than sized from the registry, which a
|
||||
// layer cannot see.
|
||||
if self.primitives.len() <= kind.id as usize {
|
||||
self.primitives.resize_with(kind.id as usize + 1, || None);
|
||||
}
|
||||
let inst_idx = self.primitives[kind.id as usize]
|
||||
.get_or_insert_with(InstanceList::new::<P>)
|
||||
.push(
|
||||
id,
|
||||
PrimitiveInstance { region, mask_idx },
|
||||
bytemuck::bytes_of(&primitive),
|
||||
);
|
||||
PrimitiveHandle {
|
||||
layer,
|
||||
kind: kind.id,
|
||||
inst_idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primitives(&self) -> &[Option<InstanceList>] {
|
||||
&self.primitives
|
||||
let vec = P::vec(&mut self.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() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
i
|
||||
};
|
||||
PrimitiveHandle::new::<P>(layer, inst_i, i)
|
||||
}
|
||||
|
||||
/// returns (old index, new index)
|
||||
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.primitives
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
|
||||
.flat_map(|(kind, list)| list.apply_free(kind))
|
||||
self.free.sort_by(|a, b| b.cmp(a));
|
||||
self.free.drain(..).filter_map(|i| {
|
||||
self.instances.swap_remove(i);
|
||||
self.assoc.swap_remove(i);
|
||||
if i == self.instances.len() {
|
||||
return None;
|
||||
}
|
||||
let id = self.assoc[i];
|
||||
let old = self.instances.len();
|
||||
Some(PrimitiveChange { id, old, new: i })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self.updated = true;
|
||||
self.list(h).free(h.inst_idx)
|
||||
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 {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||
self.updated = true;
|
||||
&mut self.list(h).instances[h.inst_idx].region
|
||||
&mut self.instances[h.inst_idx].region
|
||||
}
|
||||
|
||||
/// A handle is only ever made by `write`, which is what created the list.
|
||||
fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList {
|
||||
self.primitives[h.kind as usize]
|
||||
.as_mut()
|
||||
.expect("handle names a primitive this layer never drew")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveInst<P> {
|
||||
pub kind: PrimitiveKind<P>,
|
||||
pub id: WidgetId,
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
pub struct PrimitiveChange {
|
||||
pub id: WidgetId,
|
||||
/// Which registered primitive's list moved, since they index separately.
|
||||
pub kind: u32,
|
||||
pub old: usize,
|
||||
pub new: usize,
|
||||
}
|
||||
@@ -317,12 +182,29 @@ pub struct PrimitiveChange {
|
||||
#[derive(Debug)]
|
||||
pub struct PrimitiveHandle {
|
||||
pub layer: usize,
|
||||
pub kind: u32,
|
||||
pub inst_idx: usize,
|
||||
pub data_idx: usize,
|
||||
pub binding: u32,
|
||||
}
|
||||
|
||||
impl PrimitiveHandle {
|
||||
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
|
||||
Self {
|
||||
layer,
|
||||
inst_idx,
|
||||
data_idx,
|
||||
binding: P::BINDING,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
textures: TexturePrimitive => 1,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RectPrimitive {
|
||||
pub color: Color<u8>,
|
||||
pub radius: f32,
|
||||
@@ -330,10 +212,6 @@ pub struct RectPrimitive {
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl Primitive for RectPrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/rect.wgsl");
|
||||
}
|
||||
|
||||
impl RectPrimitive {
|
||||
pub fn color(color: Color<u8>) -> Self {
|
||||
Self {
|
||||
@@ -345,51 +223,60 @@ impl RectPrimitive {
|
||||
}
|
||||
}
|
||||
|
||||
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: Vec2,
|
||||
pub uv_max: Vec2,
|
||||
/// Which atlas array layer this glyph is on.
|
||||
pub layer: u32,
|
||||
pub color: Color<u8>,
|
||||
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 {
|
||||
const WGSL: &'static str = include_str!("shader/glyph.wgsl");
|
||||
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||
Box::new(GlyphRender::new(device, queue))
|
||||
}
|
||||
}
|
||||
|
||||
/// One drawn image. Its shader reads nothing per instance; the slot names the
|
||||
/// texture to bind for it.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TexturePrimitive {
|
||||
pub slot: u32,
|
||||
pub view_idx: u32,
|
||||
pub sampler_idx: u32,
|
||||
}
|
||||
|
||||
impl Primitive for TexturePrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/texture.wgsl");
|
||||
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||
Box::new(ImageRender::new(device, queue))
|
||||
}
|
||||
pub struct PrimitiveVec<T> {
|
||||
vec: Vec<T>,
|
||||
free: Vec<usize>,
|
||||
}
|
||||
|
||||
impl From<&TextureHandle> for TexturePrimitive {
|
||||
fn from(handle: &TextureHandle) -> Self {
|
||||
impl<T> PrimitiveVec<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slot: handle.slot(),
|
||||
vec: Vec::new(),
|
||||
free: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn add(&mut self, t: T) -> usize {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.vec[i] = t;
|
||||
i
|
||||
} else {
|
||||
let i = self.vec.len();
|
||||
self.vec.push(t);
|
||||
i
|
||||
}
|
||||
}
|
||||
pub fn free(&mut self, i: usize) {
|
||||
self.free.push(i);
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.free.clear();
|
||||
self.vec.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for PrimitiveVec<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for PrimitiveVec<T> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.vec
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for PrimitiveVec<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.vec
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
enable wgpu_binding_array;
|
||||
|
||||
const RECT: u32 = 0u;
|
||||
const TEXTURE: u32 = 1u;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(1) @binding(RECT)
|
||||
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,
|
||||
}
|
||||
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
}
|
||||
|
||||
struct UiSpan {
|
||||
start: UiScalar,
|
||||
end: UiScalar,
|
||||
}
|
||||
|
||||
struct UiScalar {
|
||||
rel: f32,
|
||||
abs: f32,
|
||||
}
|
||||
|
||||
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>,
|
||||
};
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) x_start: vec2<f32>,
|
||||
@location(1) x_end: vec2<f32>,
|
||||
@location(2) y_start: vec2<f32>,
|
||||
@location(3) y_end: vec2<f32>,
|
||||
@location(4) binding: u32,
|
||||
@location(5) idx: u32,
|
||||
@location(6) mask_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) top_left: vec2<f32>,
|
||||
@location(1) bot_right: vec2<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) @interpolate(flat) binding: u32,
|
||||
@location(4) @interpolate(flat) idx: u32,
|
||||
@location(5) @interpolate(flat) mask_idx: u32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Region {
|
||||
pos: vec2<f32>,
|
||||
uv: vec2<f32>,
|
||||
top_left: vec2<f32>,
|
||||
bot_right: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
in: InstanceInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||
|
||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
);
|
||||
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;
|
||||
out.bot_right = bot_right;
|
||||
out.mask_idx = in.mask_idx;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(
|
||||
in: VertexOutput
|
||||
) -> @location(0) vec4<f32> {
|
||||
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: {
|
||||
color = draw_rounded_rect(region, rects[i]);
|
||||
}
|
||||
case TEXTURE: {
|
||||
color = draw_texture(region, textures[i]);
|
||||
}
|
||||
default: {
|
||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
if in.mask_idx != 4294967295u {
|
||||
let mask = masks[in.mask_idx];
|
||||
let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs));
|
||||
let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs));
|
||||
|
||||
let top_left = floor(tl.rel * window.dim) + floor(tl.abs);
|
||||
let bot_right = floor(br.rel * window.dim) + floor(br.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)?
|
||||
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 = unpack4x8unorm(rect.color);
|
||||
|
||||
let edge = 0.5;
|
||||
|
||||
let size = region.bot_right - region.top_left;
|
||||
let corner = size / 2.0;
|
||||
let center = region.top_left + corner;
|
||||
|
||||
let dist = distance_from_rect(region.pos, center, corner, rect.radius);
|
||||
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||
|
||||
if rect.thickness > 0.0 {
|
||||
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
|
||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||
// vec from center to pixel
|
||||
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(0.0))) - radius;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Matches `GlyphEntry::IS_COLORED`.
|
||||
const COLORED: u32 = 1u;
|
||||
|
||||
// The glyph atlas, whose array layers are its pages.
|
||||
@group(2) @binding(0)
|
||||
var atlas: texture_2d_array<f32>;
|
||||
@group(2) @binding(1)
|
||||
var samp: sampler;
|
||||
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
// Which layer of the atlas array this glyph's page is.
|
||||
layer: u32,
|
||||
color: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
@fragment
|
||||
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(atlas, samp, uv, i32(g.layer));
|
||||
if (g.flags & COLORED) != 0u {
|
||||
return masked(in, texel);
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
color.a *= texel.a;
|
||||
return masked(in, color);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// 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. What it samples, if anything, is bound at group
|
||||
// 2: the texture at binding 0 and the sampler at binding 1.
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(0) @binding(1)
|
||||
var<storage> masks: array<Mask>;
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
};
|
||||
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
}
|
||||
|
||||
struct UiSpan {
|
||||
start: UiScalar,
|
||||
end: UiScalar,
|
||||
}
|
||||
|
||||
struct UiScalar {
|
||||
rel: f32,
|
||||
abs: f32,
|
||||
}
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) x_start: vec2<f32>,
|
||||
@location(1) x_end: vec2<f32>,
|
||||
@location(2) y_start: vec2<f32>,
|
||||
@location(3) y_end: vec2<f32>,
|
||||
@location(4) mask_idx: u32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) top_left: vec2<f32>,
|
||||
@location(1) bot_right: vec2<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) @interpolate(flat) mask_idx: u32,
|
||||
@location(4) @interpolate(flat) idx: u32,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@builtin(vertex_index) vi: u32,
|
||||
@builtin(instance_index) ii: u32,
|
||||
in: InstanceInput,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
|
||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||
|
||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||
let size = bot_right - top_left;
|
||||
|
||||
let uv = vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
);
|
||||
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.top_left = top_left;
|
||||
out.bot_right = bot_right;
|
||||
out.mask_idx = in.mask_idx;
|
||||
out.idx = ii;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||
if in.mask_idx == 4294967295u {
|
||||
return color;
|
||||
}
|
||||
let mask = masks[in.mask_idx];
|
||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
||||
|
||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
||||
let pos = in.clip_position.xy;
|
||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||
return color * 0.0;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
struct Rect {
|
||||
color: u32,
|
||||
radius: f32,
|
||||
thickness: f32,
|
||||
inner_radius: f32,
|
||||
}
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<storage> rects: array<Rect>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let rect = rects[in.idx];
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
|
||||
let edge = 0.5;
|
||||
let size = in.bot_right - in.top_left;
|
||||
let corner = size / 2.0;
|
||||
let center = in.top_left + corner;
|
||||
let pos = in.clip_position.xy;
|
||||
|
||||
let dist = distance_from_rect(pos, center, corner, rect.radius);
|
||||
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||
|
||||
if rect.thickness > 0.0 {
|
||||
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
|
||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||
}
|
||||
|
||||
return masked(in, color);
|
||||
}
|
||||
|
||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||
// vec from center to pixel
|
||||
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(0.0))) - radius;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// The image this instance draws, bound for it alone.
|
||||
@group(2) @binding(0)
|
||||
var image: texture_2d<f32>;
|
||||
@group(2) @binding(1)
|
||||
var samp: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return masked(in, textureSample(image, samp, in.uv));
|
||||
}
|
||||
+84
-198
@@ -1,119 +1,59 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{
|
||||
PatchRect, TextureUpdate, Textures, UiData,
|
||||
render::{
|
||||
TexturePrimitive,
|
||||
primitive::{ListDraw, PrimitiveRender},
|
||||
},
|
||||
};
|
||||
use crate::{TextureUpdate, Textures};
|
||||
|
||||
/// Draws standalone images, which it owns. Each is its own texture, so each
|
||||
/// instance binds its own and is a draw of its own.
|
||||
pub struct ImageRender {
|
||||
textures: GpuTextures,
|
||||
layout: BindGroupLayout,
|
||||
sampler: Sampler,
|
||||
}
|
||||
|
||||
impl ImageRender {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
textures: GpuTextures::new(device, queue),
|
||||
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
|
||||
sampler: default_sampler(device),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveRender for ImageRender {
|
||||
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||
Some(&self.layout)
|
||||
}
|
||||
|
||||
fn update(&mut self, ui: &mut UiData) {
|
||||
self.textures
|
||||
.update(&mut ui.textures, &self.layout, &self.sampler);
|
||||
}
|
||||
|
||||
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
|
||||
let slots = list
|
||||
.data()
|
||||
.chunks_exact(list.stride())
|
||||
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
|
||||
out.extend(slots);
|
||||
}
|
||||
|
||||
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||
for (i, &slot) in list.bindings.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
queue: Queue,
|
||||
slots: Vec<Option<ImageGpu>>,
|
||||
}
|
||||
|
||||
struct ImageGpu {
|
||||
/// Kept for `patch`, which needs the texture rather than the view.
|
||||
texture: Texture,
|
||||
group: BindGroup,
|
||||
views: Vec<TextureView>,
|
||||
view_count: usize,
|
||||
samplers: Vec<Sampler>,
|
||||
null_view: TextureView,
|
||||
no_views: Vec<TextureView>,
|
||||
}
|
||||
|
||||
impl GpuTextures {
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
||||
let mut changed = false;
|
||||
for update in textures.updates() {
|
||||
changed = true;
|
||||
match update {
|
||||
TextureUpdate::Push(image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots.push(Some(image));
|
||||
}
|
||||
TextureUpdate::Set(i, image) => {
|
||||
let image = self.create(image, layout, sampler);
|
||||
self.slots[i as usize] = Some(image);
|
||||
}
|
||||
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||
TextureUpdate::PushFree => self.slots.push(None),
|
||||
TextureUpdate::SetFree => {}
|
||||
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||
TextureUpdate::Push(image) => self.push(image),
|
||||
TextureUpdate::Set(i, image) => self.set(i, image),
|
||||
TextureUpdate::SetFree => self.view_count += 1,
|
||||
TextureUpdate::Free(i) => self.free(i),
|
||||
TextureUpdate::PushFree => self.push_free(),
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
fn set(&mut self, i: u32, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let view = self.create_view(image);
|
||||
self.views[i as usize] = view;
|
||||
}
|
||||
fn free(&mut self, i: u32) {
|
||||
self.view_count -= 1;
|
||||
self.views[i as usize] = self.null_view.clone();
|
||||
}
|
||||
fn push(&mut self, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let view = self.create_view(image);
|
||||
self.views.push(view);
|
||||
}
|
||||
fn push_free(&mut self) {
|
||||
self.view_count += 1;
|
||||
self.views.push(self.null_view.clone());
|
||||
}
|
||||
|
||||
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||
}
|
||||
|
||||
fn create(
|
||||
&self,
|
||||
image: &DynamicImage,
|
||||
layout: &BindGroupLayout,
|
||||
sampler: &Sampler,
|
||||
) -> ImageGpu {
|
||||
let rgba = image.to_rgba8();
|
||||
let (width, height) = rgba.dimensions();
|
||||
fn create_view(&self, image: &DynamicImage) -> TextureView {
|
||||
let image = image.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let texture = self.device.create_texture_with_data(
|
||||
&self.queue,
|
||||
&TextureDescriptor {
|
||||
label: Some("image"),
|
||||
label: None,
|
||||
size: Extent3d {
|
||||
width,
|
||||
height,
|
||||
@@ -123,119 +63,65 @@ impl GpuTextures {
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
rgba.as_bytes(),
|
||||
image.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
|
||||
ImageGpu { texture, group }
|
||||
texture.create_view(&TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||
return;
|
||||
};
|
||||
let dst = TexelCopyTextureInfo {
|
||||
texture: &slot.texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: 0,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
match image.as_rgba8() {
|
||||
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
|
||||
// The texture is rgba8, so any other layout has to be converted --
|
||||
// and converting the rectangle is cheaper than the whole image.
|
||||
None => {
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
|
||||
}
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
let null_view = null_texture_view(device);
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
views: Vec::new(),
|
||||
samplers: vec![default_sampler(device)],
|
||||
no_views: vec![null_view.clone()],
|
||||
null_view,
|
||||
view_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
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 view_count(&self) -> usize {
|
||||
self.view_count
|
||||
}
|
||||
let stride = src.width() * 4;
|
||||
queue.write_texture(
|
||||
dst,
|
||||
src.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: (rect.y * stride + rect.x * 4) as u64,
|
||||
bytes_per_row: Some(stride),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// What a primitive that samples binds: a texture, and the sampler that reads
|
||||
/// it.
|
||||
pub fn sampled_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
label: &'static str,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(view),
|
||||
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,
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
label: Some(label),
|
||||
})
|
||||
}
|
||||
|
||||
/// The layout for one of those. The dimension differs -- the atlas is an
|
||||
/// array of pages and an image is not -- and nothing else does.
|
||||
pub fn sampled_layout(
|
||||
device: &Device,
|
||||
dimension: TextureViewDimension,
|
||||
label: &'static str,
|
||||
) -> 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: dimension,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some(label),
|
||||
})
|
||||
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 {
|
||||
|
||||
@@ -21,25 +21,17 @@ impl<T: Pod> ArrBuf<T> {
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||
/// `BindGroup` holding it.
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||
let resized = self.len != data.len();
|
||||
if resized {
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
|
||||
if self.len != data.len() {
|
||||
self.len = data.len();
|
||||
self.buffer =
|
||||
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
|
||||
}
|
||||
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
|
||||
resized
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
|
||||
let mut size = size as u64;
|
||||
if usage.contains(BufferUsages::STORAGE) {
|
||||
// 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 {
|
||||
@@ -49,4 +41,8 @@ impl<T: Pod> ArrBuf<T> {
|
||||
usage,
|
||||
})
|
||||
}
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
+3
-7
@@ -1,6 +1,4 @@
|
||||
use crate::{
|
||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
};
|
||||
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
|
||||
|
||||
mod active;
|
||||
mod cache;
|
||||
@@ -9,15 +7,13 @@ mod render_state;
|
||||
mod size;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use painter::Painter;
|
||||
pub use render_state::*;
|
||||
pub use size::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
/// Every primitive this ui can draw.
|
||||
pub primitives: PrimitiveRegistry,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
@@ -32,7 +28,7 @@ pub trait UiRsc {
|
||||
#[allow(unused_variables)]
|
||||
fn on_remove(&mut self, id: WidgetId) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_draw(&mut self, active: &ActiveData) {}
|
||||
fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_undraw(&mut self, active: &ActiveData) {}
|
||||
|
||||
|
||||
+21
-75
@@ -1,10 +1,7 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, Widget, WidgetId,
|
||||
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
@@ -24,26 +21,15 @@ pub struct Painter<'a> {
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Takes the kind, for a caller writing many of one primitive.
|
||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||
let h = self.state.layers.write(
|
||||
self.layer,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id: self.id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
},
|
||||
);
|
||||
self.push_primitive(h);
|
||||
}
|
||||
|
||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
@@ -52,13 +38,11 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
/// Writes a primitive to be rendered
|
||||
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
||||
let primitive = primitive.into_primitive(self);
|
||||
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
||||
let primitive = primitive.into_primitive(self);
|
||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.primitive_at(primitive, region.within(&self.region));
|
||||
}
|
||||
|
||||
@@ -91,38 +75,25 @@ impl<'a> Painter<'a> {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.render(buffer, attrs, width)
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.primitive_at(handle.primitive(), region.within(&self.region));
|
||||
}
|
||||
|
||||
// TODO: merge the text methods into the primitive ones.
|
||||
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;
|
||||
region.y.end = region.y.start;
|
||||
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.write(
|
||||
kind,
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
layer: glyph.entry.layer,
|
||||
color: text.color,
|
||||
flags: glyph.entry.flags(),
|
||||
},
|
||||
region,
|
||||
);
|
||||
}
|
||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||
self.textures.push(handle.clone());
|
||||
self.primitive(handle.primitive());
|
||||
}
|
||||
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.primitive_at(handle.primitive(), region);
|
||||
}
|
||||
|
||||
/// returns (handle, offset from top left)
|
||||
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.draw(buffer, attrs, &mut ui.textures)
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
@@ -172,28 +143,3 @@ impl<'a> Painter<'a> {
|
||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||
}
|
||||
}
|
||||
|
||||
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
||||
/// and does whatever else drawing it needs.
|
||||
pub trait PrimitiveLike {
|
||||
type Primitive: Primitive;
|
||||
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
||||
}
|
||||
|
||||
impl<P: Primitive> PrimitiveLike for P {
|
||||
type Primitive = P;
|
||||
fn into_primitive(self, _: &mut Painter) -> P {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveLike for &TextureHandle {
|
||||
type Primitive = TexturePrimitive;
|
||||
|
||||
/// Retains a share of the handle, so the slot the primitive names cannot
|
||||
/// be freed and reused while it is still drawn.
|
||||
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
||||
painter.textures.push(self.clone());
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
+16
-18
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
||||
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
|
||||
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||
ui::cache::Cache,
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
};
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
pub layers: PrimitiveLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
pub cache: Cache,
|
||||
|
||||
@@ -52,7 +52,7 @@ impl UiRenderState {
|
||||
);
|
||||
}
|
||||
let root = root.into();
|
||||
if self.needs_full_redraw(root) {
|
||||
if self.root_changed(root) || self.resized {
|
||||
self.redraw_all(root, rsc);
|
||||
self.old_root = root.map(|r| r.id());
|
||||
self.resized = false;
|
||||
@@ -81,10 +81,12 @@ impl UiRenderState {
|
||||
old_children: Option<Vec<WidgetId>>,
|
||||
rsc: &mut dyn UiRsc,
|
||||
) {
|
||||
let mut redrawn = old_children.is_some();
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
if let Some(active) = self.active.get_mut(&id)
|
||||
&& !rsc.widgets().needs_redraw.contains(&id)
|
||||
{
|
||||
redrawn = true;
|
||||
// check to see if we can skip drawing first
|
||||
if active.region == region {
|
||||
return;
|
||||
@@ -149,7 +151,7 @@ impl UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
rsc.on_draw(&active);
|
||||
rsc.on_draw(&active, redrawn);
|
||||
self.active.insert(id, active);
|
||||
}
|
||||
|
||||
@@ -218,17 +220,12 @@ impl UiRenderState {
|
||||
root.into().map(|r| r.id()) != self.old_root
|
||||
}
|
||||
|
||||
// Scheduling and drawing must use the same full-redraw predicate.
|
||||
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||
self.root_changed(root) || self.resized
|
||||
}
|
||||
|
||||
pub fn needs_redraw<'a>(
|
||||
&self,
|
||||
root: impl Into<Option<&'a StrongWidget>>,
|
||||
widgets: &Widgets,
|
||||
) -> bool {
|
||||
self.needs_full_redraw(root) || widgets.has_updates()
|
||||
self.root_changed(root) || widgets.has_updates()
|
||||
}
|
||||
|
||||
pub fn active_widgets(&self) -> usize {
|
||||
@@ -243,14 +240,14 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
pub fn debug_layers(&self) {
|
||||
for ((idx, depth), draws) in self.layers.iter_depth() {
|
||||
for ((idx, depth), primitives) in self.layers.iter_depth() {
|
||||
let indent = " ".repeat(depth * 2);
|
||||
let counts: Vec<String> = draws
|
||||
.primitives()
|
||||
.iter()
|
||||
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
||||
.collect();
|
||||
println!("{indent}{idx}: [{}]", counts.join(", "));
|
||||
let len = primitives.instances().len();
|
||||
print!("{indent}{idx}: {len} primitives");
|
||||
if len >= 1 {
|
||||
print!(" ({})", primitives.instances()[0].binding);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +305,7 @@ impl UiRenderState {
|
||||
source,
|
||||
cache: &mut self.cache,
|
||||
text: &mut ui.text,
|
||||
textures: &mut ui.textures,
|
||||
widgets: &ui.widgets,
|
||||
outer,
|
||||
output_size: self.output_size,
|
||||
|
||||
+6
-9
@@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
|
||||
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||
};
|
||||
|
||||
pub struct SizeCtx<'a> {
|
||||
pub text: &'a mut TextData,
|
||||
pub textures: &'a mut Textures,
|
||||
pub(super) source: WidgetId,
|
||||
pub(super) widgets: &'a Widgets,
|
||||
pub(super) cache: &'a mut Cache,
|
||||
@@ -32,6 +33,7 @@ impl SizeCtx<'_> {
|
||||
.get_dyn_dynamic(id)
|
||||
.desired_len::<A>(&mut SizeCtx {
|
||||
text: self.text,
|
||||
textures: self.textures,
|
||||
source: self.source,
|
||||
widgets: self.widgets,
|
||||
cache: self.cache,
|
||||
@@ -74,13 +76,8 @@ impl SizeCtx<'_> {
|
||||
self.output_size
|
||||
}
|
||||
|
||||
pub fn draw_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
self.text.render(buffer, attrs, width)
|
||||
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||
self.text.draw(buffer, attrs, self.textures)
|
||||
}
|
||||
|
||||
pub fn label(&self, id: WidgetId) -> &String {
|
||||
|
||||
@@ -16,7 +16,7 @@ pub use id::*;
|
||||
pub use math::*;
|
||||
pub use refcount::*;
|
||||
pub use slot::*;
|
||||
pub(crate) use trust::*;
|
||||
pub use trust::*;
|
||||
pub use typemap::*;
|
||||
pub use vec2::*;
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||
pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||
unsafe { std::mem::transmute::<&T, &T>(x) }
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||
pub unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
pub unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
std::mem::transmute::<&T, &mut T>(x)
|
||||
|
||||
@@ -34,7 +34,7 @@ pub trait HasRoot {
|
||||
|
||||
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
||||
#[track_caller]
|
||||
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>;
|
||||
fn add(self, rsc: &mut Rsc) -> WidgetArr<LEN>;
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
|
||||
@@ -58,6 +58,13 @@ macro_rules! impl_widget_arr {
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> IntoWidgetVec<Rsc, ($($Tag,)*), ArrTag> for ($($W,)*) {
|
||||
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($W,)*) = self;
|
||||
vec![$($W.add(rsc).upgrade(rsc),)*]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -1,4 +1,4 @@
|
||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx, UiRsc};
|
||||
use std::any::Any;
|
||||
|
||||
mod data;
|
||||
@@ -85,3 +85,16 @@ impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> f
|
||||
self(state)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoWidgetVec<Rsc, WTag, GTag> {
|
||||
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget>;
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc, I: IntoIterator, Tag> IntoWidgetVec<Rsc, Tag, IterTag> for I
|
||||
where
|
||||
I::Item: WidgetLike<Rsc, Tag>,
|
||||
{
|
||||
fn into_vec(self, rsc: &mut Rsc) -> Vec<StrongWidget> {
|
||||
self.into_iter().map(|w| w.add_strong(rsc).any()).collect()
|
||||
}
|
||||
}
|
||||
@@ -62,3 +62,4 @@ impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
|
||||
}
|
||||
|
||||
pub struct ArrTag;
|
||||
pub struct IterTag;
|
||||
+1
-5
@@ -10,11 +10,7 @@ struct State {
|
||||
}
|
||||
|
||||
impl DefaultAppState for State {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cosmic_text::Family;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
@@ -15,11 +16,7 @@ pub struct Client {
|
||||
}
|
||||
|
||||
impl DefaultAppState for Client {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||
let rrect = rect(Color::WHITE).radius(20);
|
||||
let pad_test = (
|
||||
rrect.color(Color::BLUE),
|
||||
@@ -147,7 +144,7 @@ impl DefaultAppState for Client {
|
||||
.span(Dir::DOWN)
|
||||
.add(rsc);
|
||||
|
||||
let main = WidgetPtr::new().add(rsc);
|
||||
let main = WidgetPtr::empty().add(rsc);
|
||||
|
||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||
let mut switch_button = |color, to: WeakWidget, label| {
|
||||
@@ -212,10 +209,10 @@ impl DefaultAppState for Client {
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
let new = format!(
|
||||
"widgets: {}\nactive: {}\ntextures: {}",
|
||||
"widgets: {}\nactive: {}\nviews: {}",
|
||||
rsc.widgets().len(),
|
||||
render.active_widgets(),
|
||||
rsc.ui().textures.count(),
|
||||
self.ui_state.renderer.ui.view_count(),
|
||||
);
|
||||
if new != *rsc.widgets()[self.info].content {
|
||||
*rsc.widgets_mut()[self.info].content = new;
|
||||
|
||||
+1
-5
@@ -11,11 +11,7 @@ struct State {
|
||||
}
|
||||
|
||||
impl DefaultAppState for State {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||
let rect = rect(Color::RED).add(rsc);
|
||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
+1
-5
@@ -36,11 +36,7 @@ impl Test {
|
||||
}
|
||||
|
||||
impl DefaultAppState for State {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||
let test = Test::new(rsc);
|
||||
|
||||
test.on(CursorSense::click(), move |_, rsc| {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.103"
|
||||
quote = "1.0.42"
|
||||
syn = { version = "2.0.111", features = ["full"] }
|
||||
syn = { version = "3.0.3", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -1,3 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["clippy", "rustfmt"]
|
||||
@@ -7,3 +7,11 @@ impl Event for Submit {}
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
pub struct Edited;
|
||||
impl Event for Edited {}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
pub struct Draw;
|
||||
impl Event for Draw {}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
pub struct Undraw;
|
||||
impl Event for Undraw {}
|
||||
+145
-45
@@ -29,7 +29,26 @@ pub use sense::*;
|
||||
pub use state::*;
|
||||
pub use task::*;
|
||||
|
||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
||||
pub struct EventSender<State: DefaultAppState> {
|
||||
proxy: EventLoopProxy<UiMainEvent<State>>,
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> Clone for EventSender<State> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
proxy: self.proxy.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> EventSender<State> {
|
||||
pub fn send(&self, event: State::Event) {
|
||||
let _ = self.proxy.send_event(UiMainEvent::App(event));
|
||||
}
|
||||
pub fn run(&self, f: impl MainCallback<State>) {
|
||||
let _ = self.proxy.send_event(UiMainEvent::Callback(Box::new(f)));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultUiState {
|
||||
pub root: Option<StrongWidget>,
|
||||
@@ -70,9 +89,8 @@ pub trait HasDefaultUiState: Sized + 'static {
|
||||
}
|
||||
|
||||
pub trait DefaultAppState: HasDefaultUiState {
|
||||
type Event = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
|
||||
-> Self;
|
||||
type Event: Send = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(
|
||||
&mut self,
|
||||
@@ -96,23 +114,54 @@ pub trait DefaultAppState: HasDefaultUiState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultRsc<State: 'static> {
|
||||
pub struct DefaultRsc<State: 'static + DefaultAppState> {
|
||||
pub ui: UiData,
|
||||
pub events: EventManager<Self>,
|
||||
pub tasks: Tasks<Self>,
|
||||
pub state: WidgetState,
|
||||
pub widget_events: Vec<WidgetEvent>,
|
||||
pub window_event: EventSender<State>,
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
impl<State> DefaultRsc<State> {
|
||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, recv) = Tasks::init(window);
|
||||
pub struct WidgetEvent {
|
||||
id: WidgetId,
|
||||
ty: WidgetEventType,
|
||||
}
|
||||
|
||||
pub enum WidgetEventType {
|
||||
Draw,
|
||||
Undraw,
|
||||
Remove,
|
||||
}
|
||||
|
||||
pub trait MainCallback<State>: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static {}
|
||||
impl<F: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static, State> MainCallback<State> for F {}
|
||||
|
||||
pub enum UiMainEvent<State: DefaultAppState> {
|
||||
RequestUpdate,
|
||||
Callback(Box<dyn MainCallback<State>>),
|
||||
App(State::Event),
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> DefaultRsc<State> {
|
||||
fn init(proxy: EventLoopProxy<UiMainEvent<State>>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let window_event = EventSender {
|
||||
proxy: proxy.clone(),
|
||||
};
|
||||
let (tasks, recv) = Tasks::init(move || {
|
||||
if proxy.send_event(UiMainEvent::RequestUpdate).is_err() {
|
||||
panic!("main thread blew up or smth");
|
||||
}
|
||||
});
|
||||
(
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
events: Default::default(),
|
||||
tasks,
|
||||
widget_events: Default::default(),
|
||||
state: Default::default(),
|
||||
window_event,
|
||||
_state: Default::default(),
|
||||
},
|
||||
recv,
|
||||
@@ -124,7 +173,7 @@ impl<State> DefaultRsc<State> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State> UiRsc for DefaultRsc<State> {
|
||||
impl<State: DefaultAppState> UiRsc for DefaultRsc<State> {
|
||||
fn ui(&self) -> &UiData {
|
||||
&self.ui
|
||||
}
|
||||
@@ -133,25 +182,39 @@ impl<State> UiRsc for DefaultRsc<State> {
|
||||
&mut self.ui
|
||||
}
|
||||
|
||||
fn on_draw(&mut self, active: &ActiveData) {
|
||||
fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {
|
||||
self.events.draw(active);
|
||||
if !redrawn {
|
||||
self.widget_events.push(WidgetEvent {
|
||||
id: active.id,
|
||||
ty: WidgetEventType::Draw,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undraw(&mut self, active: &ActiveData) {
|
||||
self.events.undraw(active);
|
||||
self.widget_events.push(WidgetEvent {
|
||||
id: active.id,
|
||||
ty: WidgetEventType::Undraw,
|
||||
});
|
||||
}
|
||||
|
||||
fn on_remove(&mut self, id: WidgetId) {
|
||||
self.events.remove(id);
|
||||
self.state.remove(id);
|
||||
self.widget_events.push(WidgetEvent {
|
||||
id,
|
||||
ty: WidgetEventType::Remove,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasState for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState> HasState for DefaultRsc<State> {
|
||||
type State = State;
|
||||
}
|
||||
|
||||
impl<State: 'static> HasEvents for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState> HasEvents for DefaultRsc<State> {
|
||||
fn events(&self) -> &EventManager<Self> {
|
||||
&self.events
|
||||
}
|
||||
@@ -161,13 +224,13 @@ impl<State: 'static> HasEvents for DefaultRsc<State> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasTasks for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState> HasTasks for DefaultRsc<State> {
|
||||
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
|
||||
&mut self.tasks
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState> HasWidgetState for DefaultRsc<State> {
|
||||
fn widget_state(&self) -> &WidgetState {
|
||||
&self.state
|
||||
}
|
||||
@@ -185,15 +248,15 @@ pub struct DefaultApp<State: DefaultAppState> {
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
type Event = State::Event;
|
||||
type Event = UiMainEvent<State>;
|
||||
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(State::window_attributes())
|
||||
.unwrap();
|
||||
let default_state = DefaultUiState::new(window);
|
||||
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
|
||||
let state = State::new(default_state, &mut rsc, proxy);
|
||||
let (mut rsc, task_recv) = DefaultRsc::init(proxy);
|
||||
let state = State::new(default_state, &mut rsc);
|
||||
let render = UiRenderState::new();
|
||||
Self {
|
||||
rsc,
|
||||
@@ -204,38 +267,39 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
|
||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
||||
match event {
|
||||
UiMainEvent::RequestUpdate => {
|
||||
self.check_updates();
|
||||
}
|
||||
UiMainEvent::App(event) => {
|
||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
||||
}
|
||||
UiMainEvent::Callback(f) => f(&mut self.rsc),
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
let Self {
|
||||
rsc,
|
||||
render,
|
||||
state,
|
||||
task_recv,
|
||||
rsc, render, state, ..
|
||||
} = self;
|
||||
|
||||
for update in task_recv.try_iter() {
|
||||
update(state, rsc);
|
||||
}
|
||||
|
||||
// input handling
|
||||
let ui_state = state.default_state_mut();
|
||||
let input_changed = ui_state.input.event(&event);
|
||||
let cursor_state = ui_state.cursor_state().clone();
|
||||
let old = ui_state.focus;
|
||||
if cursor_state.buttons.left.is_start() {
|
||||
ui_state.focus = None;
|
||||
}
|
||||
if input_changed {
|
||||
if ui_state.input.event(&event) {
|
||||
let cursor_state = ui_state.cursor_state().clone();
|
||||
let old = ui_state.focus;
|
||||
if cursor_state.buttons.left.is_start() {
|
||||
ui_state.focus = None;
|
||||
}
|
||||
let window_size = ui_state.window_size();
|
||||
render.run_sensors(rsc, state, cursor_state, window_size);
|
||||
if old != state.default_state().focus
|
||||
&& let Some(old) = old
|
||||
{
|
||||
old.edit(rsc).deselect();
|
||||
}
|
||||
}
|
||||
let ui_state = state.default_state_mut();
|
||||
if old != ui_state.focus
|
||||
&& let Some(old) = old
|
||||
{
|
||||
old.edit(rsc).deselect();
|
||||
}
|
||||
match &event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => {
|
||||
@@ -297,11 +361,9 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
_ => (),
|
||||
}
|
||||
state.window_event(event, rsc, render);
|
||||
let ui_state = self.state.default_state_mut();
|
||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
ui_state.input.end_frame();
|
||||
|
||||
self.check_updates();
|
||||
self.state.default_state_mut().input.end_frame();
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
@@ -309,13 +371,49 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> DefaultApp<State> {
|
||||
pub fn check_updates(&mut self) {
|
||||
let Self {
|
||||
rsc,
|
||||
render,
|
||||
state,
|
||||
task_recv,
|
||||
} = self;
|
||||
|
||||
for update in task_recv.try_iter() {
|
||||
update(state, rsc);
|
||||
}
|
||||
|
||||
let mut events = std::mem::take(&mut rsc.widget_events);
|
||||
for event in events.drain(..) {
|
||||
match event.ty {
|
||||
WidgetEventType::Draw => {
|
||||
rsc.run_event::<Draw>(event.id, (), state);
|
||||
}
|
||||
WidgetEventType::Undraw => {
|
||||
rsc.run_event::<Undraw>(event.id, (), state);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
rsc.widget_events = events;
|
||||
|
||||
let ui_state = state.default_state();
|
||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RscIdx<Rsc> {
|
||||
type Output;
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
|
||||
}
|
||||
|
||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I>
|
||||
for DefaultRsc<State>
|
||||
{
|
||||
type Output = I::Output;
|
||||
|
||||
fn index(&self, index: I) -> &Self::Output {
|
||||
@@ -323,7 +421,9 @@ impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for Defaul
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
|
||||
impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I>
|
||||
for DefaultRsc<State>
|
||||
{
|
||||
fn index_mut(&mut self, index: I) -> &mut Self::Output {
|
||||
index.get_mut(self)
|
||||
}
|
||||
|
||||
+21
-20
@@ -1,4 +1,4 @@
|
||||
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use wgpu::*;
|
||||
@@ -23,18 +23,9 @@ impl UiRenderer {
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture) => texture,
|
||||
CurrentSurfaceTexture::Suboptimal(texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
texture
|
||||
}
|
||||
CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Lost => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return;
|
||||
}
|
||||
CurrentSurfaceTexture::Timeout
|
||||
| CurrentSurfaceTexture::Occluded
|
||||
| CurrentSurfaceTexture::Validation => return,
|
||||
CurrentSurfaceTexture::Success(v) => v,
|
||||
CurrentSurfaceTexture::Suboptimal(v) => v,
|
||||
_ => panic!("failed"),
|
||||
};
|
||||
let view = output
|
||||
.texture
|
||||
@@ -58,7 +49,6 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
}
|
||||
|
||||
@@ -66,7 +56,7 @@ impl UiRenderer {
|
||||
self.config.width = size.width;
|
||||
self.config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.ui.resize((size.width, size.height), &self.queue);
|
||||
self.ui.resize(size, &self.queue);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
@@ -80,8 +70,10 @@ impl UiRenderer {
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor {
|
||||
backends: Backends::PRIMARY,
|
||||
display: Some(Box::new(window.clone())),
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
flags: Default::default(),
|
||||
memory_budget_thresholds: Default::default(),
|
||||
backend_options: Default::default(),
|
||||
display: None,
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
@@ -98,9 +90,18 @@ impl UiRenderer {
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
let ui_limits = UiLimits::default();
|
||||
|
||||
let (device, queue) = adapter
|
||||
.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(),
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -120,20 +121,20 @@ impl UiRenderer {
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
present_mode: PresentMode::AutoNoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
color_space: Default::default(),
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let ui = UiRenderNode::new(&device, &config);
|
||||
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
|
||||
+5
-6
@@ -13,7 +13,6 @@ use tokio::{
|
||||
unbounded_channel as async_channel,
|
||||
},
|
||||
};
|
||||
use winit::window::Window;
|
||||
|
||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
||||
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
||||
@@ -23,7 +22,7 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
|
||||
|
||||
pub struct Tasks<Rsc: HasState> {
|
||||
start: AsyncSender<BoxTask>,
|
||||
window: Arc<Window>,
|
||||
request_update: Arc<dyn Fn() + Send + Sync>,
|
||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@ impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
impl<Rsc: HasState> Tasks<Rsc> {
|
||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||
pub fn init(request_update: impl Fn() + 'static + Send + Sync) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||
let (start, start_recv) = async_channel();
|
||||
let (msgs, msgs_recv) = sync_channel();
|
||||
std::thread::spawn(|| {
|
||||
@@ -56,7 +55,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
Self {
|
||||
start,
|
||||
msg_send: msgs,
|
||||
window,
|
||||
request_update: Arc::new(request_update),
|
||||
},
|
||||
msgs_recv,
|
||||
)
|
||||
@@ -67,10 +66,10 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
F::CallOnceFuture: Send,
|
||||
{
|
||||
let send = self.msg_send.clone();
|
||||
let window = self.window.clone();
|
||||
let request_update = self.request_update.clone();
|
||||
let _ = self.start.send(Box::pin(async move {
|
||||
task(TaskCtx::new(send)).await;
|
||||
window.request_redraw();
|
||||
request_update();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(gen_blocks)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ pub struct Image {
|
||||
|
||||
impl Widget for Image {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.primitive(&self.handle);
|
||||
painter.texture(&self.handle);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
|
||||
@@ -5,6 +5,7 @@ mod ptr;
|
||||
mod rect;
|
||||
mod text;
|
||||
mod trait_fns;
|
||||
mod selector;
|
||||
|
||||
pub use image::*;
|
||||
pub use mask::*;
|
||||
@@ -13,3 +14,4 @@ pub use ptr::*;
|
||||
pub use rect::*;
|
||||
pub use text::*;
|
||||
pub use trait_fns::*;
|
||||
pub use selector::*;
|
||||
@@ -152,32 +152,32 @@ impl Span {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||
pub children: Wa,
|
||||
pub struct SpanBuilder<Children, Rsc, Tag, GTag> {
|
||||
pub children: Children,
|
||||
pub dir: Dir,
|
||||
pub gap: f32,
|
||||
_pd: PhantomData<(State, Tag)>,
|
||||
_pd: PhantomData<(Rsc, Tag, GTag)>,
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
|
||||
for SpanBuilder<Rsc, LEN, Wa, Tag>
|
||||
impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
|
||||
for SpanBuilder<Children, Rsc, Tag, GTag>
|
||||
{
|
||||
type Widget = Span;
|
||||
|
||||
#[track_caller]
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget {
|
||||
Span {
|
||||
children: self.children.add(rsc).arr.into_iter().collect(),
|
||||
children: self.children.into_vec(rsc),
|
||||
dir: self.dir,
|
||||
gap: self.gap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
SpanBuilder<State, LEN, Wa, Tag>
|
||||
impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
|
||||
SpanBuilder<Children, Rsc, Tag, GTag>
|
||||
{
|
||||
pub fn new(children: Wa, dir: Dir) -> Self {
|
||||
pub fn new(children: Children, dir: Dir) -> Self {
|
||||
Self {
|
||||
children,
|
||||
dir,
|
||||
|
||||
@@ -42,30 +42,28 @@ pub enum StackSize {
|
||||
Child(usize),
|
||||
}
|
||||
|
||||
pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||
pub children: Wa,
|
||||
pub struct StackBuilder<Children, Rsc, Tag, GTag> {
|
||||
pub children: Children,
|
||||
pub size: StackSize,
|
||||
_pd: PhantomData<(State, Tag)>,
|
||||
_pd: PhantomData<(Rsc, Tag, GTag)>,
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
|
||||
for StackBuilder<Rsc, LEN, Wa, Tag>
|
||||
impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> WidgetFnTrait<Rsc>
|
||||
for StackBuilder<Children, Rsc, Tag, GTag>
|
||||
{
|
||||
type Widget = Stack;
|
||||
|
||||
#[track_caller]
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget {
|
||||
Stack {
|
||||
children: self.children.add(rsc).arr.into_iter().collect(),
|
||||
children: self.children.into_vec(rsc),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
StackBuilder<State, LEN, Wa, Tag>
|
||||
{
|
||||
pub fn new(children: Wa) -> Self {
|
||||
impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag> StackBuilder<Children, Rsc, Tag, GTag> {
|
||||
pub fn new(children: Children) -> Self {
|
||||
Self {
|
||||
children,
|
||||
size: StackSize::default(),
|
||||
|
||||
+4
-2
@@ -30,8 +30,10 @@ impl Widget for WidgetPtr {
|
||||
}
|
||||
|
||||
impl WidgetPtr {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
pub fn new(widget: StrongWidget) -> Self {
|
||||
Self {
|
||||
inner: Some(widget),
|
||||
}
|
||||
}
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use iris_core::util::HashMap;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct WidgetSelector<T> {
|
||||
current: (T, StrongWidget),
|
||||
map: HashMap<T, StrongWidget>,
|
||||
}
|
||||
|
||||
impl<T: Hash + Eq> WidgetSelector<T> {
|
||||
pub fn new(key: T, widget: StrongWidget) -> Self {
|
||||
Self {
|
||||
current: (key, widget),
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, key: T, widget: StrongWidget) {
|
||||
self.map.insert(key, widget);
|
||||
}
|
||||
|
||||
pub fn select(&mut self, key: T) -> bool {
|
||||
if let Some(val) = self.map.remove(&key) {
|
||||
let mut new = (key, val);
|
||||
std::mem::swap(&mut new, &mut self.current);
|
||||
self.map.insert(new.0, new.1);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Widget for WidgetSelector<T> {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.widget(&self.current.1);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
ctx.width(&self.current.1)
|
||||
}
|
||||
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
ctx.height(&self.current.1)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use cosmic_text::{Attrs, Family, Metrics};
|
||||
use std::marker::{PhantomData, Sized};
|
||||
|
||||
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
|
||||
@@ -19,7 +20,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
|
||||
self.attrs.color = color;
|
||||
self
|
||||
}
|
||||
pub fn family(mut self, family: Family) -> Self {
|
||||
pub fn family(mut self, family: Family<'static>) -> Self {
|
||||
self.attrs.family = family;
|
||||
self
|
||||
}
|
||||
@@ -81,13 +82,19 @@ impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
|
||||
state: &mut Rsc,
|
||||
builder: TextBuilder<Rsc, Self, H>,
|
||||
) -> Self::Output {
|
||||
let buf = TextBuffer::new(&builder.content);
|
||||
let mut buf = TextBuffer::new_empty(Metrics::new(
|
||||
builder.attrs.font_size,
|
||||
builder.attrs.line_height,
|
||||
));
|
||||
let hint = builder.hint.get(state);
|
||||
let font_system = &mut state.ui_mut().text.font_system;
|
||||
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
|
||||
let mut text = Text {
|
||||
content: builder.content.into(),
|
||||
view: TextView::new(buf, builder.attrs, hint),
|
||||
};
|
||||
text.content.changed = false;
|
||||
builder.attrs.apply(font_system, &mut text.view.buf, None);
|
||||
text
|
||||
}
|
||||
}
|
||||
@@ -103,11 +110,19 @@ impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
|
||||
state: &mut State,
|
||||
builder: TextBuilder<State, Self, H>,
|
||||
) -> Self::Output {
|
||||
let buf = TextBuffer::new(&builder.content);
|
||||
TextEdit::new(
|
||||
let buf = TextBuffer::new_empty(Metrics::new(
|
||||
builder.attrs.font_size,
|
||||
builder.attrs.line_height,
|
||||
));
|
||||
let mut text = TextEdit::new(
|
||||
TextView::new(buf, builder.attrs, builder.hint.get(state)),
|
||||
builder.output.mode,
|
||||
)
|
||||
);
|
||||
let font_system = &mut state.ui_mut().text.font_system;
|
||||
text.buf
|
||||
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
|
||||
builder.attrs.apply(font_system, &mut text.buf, None);
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+393
-242
@@ -1,30 +1,17 @@
|
||||
use crate::prelude::*;
|
||||
use iris_core::{TextData, UiColor};
|
||||
use parley::{Affinity, Layout, Selection};
|
||||
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use winit::{
|
||||
event::KeyEvent,
|
||||
keyboard::{Key, NamedKey},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Motion {
|
||||
Left,
|
||||
Right,
|
||||
LeftWord,
|
||||
RightWord,
|
||||
Up,
|
||||
Down,
|
||||
LineStart,
|
||||
LineEnd,
|
||||
}
|
||||
|
||||
pub struct TextEdit {
|
||||
view: TextView,
|
||||
/// `None` represents unfocused, which Parley's `Selection` cannot express.
|
||||
selection: Option<Selection>,
|
||||
history: Vec<(String, Option<Selection>)>,
|
||||
double_hit: Option<usize>,
|
||||
selection: TextSelection,
|
||||
history: Vec<(String, TextSelection)>,
|
||||
double_hit: Option<Cursor>,
|
||||
pub mode: EditMode,
|
||||
}
|
||||
|
||||
@@ -38,19 +25,27 @@ impl TextEdit {
|
||||
pub fn new(view: TextView, mode: EditMode) -> Self {
|
||||
Self {
|
||||
view,
|
||||
selection: None,
|
||||
selection: Default::default(),
|
||||
history: Default::default(),
|
||||
double_hit: None,
|
||||
mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
let sel = self.selection?;
|
||||
if sel.is_collapsed() {
|
||||
return None;
|
||||
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
let mut iter = self.buf.lines.iter().skip(start.line);
|
||||
let first = iter.next().unwrap();
|
||||
if start.line == end.line {
|
||||
first.text()[start.index..end.index].to_string()
|
||||
} else {
|
||||
let mut str = first.text()[start.index..].to_string();
|
||||
for _ in (start.line + 1)..end.line {
|
||||
str = str + "\n" + iter.next().unwrap().text();
|
||||
}
|
||||
let last = iter.next().unwrap();
|
||||
str = str + "\n" + &last.text()[..end.index];
|
||||
str
|
||||
}
|
||||
Some(self.buf.text()[sel.text_range()].to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,29 +57,39 @@ impl Widget for TextEdit {
|
||||
painter.layer = base;
|
||||
let region = self.region();
|
||||
|
||||
let Some(selection) = self.selection else {
|
||||
return;
|
||||
};
|
||||
let layout = self.view.buf.layout();
|
||||
|
||||
// parley reports selection as boxes in layout space, so bidi and
|
||||
// wrapped lines come out right without this code knowing about either.
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
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(
|
||||
RectPrimitive::color(Color::SKY),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
let size = vec2(1, self.attrs.line_height);
|
||||
match self.selection {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
if let Some(offset) = cursor_pos(cursor, &self.buf) {
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
TextSelection::Span { start, end } => {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
|
||||
let top_left = vec2(x, self.attrs.line_height * l as f32);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::SKY),
|
||||
size.with_x(width)
|
||||
.align(Align::TOP_LEFT)
|
||||
.offset(top_left)
|
||||
.within(®ion),
|
||||
);
|
||||
}
|
||||
if let Some(end_offset) = cursor_pos(end, &self.buf) {
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT)
|
||||
.offset(end_offset)
|
||||
.within(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||
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(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
@@ -96,58 +101,154 @@ impl Widget for TextEdit {
|
||||
}
|
||||
}
|
||||
|
||||
const CARET_WIDTH: f32 = 1.0;
|
||||
/// provides top left + width
|
||||
fn iter_layout_lines(
|
||||
start: Cursor,
|
||||
end: Cursor,
|
||||
buf: &TextBuffer,
|
||||
) -> impl Iterator<Item = (usize, f32, f32)> {
|
||||
gen move {
|
||||
let mut iter = buf.layout_runs().enumerate();
|
||||
for (i, line) in iter.by_ref() {
|
||||
if line.line_i == start.line
|
||||
&& let Some(start_x) = index_x(&line, start.index)
|
||||
{
|
||||
if start.line == end.line
|
||||
&& let Some(end_x) = index_x(&line, end.index)
|
||||
{
|
||||
yield (i, start_x, end_x - start_x);
|
||||
return;
|
||||
}
|
||||
yield (i, start_x, line.line_w - start_x);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (i, line) in iter {
|
||||
if line.line_i > end.line {
|
||||
return;
|
||||
}
|
||||
if line.line_i == end.line
|
||||
&& let Some(end_x) = index_x(&line, end.index)
|
||||
{
|
||||
yield (i, 0.0, end_x);
|
||||
return;
|
||||
}
|
||||
yield (i, 0.0, line.line_w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copied & modified from fn found in Editor in cosmic_text
|
||||
/// returns x pos of a (non layout) index within an layout run
|
||||
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
|
||||
for glyph in run.glyphs.iter() {
|
||||
if index == glyph.start {
|
||||
return Some(glyph.x);
|
||||
} else if index > glyph.start && index < glyph.end {
|
||||
// Guess x offset based on characters
|
||||
let mut before = 0;
|
||||
let mut total = 0;
|
||||
|
||||
let cluster = &run.text[glyph.start..glyph.end];
|
||||
for (i, _) in cluster.grapheme_indices(true) {
|
||||
if glyph.start + i < index {
|
||||
before += 1;
|
||||
}
|
||||
total += 1;
|
||||
}
|
||||
|
||||
let offset = glyph.w * (before as f32) / (total as f32);
|
||||
return Some(glyph.x + offset);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// returns top of line segment where cursor should visually select
|
||||
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
|
||||
let mut prev = None;
|
||||
for run in buf
|
||||
.layout_runs()
|
||||
.skip_while(|r| r.line_i < cursor.line)
|
||||
.take_while(|r| r.line_i == cursor.line)
|
||||
{
|
||||
prev = Some(vec2(run.line_w, run.line_top));
|
||||
if let Some(pos) = index_x(&run, cursor.index) {
|
||||
return Some(vec2(pos, run.line_top));
|
||||
}
|
||||
}
|
||||
prev
|
||||
}
|
||||
|
||||
pub struct TextEditCtx<'a> {
|
||||
pub text: &'a mut TextEdit,
|
||||
pub data: &'a mut TextData,
|
||||
pub font_system: &'a mut FontSystem,
|
||||
}
|
||||
|
||||
impl<'a> TextEditCtx<'a> {
|
||||
fn layout(&mut self) -> &Layout<UiColor> {
|
||||
let attrs = self.text.view.attrs.clone();
|
||||
let width = self.text.view.wrap_width();
|
||||
self.text.view.buf.shape(self.data, &attrs, width);
|
||||
self.text.view.buf.layout()
|
||||
}
|
||||
|
||||
fn clamp_selection_to_layout(&mut self) {
|
||||
if let Some(sel) = self.text.selection {
|
||||
let layout = self.layout();
|
||||
self.text.selection = Some(sel.refresh(layout));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> String {
|
||||
let text = std::mem::take(self.text.view.buf.edit());
|
||||
self.text.selection = None;
|
||||
let text = self
|
||||
.text
|
||||
.buf
|
||||
.lines
|
||||
.drain(..)
|
||||
.map(|l| l.into_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.text
|
||||
.buf
|
||||
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
|
||||
self.text.selection.clear();
|
||||
text
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.text.selection = None;
|
||||
self.text
|
||||
.buf
|
||||
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
|
||||
self.text.selection.clear();
|
||||
}
|
||||
|
||||
pub fn motion(&mut self, motion: Motion, select: bool) {
|
||||
let Some(sel) = self.text.selection else {
|
||||
return;
|
||||
};
|
||||
let layout = self.layout();
|
||||
let sel = apply_motion(sel, layout, motion, select);
|
||||
self.text.selection = Some(sel);
|
||||
if let TextSelection::Pos(cursor) = self.text.selection
|
||||
&& let Some(new) = self.buf_motion(cursor, motion)
|
||||
{
|
||||
if select {
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: cursor,
|
||||
end: new,
|
||||
};
|
||||
} else {
|
||||
self.text.selection = TextSelection::Pos(new);
|
||||
}
|
||||
} else if let TextSelection::Span { start, end } = self.text.selection {
|
||||
if select {
|
||||
if let Some(cursor) = self.buf_motion(end, motion) {
|
||||
self.text.selection = TextSelection::Span { start, end: cursor };
|
||||
}
|
||||
} else {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
let sel = &mut self.text.selection;
|
||||
match motion {
|
||||
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
|
||||
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
|
||||
_ => {
|
||||
if let Some(cursor) = self.buf_motion(end, motion) {
|
||||
self.text.selection = TextSelection::Pos(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the `len` characters before the caret. This is the IME's
|
||||
/// preedit path: it re-sends the whole composition each time.
|
||||
pub fn replace(&mut self, len: usize, text: &str) {
|
||||
let text = self.string(text);
|
||||
for _ in 0..len {
|
||||
self.backspace(false);
|
||||
self.delete(false);
|
||||
}
|
||||
self.insert_str(&text);
|
||||
self.insert_inner(&text, false);
|
||||
}
|
||||
|
||||
fn string(&self, text: &str) -> String {
|
||||
@@ -160,173 +261,202 @@ impl<'a> TextEditCtx<'a> {
|
||||
|
||||
pub fn insert(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.insert_str(&text);
|
||||
}
|
||||
|
||||
fn insert_str(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
let mut lines = text.split('\n');
|
||||
let Some(first) = lines.next() else {
|
||||
return;
|
||||
}
|
||||
self.clear_span();
|
||||
let at = match self.text.selection {
|
||||
Some(sel) => sel.focus().index(),
|
||||
None => return,
|
||||
};
|
||||
let at = at.min(self.text.view.buf.text().len());
|
||||
self.text.view.buf.edit().insert_str(at, text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(at + text.len());
|
||||
self.insert_inner(first, true);
|
||||
for line in lines {
|
||||
self.newline();
|
||||
self.insert_inner(line, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_span(&mut self) -> bool {
|
||||
let Some(sel) = self.text.selection else {
|
||||
return false;
|
||||
};
|
||||
if sel.is_collapsed() {
|
||||
return false;
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
self.delete_between(start, end);
|
||||
let (start, _) = sort_cursors(start, end);
|
||||
self.text.selection = TextSelection::Pos(start);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
let range = sel.text_range();
|
||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(range.start);
|
||||
true
|
||||
}
|
||||
|
||||
fn set_caret(&mut self, index: usize) {
|
||||
let index = index.min(self.text.view.buf.text().len());
|
||||
let layout = self.layout();
|
||||
self.text.selection = Some(Selection::from_byte_index(
|
||||
layout,
|
||||
index,
|
||||
Affinity::default(),
|
||||
));
|
||||
pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
if start.line == end.line {
|
||||
let line = &mut lines[start.line];
|
||||
let text = line.text();
|
||||
let text = text[..start.index].to_string() + &text[end.index..];
|
||||
edit_line(line, text);
|
||||
} else {
|
||||
// start
|
||||
let start_text = lines[start.line].text()[..start.index].to_string();
|
||||
let end_text = &lines[end.line].text()[end.index..];
|
||||
let text = start_text + end_text;
|
||||
edit_line(&mut lines[start.line], text);
|
||||
}
|
||||
// between
|
||||
let range = (start.line + 1)..=end.line;
|
||||
if !range.is_empty() {
|
||||
lines.splice(range, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_inner(&mut self, text: &str, mov: bool) {
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let line = &mut self.text.view.buf.lines[cursor.line];
|
||||
let mut line_text = line.text().to_string();
|
||||
line_text.insert_str(cursor.index, text);
|
||||
edit_line(line, line_text);
|
||||
if mov {
|
||||
for _ in 0..text.chars().count() {
|
||||
self.motion(Motion::Right, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newline(&mut self) {
|
||||
if self.text.mode == EditMode::MultiLine {
|
||||
self.insert_str("\n");
|
||||
if self.text.mode == EditMode::SingleLine {
|
||||
return;
|
||||
}
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
let new = line.split_off(cursor.index);
|
||||
cursor.line += 1;
|
||||
lines.insert(cursor.line, new);
|
||||
cursor.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, word: bool) {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
&& (cursor.index != 0 || cursor.line != 0)
|
||||
{
|
||||
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
|
||||
self.delete(word);
|
||||
}
|
||||
let Some(sel) = self.text.selection else {
|
||||
return;
|
||||
};
|
||||
let end = sel.focus().index();
|
||||
if end == 0 {
|
||||
return;
|
||||
}
|
||||
let layout = self.layout();
|
||||
let start = if word {
|
||||
sel.focus().previous_logical_word(layout).index()
|
||||
} else {
|
||||
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
|
||||
return;
|
||||
};
|
||||
let range = cluster.text_range();
|
||||
if cluster.is_hard_line_break() || cluster.is_emoji() {
|
||||
range.start
|
||||
} else {
|
||||
self.text.view.buf.text()[..range.end]
|
||||
.char_indices()
|
||||
.next_back()
|
||||
.map_or(range.start, |(start, _)| start)
|
||||
}
|
||||
};
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, word: bool) {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
{
|
||||
if word {
|
||||
let start = *cursor;
|
||||
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
|
||||
self.delete_between(start, end);
|
||||
}
|
||||
} else {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
if cursor.index == line.text().len() {
|
||||
if cursor.line == lines.len() - 1 {
|
||||
return;
|
||||
}
|
||||
let add = lines.remove(cursor.line + 1).into_text();
|
||||
let line = &mut lines[cursor.line];
|
||||
let mut cur = line.text().to_string();
|
||||
cur.push_str(&add);
|
||||
edit_line(line, cur);
|
||||
} else {
|
||||
let mut text = line.text().to_string();
|
||||
text.remove(cursor.index);
|
||||
edit_line(line, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(sel) = self.text.selection else {
|
||||
return;
|
||||
};
|
||||
let start = sel.focus().index();
|
||||
if start >= self.text.view.buf.text().len() {
|
||||
return;
|
||||
}
|
||||
let layout = self.layout();
|
||||
let end = if word {
|
||||
sel.focus().next_logical_word(layout).index()
|
||||
} else {
|
||||
let clusters = sel.focus().logical_clusters(layout);
|
||||
let Some(cluster) = clusters[1].as_ref() else {
|
||||
return;
|
||||
};
|
||||
cluster.text_range().end
|
||||
};
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
|
||||
fn delete_range(&mut self, start: usize, end: usize) {
|
||||
self.text.view.buf.edit().replace_range(start..end, "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(start);
|
||||
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
|
||||
self.text
|
||||
.buf
|
||||
.cursor_motion(self.font_system, cursor, None, motion)
|
||||
.map(|r| r.0)
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
let len = self.text.view.buf.text().len();
|
||||
if len == 0 {
|
||||
return;
|
||||
pub fn select_word_at(&mut self, cursor: Cursor) {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.buf_motion(cursor, Motion::LeftWord),
|
||||
self.buf_motion(cursor, Motion::RightWord),
|
||||
) {
|
||||
self.text.selection = TextSelection::Span { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_line_at(&mut self, cursor: Cursor) {
|
||||
let end = self.text.buf.lines[cursor.line].text().len();
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: Cursor::new(cursor.line, 0),
|
||||
end: Cursor::new(cursor.line, end),
|
||||
}
|
||||
let layout = self.layout();
|
||||
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
||||
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
||||
self.text.selection = Some(Selection::new(anchor, focus));
|
||||
}
|
||||
|
||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||
let prev_sel = self.text.selection;
|
||||
let prev_hit = self.text.double_hit;
|
||||
|
||||
let layout = self.layout();
|
||||
let (selection, double_hit) = if drag {
|
||||
let Some(selection) = prev_sel else {
|
||||
return;
|
||||
};
|
||||
(selection.extend_to_point(layout, pos.x, pos.y), prev_hit)
|
||||
} else {
|
||||
let hit = Selection::from_point(layout, pos.x, pos.y);
|
||||
let index = hit.focus().index();
|
||||
// Successive clicks at one index select the word, then the line.
|
||||
if recent && prev_hit == Some(index) {
|
||||
(Selection::line_from_point(layout, pos.x, pos.y), None)
|
||||
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
|
||||
(
|
||||
Selection::word_from_point(layout, pos.x, pos.y),
|
||||
Some(index),
|
||||
)
|
||||
} else {
|
||||
(hit, None)
|
||||
let hit = self.text.buf.hit(pos.x, pos.y);
|
||||
let sel = &mut self.text.selection;
|
||||
match sel {
|
||||
TextSelection::None => {
|
||||
if !drag && let Some(hit) = hit {
|
||||
*sel = TextSelection::Pos(hit)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.text.selection = Some(selection);
|
||||
self.text.double_hit = double_hit;
|
||||
TextSelection::Pos(pos) => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => (),
|
||||
(Some(hit), false) => {
|
||||
if recent && hit == *pos {
|
||||
self.text.double_hit = Some(hit);
|
||||
return self.select_word_at(hit);
|
||||
} else {
|
||||
*pos = hit
|
||||
}
|
||||
}
|
||||
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
|
||||
},
|
||||
TextSelection::Span { start, end } => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => *sel = TextSelection::Pos(*start),
|
||||
(Some(hit), false) => {
|
||||
if recent
|
||||
&& let Some(double) = self.text.double_hit
|
||||
&& double == hit
|
||||
{
|
||||
return self.select_line_at(hit);
|
||||
} else {
|
||||
*sel = TextSelection::Pos(hit)
|
||||
}
|
||||
}
|
||||
(Some(hit), true) => *end = hit,
|
||||
},
|
||||
}
|
||||
if let TextSelection::Span { start, end } = sel
|
||||
&& start == end
|
||||
{
|
||||
*sel = TextSelection::Pos(*start);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.text.selection = None;
|
||||
self.text.double_hit = None;
|
||||
self.text.selection = TextSelection::None;
|
||||
}
|
||||
|
||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||
let old = (self.text.content(), self.text.selection);
|
||||
let mut undo = false;
|
||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.clamp_selection_to_layout();
|
||||
}
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
} else if self.text.content() != old.0 {
|
||||
self.text.history.push(old);
|
||||
}
|
||||
res
|
||||
@@ -351,25 +481,21 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowRight => {
|
||||
let motion = if modifiers.control {
|
||||
Motion::RightWord
|
||||
if modifiers.control {
|
||||
self.motion(Motion::RightWord, modifiers.shift)
|
||||
} else {
|
||||
Motion::Right
|
||||
};
|
||||
self.motion(motion, modifiers.shift);
|
||||
self.motion(Motion::Right, modifiers.shift)
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowLeft => {
|
||||
let motion = if modifiers.control {
|
||||
Motion::LeftWord
|
||||
if modifiers.control {
|
||||
self.motion(Motion::LeftWord, modifiers.shift)
|
||||
} else {
|
||||
Motion::Left
|
||||
};
|
||||
self.motion(motion, modifiers.shift);
|
||||
self.motion(Motion::Left, modifiers.shift)
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
|
||||
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
|
||||
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
|
||||
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
|
||||
NamedKey::Escape => {
|
||||
self.deselect();
|
||||
return TextInputResult::Unfocus;
|
||||
@@ -381,18 +507,34 @@ impl<'a> TextEditCtx<'a> {
|
||||
match text.as_str() {
|
||||
"v" => return TextInputResult::Paste,
|
||||
"c" => {
|
||||
if let Some(content) = self.text.selected_text() {
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
let content = self.text.select_content(start, end);
|
||||
return TextInputResult::Copy(content);
|
||||
}
|
||||
}
|
||||
"x" => {
|
||||
if let Some(content) = self.text.selected_text() {
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
let content = self.text.select_content(start, end);
|
||||
self.clear_span();
|
||||
return TextInputResult::Copy(content);
|
||||
}
|
||||
}
|
||||
"a" => self.select_all(),
|
||||
"z" => *undo = true,
|
||||
"a" => {
|
||||
if !self.text.buf.lines[0].text().is_empty()
|
||||
|| self.text.buf.lines.len() > 1
|
||||
{
|
||||
let lines = &self.text.buf.lines;
|
||||
let last_line = lines.len() - 1;
|
||||
let last_idx = lines[last_line].text().len();
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: Cursor::new(0, 0),
|
||||
end: Cursor::new(last_line, last_idx),
|
||||
};
|
||||
}
|
||||
}
|
||||
"z" => {
|
||||
*undo = true;
|
||||
}
|
||||
_ => self.insert(text),
|
||||
}
|
||||
} else {
|
||||
@@ -405,24 +547,6 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_motion(
|
||||
sel: Selection,
|
||||
layout: &Layout<UiColor>,
|
||||
motion: Motion,
|
||||
extend: bool,
|
||||
) -> Selection {
|
||||
match motion {
|
||||
Motion::Left => sel.previous_visual(layout, extend),
|
||||
Motion::Right => sel.next_visual(layout, extend),
|
||||
Motion::LeftWord => sel.previous_visual_word(layout, extend),
|
||||
Motion::RightWord => sel.next_visual_word(layout, extend),
|
||||
Motion::Up => sel.previous_line(layout, extend),
|
||||
Motion::Down => sel.next_line(layout, extend),
|
||||
Motion::LineStart => sel.line_start(layout, extend),
|
||||
Motion::LineEnd => sel.line_end(layout, extend),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Modifiers {
|
||||
pub shift: bool,
|
||||
@@ -445,6 +569,33 @@ pub enum TextInputResult {
|
||||
Paste,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum TextSelection {
|
||||
#[default]
|
||||
None,
|
||||
Pos(Cursor),
|
||||
Span {
|
||||
start: Cursor,
|
||||
end: Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
cursor.line = 0;
|
||||
cursor.index = 0;
|
||||
cursor.affinity = Affinity::default();
|
||||
}
|
||||
TextSelection::Span { start: _, end: _ } => {
|
||||
*self = TextSelection::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputResult {
|
||||
pub fn unfocus(&self) -> bool {
|
||||
matches!(self, TextInputResult::Unfocus)
|
||||
@@ -474,7 +625,7 @@ impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
|
||||
let ui = ui.ui_mut();
|
||||
TextEditCtx {
|
||||
text: ui.widgets.get_mut(self).unwrap(),
|
||||
data: &mut ui.text,
|
||||
font_system: &mut ui.text.font_system,
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-39
@@ -6,8 +6,11 @@ pub use edit::*;
|
||||
use iris_core::util::MutDetect;
|
||||
|
||||
use crate::prelude::*;
|
||||
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub const SHAPING: Shaping = Shaping::Advanced;
|
||||
|
||||
pub struct Text {
|
||||
pub content: MutDetect<String>,
|
||||
view: TextView,
|
||||
@@ -22,16 +25,6 @@ pub struct TextView {
|
||||
pub hint: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
pub fn wrap_width(&self) -> Option<f32> {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||
Self {
|
||||
@@ -52,26 +45,45 @@ impl TextView {
|
||||
.align(self.align)
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
||||
fn tex_region(&self, tex: &RenderedText) -> UiRegion {
|
||||
let region = tex.size.align(self.align);
|
||||
let dims = tex.handle.size();
|
||||
let mut region = region.offset(tex.top_left_offset);
|
||||
region.x.end = region.x.start + UiScalar::abs(dims.x);
|
||||
region.y.end = region.y.start + UiScalar::abs(dims.y);
|
||||
region
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
|
||||
let width = if self.attrs.wrap {
|
||||
Some(ctx.px_size().x)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
||||
self.width = width;
|
||||
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
|
||||
self.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
if width == self.width
|
||||
&& let Some(tex) = &self.tex
|
||||
&& !self.attrs.changed
|
||||
&& !self.buf.changed
|
||||
{
|
||||
return tex.clone();
|
||||
}
|
||||
self.tex.as_ref().unwrap()
|
||||
self.width = width;
|
||||
let font_system = &mut ctx.text.font_system;
|
||||
self.attrs.apply(font_system, &mut self.buf, width);
|
||||
self.buf.shape_until_scroll(font_system, false);
|
||||
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
|
||||
self.tex = Some(tex.clone());
|
||||
self.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
tex
|
||||
}
|
||||
pub fn tex(&self) -> Option<&RenderedText> {
|
||||
self.tex.as_ref()
|
||||
}
|
||||
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_empty()
|
||||
&& let Some(hint) = &self.hint
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
ctx.width(hint)
|
||||
} else {
|
||||
@@ -79,8 +91,9 @@ impl TextView {
|
||||
}
|
||||
}
|
||||
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_empty()
|
||||
&& let Some(hint) = &self.hint
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
ctx.height(hint)
|
||||
} else {
|
||||
@@ -88,39 +101,48 @@ impl TextView {
|
||||
}
|
||||
}
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
||||
let align = self.align;
|
||||
if self.is_empty() && self.hint.is_some() {
|
||||
let region = self.render(&mut painter.size_ctx()).size.align(align);
|
||||
if let Some(hint) = &self.hint {
|
||||
painter.widget(hint);
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
let tex = self.render(&mut painter.size_ctx());
|
||||
let region = tex.size.align(align);
|
||||
let within = region.within(&painter.region());
|
||||
painter.glyphs(tex, within);
|
||||
let region = self.tex_region(&tex);
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
painter.widget(hint);
|
||||
} else {
|
||||
painter.texture_within(&tex.handle, region);
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.buf.text().to_string()
|
||||
self.buf
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new(content: impl Into<String>) -> Self {
|
||||
let content: String = content.into();
|
||||
let attrs = TextAttrs::default();
|
||||
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
|
||||
Self {
|
||||
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None),
|
||||
content: content.into(),
|
||||
content: content.into().into(),
|
||||
view: TextView::new(buf, attrs, None),
|
||||
}
|
||||
}
|
||||
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
|
||||
fn update_buf(&mut self, ctx: &mut SizeCtx) {
|
||||
if self.content.changed {
|
||||
self.content.changed = false;
|
||||
self.view.buf.set_text(self.content.as_str());
|
||||
self.view.buf.set_text(
|
||||
&mut ctx.text.font_system,
|
||||
&self.content,
|
||||
&Attrs::new().family(self.view.attrs.family),
|
||||
SHAPING,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +164,16 @@ impl Widget for Text {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
|
||||
let start = a.min(b);
|
||||
let end = a.max(b);
|
||||
(start, end)
|
||||
}
|
||||
|
||||
pub fn edit_line(line: &mut BufferLine, text: String) {
|
||||
line.set_text(text, line.ending(), line.attrs_list().clone());
|
||||
}
|
||||
|
||||
impl Deref for Text {
|
||||
type Target = TextAttrs;
|
||||
|
||||
|
||||
+50
-7
@@ -131,18 +131,61 @@ widget_trait! {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CoreWidgetArr<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> {
|
||||
fn span(self, dir: Dir) -> SpanBuilder<Rsc, LEN, Wa, Tag>;
|
||||
fn stack(self) -> StackBuilder<Rsc, LEN, Wa, Tag>;
|
||||
pub trait CoreWidgetArr<Children, Rsc, Tag, GTag> {
|
||||
fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag>;
|
||||
fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag>;
|
||||
}
|
||||
|
||||
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
CoreWidgetArr<State, LEN, Wa, Tag> for Wa
|
||||
impl<Children: IntoWidgetVec<Rsc, Tag, GTag>, Rsc, Tag, GTag>
|
||||
CoreWidgetArr<Children, Rsc, Tag, GTag> for Children
|
||||
{
|
||||
fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag> {
|
||||
fn span(self, dir: Dir) -> SpanBuilder<Children, Rsc, Tag, GTag> {
|
||||
SpanBuilder::new(self, dir)
|
||||
}
|
||||
fn stack(self) -> StackBuilder<State, LEN, Wa, Tag> {
|
||||
fn stack(self) -> StackBuilder<Children, Rsc, Tag, GTag> {
|
||||
StackBuilder::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RscFnMap<Rsc> {
|
||||
type Input;
|
||||
fn rsc_map<O>(
|
||||
self,
|
||||
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
|
||||
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O>;
|
||||
}
|
||||
|
||||
impl<I: IntoIterator, Rsc> RscFnMap<Rsc> for I {
|
||||
type Input = I::Item;
|
||||
fn rsc_map<O>(
|
||||
self,
|
||||
f: impl Fn(Self::Input, &mut Rsc) -> O + Clone,
|
||||
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> O> {
|
||||
self.into_iter().map(move |i| {
|
||||
let f = f.clone();
|
||||
move |rsc: &mut Rsc| f(i, rsc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetFnMap<Rsc: UiRsc> {
|
||||
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
|
||||
self,
|
||||
f: impl Fn(WeakWidget) -> O + Clone,
|
||||
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget>;
|
||||
}
|
||||
|
||||
impl<I: IntoIterator, Rsc: UiRsc> WidgetFnMap<Rsc> for I
|
||||
where
|
||||
I::Item: WidgetIdFn<Rsc>,
|
||||
{
|
||||
fn widget_map<O: WidgetLike<Rsc, Tag>, Tag>(
|
||||
self,
|
||||
f: impl Fn(WeakWidget) -> O + Clone,
|
||||
) -> impl Iterator<Item = impl FnOnce(&mut Rsc) -> WeakWidget> {
|
||||
self.into_iter().map(move |f2| {
|
||||
let f = f.clone();
|
||||
move |rsc: &mut Rsc| f(f2(rsc)).add(rsc) as WeakWidget
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number
|
||||
//! of layers it walks. Recording only: the pass is built and dropped without
|
||||
//! being submitted, so this is the loop's cost and not the GPU's.
|
||||
//!
|
||||
//! cargo test --release --test draw_cost -- --ignored --nocapture
|
||||
//!
|
||||
//! Wall time is the wrong number to read for anything under a few percent --
|
||||
//! it varied by 2x between runs of one unchanged binary where instructions
|
||||
//! retired varied by 0.1%. Count those instead:
|
||||
//!
|
||||
//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored
|
||||
//!
|
||||
//! That is how `PrimitiveRender` was measured against a match in the renderer:
|
||||
//! 6 instructions per list drawn, against the ~5,400 wgpu spends recording
|
||||
//! one.
|
||||
//!
|
||||
//! The instance is leaked deliberately. A Vulkan loader may unload the driver
|
||||
//! when the last one drops, which can fault as a thread that used it exits --
|
||||
//! and every test runs on a spawned thread.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use iris::prelude::*;
|
||||
use iris_core::{
|
||||
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
||||
UiRegion, UiRenderNode, UiRenderState,
|
||||
};
|
||||
use wgpu::{Color as GpuColor, *};
|
||||
|
||||
const SIZE: u32 = 1024;
|
||||
const FRAMES: u32 = 200;
|
||||
/// Reported as the best of this many batches, since the mean moves by more
|
||||
/// than the thing being measured.
|
||||
const BATCHES: u32 = 8;
|
||||
|
||||
fn gpu() -> Option<(Device, Queue)> {
|
||||
// Probed rather than assumed: there may be no Vulkan adapter, and GL is
|
||||
// what is left when there is not.
|
||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||
{
|
||||
Ok(_) => all,
|
||||
Err(_) => Instance::new(InstanceDescriptor {
|
||||
backends: Backends::GL,
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
}),
|
||||
};
|
||||
// Leaked rather than dropped: see the note at the top of the file.
|
||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||
let adapter =
|
||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||
println!("adapter: {:?}", adapter.get_info());
|
||||
pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok()
|
||||
}
|
||||
|
||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||
SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
present_mode: PresentMode::Fifo,
|
||||
desired_maximum_frame_latency: 2,
|
||||
alpha_mode: CompositeAlphaMode::Auto,
|
||||
view_formats: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Every layer draws all three primitives, so the renderer takes a different
|
||||
/// path for each list it walks -- which is the case a single-primitive layer
|
||||
/// would never exercise. Images are bound per instance, so there are few.
|
||||
fn fill(
|
||||
ui: &mut UiData,
|
||||
render: &mut UiRenderState,
|
||||
layers: usize,
|
||||
per_layer: usize,
|
||||
) -> Vec<TextureHandle> {
|
||||
let rect = ui.primitives.kind::<RectPrimitive>();
|
||||
let glyph = ui.primitives.kind::<GlyphPrimitive>();
|
||||
let texture = ui.primitives.kind::<TexturePrimitive>();
|
||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||
let handles: Vec<_> = (0..4)
|
||||
.map(|_| ui.textures.add(image::RgbaImage::new(4, 4)))
|
||||
.collect();
|
||||
|
||||
let mut layer = 0;
|
||||
for _ in 0..layers {
|
||||
for _ in 0..per_layer {
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: rect,
|
||||
id,
|
||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: glyph,
|
||||
id,
|
||||
primitive: GlyphPrimitive {
|
||||
uv_min: vec2(0.0, 0.0),
|
||||
uv_max: vec2(1.0, 1.0),
|
||||
layer: 0,
|
||||
color: UiColor::WHITE,
|
||||
flags: 0,
|
||||
},
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
for h in &handles[..2] {
|
||||
render.layers.write(
|
||||
layer,
|
||||
PrimitiveInst {
|
||||
kind: texture,
|
||||
id,
|
||||
primitive: TexturePrimitive::from(h),
|
||||
region: UiRegion::FULL,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
},
|
||||
);
|
||||
}
|
||||
layer = render.layers.next(layer);
|
||||
}
|
||||
handles
|
||||
}
|
||||
|
||||
fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 {
|
||||
let format = TextureFormat::Bgra8Unorm;
|
||||
let mut node = UiRenderNode::new(device, &config(format));
|
||||
let mut ui = UiData::default();
|
||||
let mut render = UiRenderState::new();
|
||||
let _handles = fill(&mut ui, &mut render, layers, per_layer);
|
||||
node.update(device, queue, &mut ui, &mut render);
|
||||
|
||||
let target = device.create_texture(&TextureDescriptor {
|
||||
label: Some("draw cost"),
|
||||
size: Extent3d {
|
||||
width: SIZE,
|
||||
height: SIZE,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format,
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = target.create_view(&TextureViewDescriptor::default());
|
||||
|
||||
let record = |frames: u32| {
|
||||
let start = Instant::now();
|
||||
for _ in 0..frames {
|
||||
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||
{
|
||||
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(GpuColor::BLACK),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
node.draw(pass);
|
||||
}
|
||||
drop(encoder.finish());
|
||||
}
|
||||
start.elapsed().as_secs_f64() / frames as f64
|
||||
};
|
||||
record(FRAMES / 4);
|
||||
(0..BATCHES)
|
||||
.map(|_| record(FRAMES))
|
||||
.fold(f64::MAX, f64::min)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn draw_cost_by_layer_count() {
|
||||
let Some((device, queue)) = gpu() else {
|
||||
panic!("no wgpu device; see the this-machine-graphics notes");
|
||||
};
|
||||
println!(
|
||||
"layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}"
|
||||
);
|
||||
let base = frame_cost(&device, &queue, 1, 8) * 1e6;
|
||||
for layers in [8, 64, 256, 1024] {
|
||||
let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6;
|
||||
// Net of the empty pass, which is the same in any version of this.
|
||||
println!(
|
||||
"{layers:>5}: {per_frame:8.1} us ({:.3} us)",
|
||||
(per_frame - base).max(0.0) / layers as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use iris::prelude::*;
|
||||
|
||||
fn editor(text: &str, mode: EditMode) -> (TextEdit, TextData) {
|
||||
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
|
||||
(TextEdit::new(view, mode), TextData::default())
|
||||
}
|
||||
|
||||
fn press(edit: &mut TextEditCtx<'_>, x: f32) {
|
||||
edit.select(vec2(x, 10.0), vec2(400.0, 200.0), false, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_an_empty_field_places_input() {
|
||||
let (mut text, mut data) = editor("", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, 40.0);
|
||||
edit.insert("hello");
|
||||
|
||||
assert_eq!(edit.text.content(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preedit_replaces_the_previous_composition() {
|
||||
let (mut text, mut data) = editor("", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, 0.0);
|
||||
edit.replace(0, "に");
|
||||
edit.replace(1, "日本");
|
||||
|
||||
assert_eq!(edit.text.content(), "日本");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_respects_utf8_boundaries() {
|
||||
let (mut text, mut data) = editor("aé", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
press(&mut edit, f32::MAX);
|
||||
edit.backspace(false);
|
||||
|
||||
assert_eq!(edit.text.content(), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_replaces_the_selection() {
|
||||
let (mut text, mut data) = editor("hello", EditMode::SingleLine);
|
||||
let mut edit = TextEditCtx {
|
||||
text: &mut text,
|
||||
data: &mut data,
|
||||
};
|
||||
|
||||
edit.select_all();
|
||||
edit.insert("goodbye");
|
||||
|
||||
assert_eq!(edit.text.content(), "goodbye");
|
||||
}
|
||||
Reference in new issue
Block a user