Compare commits

...
Author SHA1 Message Date
iris c4b74f2367 Drop the prelude-bounds test
The rename is the guarantee; there is no way back to a name that is not
there any more.
2026-09-13 20:14:43 -04:00
irisandClaude Opus 5 f312db60c2 Rename the Sized widget to SetSize
It shadowed the marker trait, so a `?Sized` bound in any crate that imports
the prelude failed to resolve -- a compile error in someone else's code that
nothing here would have caught. Three files inside iris already imported
`std::marker::Sized` to get out from under it; they no longer need to.

`SetSize` rather than `FixedSize` because the size it sets need not be fixed:
`width(rest(2))` and `width(rel(0.5))` build the same widget. It pairs with
the `MaxSize` beside it -- one sets a length, the other caps it.

`tests/prelude_bounds.rs` is a compile-level guard: it fails to build if the
prelude shadows `Sized` again. The pad tab of the tabs example, which is what
uses `sized` and the flexible widths, renders pixel-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:34:52 -04:00
iris-aiandiris 00d2230b84 Build on wgpu 30 (#13)
Two majors, and the renderer is under everything else left to extract -- so it goes before the slices that would otherwise be written against wgpu 28 and then again against 30. `image` 0.25.6 -> 0.25.10 rides along. `winit` stays on 0.30.12, since 0.31 is only a prerelease and nothing here needs it; `parley` 0.11.1 is current.

What the API asked for, beyond the version:

- **An instance takes the display it will present on**, and GLES on Wayland needs it, so the window the surface is made from is handed over with it. That one matters for Android rather than for this machine.
- **`get_current_texture` returns a status rather than a `Result`**, which replaced an `unwrap` that would have panicked on a resize or an occluded window: reconfigure when the surface is outdated, lost or suboptimal, and skip the frame when there is nothing to draw into.
- **Presenting moved to the queue**, still after `pre_present_notify`.
- **Bind group and vertex buffer layouts are sparse**, so each slot states `Some(layout)`.

Verified the same way as #11: the tabs example with two runtime-added images, an image alone in a layer, and glyphs from a four-page atlas all render identically. `tests/draw_cost.rs` gives 33.6/167/587/2855 us per frame at 8/64/256/1024 layers, against 33.3/161/588/2903 on wgpu 28 -- no change.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: #13
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 19:07:47 -04:00
iris-aiandiris b234497d21 Draw the glyph atlas as an array texture and images with their own bind groups + primitive rendering overhaul
Replaces the bindless `binding_array<texture_2d<f32>>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all.

The two things being bound want opposite treatment, so they are now split:

- **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback.
- **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture.

`Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them.

Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted:

- The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`.
- `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below.

### Verification

```
cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
```

All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`.

Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless.

Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: #11
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 18:56:59 -04:00
iris-aiandiris 0f6a28b4dd Move text layout and rendering to Parley (#10)
Replace the cosmic-text path with Parley layout and Swash rasterization, backed by shared glyph-atlas pages. Shaping, editing, rasterization, and glyph rendering move together because they share the text buffer and rendered-glyph types; splitting them further would require a temporary renderer that is immediately removed.

This is reconstructed rather than replayed from the extraction history. It also fixes issues found during review:

- texture binding changes remain set when an atlas patch follows a new page
- pressing an empty field places a caret and accepts input
- selection motion delegates collapse behavior to Parley
- character deletion follows logical clusters rather than visual neighbors
- the unused root-level Swash dependency is omitted

Four public-behavior integration tests live in `tests/text_edit.rs`: empty-field input, multibyte IME preedit replacement, UTF-8-safe backspace, and selection replacement. The old twelve-test inline block and implementation-restating cases are omitted.

Every added source comment was manually reviewed. Comments that narrated implementation or history were removed; retained comments document cache/rasterization keys, GPU upload constraints, focus representation, bidi geometry, or IME semantics.

Known limitation: atlas pages currently grow without eviction. Each page is 4 MiB on CPU and GPU. An arbitrary cap would leave cached rendered-text UVs pointing at reused glyph slots, so bounding this safely needs a later generation/invalidation change.

This changes public text types and signatures. GPU glyph rendering is covered by compilation rather than a live-surface test.

Verified with:

- `cargo fmt --all --check`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test --workspace` (four integration tests pass)

Cargo still reports inherited future-incompatibility notices for existing wgpu/winit dependencies; there are no current clippy warnings.

---------

Co-authored-by: iris <2+iris@noreply.localhost>
Reviewed-on: #10
Reviewed-by: iris <2+iris@noreply.localhost>
Co-authored-by: AIris <4+iris-ai@noreply.localhost>
2026-09-13 03:39:23 -04:00
iris b90c855cf5 Merge pull request 'Preserve primitive count recursion' (#9) from iris-ai/iris:split/08-primitive-count into main
Reviewed-on: #9
2026-09-13 01:11:15 -04:00
iris 3b96324333 Remove the redundant macro comment 2026-09-13 01:10:37 -04:00
iris 4767384b08 Preserve primitive count recursion 2026-09-13 01:07:52 -04:00
iris 0191f2081b Merge pull request 'Keep unsafe reference helpers internal' (#7) from iris-ai/iris:split/06-restrict-unsafe-utils into main
Reviewed-on: #7
2026-09-13 01:05:38 -04:00
iris 472736a292 Keep the unsafe helper change minimal 2026-09-13 01:04:10 -04:00
iris 6e271e8aee Merge pull request 'Initialize the window uniform from the surface' (#8) from iris-ai/iris:split/07-initialize-window-uniform into main
Reviewed-on: #8
2026-09-13 01:01:04 -04:00
iris a1ff76776c Keep unsafe reference helpers internal 2026-09-13 00:58:56 -04:00
iris cb9cad38f2 Initialize the window uniform from the surface 2026-09-13 00:58:56 -04:00
iris db9b0f21d5 Merge pull request 'Notify winit before presenting frames' (#6) from iris-ai/iris:split/05-pre-present-notify into main
Reviewed-on: #6
2026-09-13 00:54:58 -04:00
iris 2b6a6ab378 Notify winit before presenting frames 2026-09-13 00:53:16 -04:00
iris ec2b5d4c1d Merge pull request 'Use vsync by default' (#5) from iris-ai/iris:split/04-vsync-default into main
Reviewed-on: #5
2026-09-13 00:52:03 -04:00
iris 780ac82b27 Use a vsynced presentation mode by default 2026-09-13 00:51:20 -04:00
iris 465e43075e Merge pull request 'Decouple iris-core from winit' (#4) from iris-ai/iris:split/03-core-window-independence into main
Reviewed-on: #4
2026-09-13 00:50:33 -04:00
iris 0c9a39fd06 Remove redundant resize documentation 2026-09-13 00:49:09 -04:00
iris 936fbdd8ce Merge pull request 'Request a frame after resize' (#3) from iris-ai/iris:split/02-resize-redraw into main
Reviewed-on: #3
Reviewed-by: iris <2+iris@noreply.localhost>
2026-09-13 00:46:56 -04:00
iris 3eaded125e Merge branch 'split/02-resize-redraw' into split/03-core-window-independence 2026-09-13 00:45:37 -04:00
iris 23270e49fb Drop the redundant redraw predicate test 2026-09-13 00:45:26 -04:00
iris bc6cdd13c9 Decouple iris-core from winit 2026-09-13 00:38:29 -04:00
iris 072f1e31ad Keep the redraw invariant concise 2026-09-13 00:36:23 -04:00
iris 42753141b7 Merge pull request 'Build Iris on the current nightly' (#2) from iris-ai/iris:split/01-toolchain into main
Reviewed-on: #2
Reviewed-by: iris <2+iris@noreply.localhost>
2026-09-13 00:33:51 -04:00
irisandClaude Opus 5 6884160bfe Make iris ask for the frame a resize needs
`update` redrew everything when `resized` was set, but `needs_redraw` --
which is what decides whether to request a frame at all -- did not know
about `resized`. A condition in one and not the other is a frame nobody
asks for and a stale window. The two share one `needs_redraw_all` now.

Latent on Wayland, because winit requests a redraw after a resize by
itself; a resize changes neither the root nor any widget, so nothing else
here would have asked. It stops being latent on Android, where the
surface work will not have winit underneath it and every rotation and
keyboard open is a resize.

This is not a fix for the startup defect recorded in RUST.md, where the
window keeps its pre-configure layout: that reproduces with this change
in place, and the frame it needs is requested and drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:26:19 -04:00
iris fae21a1991 Build on current nightly 2026-09-13 00:24:29 -04:00
iris 7b54aaf3c4 readme 2026-01-29 16:39:19 -05:00
iris 17c436d944 stuff 2026-01-22 23:33:46 -05:00
iris a592318a6f impl idlike for widgetview 2026-01-20 18:11:21 -05:00
iris 796bc41752 didn't actually remove state on widget remove (mem leak) 2026-01-19 21:39:13 -05:00
iris 7bafb04a34 widget state 2026-01-19 20:39:58 -05:00
iris 06dd015092 finished moving out render_state 2026-01-19 18:00:24 -05:00
iris 79813db3ba work 2026-01-12 18:40:27 -05:00
iris a9c76e4326 lol bounds 2026-01-05 17:50:18 -05:00
iris 32e45e9238 griefed last commit 2026-01-05 17:20:11 -05:00
iris 2fadfe4b82 app event stuff bruh 2026-01-05 17:16:56 -05:00
iris d11107f965 widget view 2026-01-05 17:01:09 -05:00
iris 07de7c8722 update deps 2026-01-04 14:45:18 -05:00
iris f2ac6f195f remove typed stuff / just specify rsc if needed 2026-01-03 18:06:05 -05:00
iris 59901b6580 tasks initial impl (still working on task_on trait method) 2026-01-03 16:26:23 -05:00
iris 5da1e9e767 separate state from rsc 2026-01-01 22:18:08 -05:00
83 changed files with 4636 additions and 3153 deletions

No files matched your search

Generated
+848 -596
View File
File diff suppressed because it is too large. Load diff
+11 -7
View File
@@ -1,6 +1,5 @@
[package]
name = "iris"
default-run = "test"
version.workspace = true
edition.workspace = true
@@ -9,13 +8,16 @@ edition.workspace = true
[dependencies]
iris-core = { workspace = true }
iris-macro = { workspace = true }
cosmic-text = { workspace = true }
unicode-segmentation = { workspace = true }
parley = { workspace = true }
winit = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] }
pollster = { workspace = true }
wgpu = { workspace = true }
image = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
[dev-dependencies]
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
[workspace]
members = ["core", "macro"]
@@ -27,12 +29,14 @@ edition = "2024"
[workspace.dependencies]
pollster = "0.4.0"
winit = "0.30.12"
wgpu = "27.0.1"
wgpu = "30.0.1"
bytemuck = "1.23.1"
image = "0.25.6"
cosmic-text = "0.15.0"
unicode-segmentation = "1.12.0"
image = "0.25.10"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
log = "0.4.29"
arboard = "3.6.1"
iris-core = { path = "core" }
iris-macro = { path = "macro" }
tokio = "1.49.0"
+1 -25
View File
@@ -1,19 +1,6 @@
images
settings (sampler)
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
consider typed TextureHandle<T> variants for distinct texture uses
WidgetRef<W> or smth instead of Id
enum that's either an Id or an actual concrete instance of W
@@ -24,17 +11,6 @@ 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..??
+3 -2
View File
@@ -4,9 +4,10 @@ version.workspace = true
edition.workspace = true
[dependencies]
winit = { workspace = true }
wgpu = { workspace = true }
bytemuck ={ workspace = true }
image = { workspace = true }
cosmic-text = { workspace = true }
parley = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true }
log = { workspace = true }
+11 -13
View File
@@ -1,24 +1,22 @@
use crate::{HasUi, StateLike, WidgetIdFn, WidgetLike, WidgetRef};
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
pub trait WidgetAttr<State, W: ?Sized> {
pub trait WidgetAttr<Rsc, W: ?Sized> {
type Input;
fn run(state: &mut State, id: WidgetRef<W>, input: Self::Input);
fn run(rsc: &mut Rsc, id: WeakWidget<W>, input: Self::Input);
}
pub trait Attrable<State, W: ?Sized, Tag> {
fn attr<A: WidgetAttr<State, W>>(self, input: A::Input) -> impl WidgetIdFn<State, W>;
pub trait Attrable<Rsc, W: ?Sized, Tag> {
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>;
}
impl<State: HasUi + StateLike<State>, WL: WidgetLike<State, Tag>, Tag>
Attrable<State, WL::Widget, Tag> for WL
{
fn attr<A: WidgetAttr<State, WL::Widget>>(
impl<Rsc: UiRsc, WL: WidgetLike<Rsc, Tag>, Tag> Attrable<Rsc, WL::Widget, Tag> for WL {
fn attr<A: WidgetAttr<Rsc, WL::Widget>>(
self,
input: A::Input,
) -> impl WidgetIdFn<State, WL::Widget> {
|state| {
let id = self.add(state);
A::run(state, id, input);
) -> impl WidgetIdFn<Rsc, WL::Widget> {
|rsc| {
let id = self.add(rsc);
A::run(rsc, id, input);
id
}
}
+11 -109
View File
@@ -1,116 +1,18 @@
use crate::{HasEvents, HasState, HasUi, StateLike, Ui, Widget, WidgetRef};
use std::ops::{Deref, DerefMut};
use crate::{HasEvents, WeakWidget, Widget};
pub struct EventCtx<'a, State, Data> {
pub state: &'a mut State,
pub data: &'a mut Data,
pub struct EventCtx<'a, Rsc: HasEvents, Data> {
pub state: &'a mut Rsc::State,
pub data: Data,
}
pub struct EventIdCtx<'a, State, Data, W: ?Sized> {
pub widget: WidgetRef<W>,
pub state: &'a mut State,
pub data: &'a mut Data,
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> {
pub widget: WeakWidget<W>,
pub state: &'a mut Rsc::State,
pub data: Data,
}
impl<State: HasUi, Data, W: ?Sized> Deref for EventIdCtx<'_, State, Data, W> {
type Target = State;
fn deref(&self) -> &Self::Target {
self.state
impl<Rsc: HasEvents, Data, W: Widget> EventIdCtx<'_, Rsc, Data, W> {
pub fn widget<'a>(&self, rsc: &'a mut Rsc) -> &'a mut W {
&mut rsc.ui_mut().widgets[self.widget]
}
}
impl<State: HasUi, Data, W: ?Sized> DerefMut for EventIdCtx<'_, State, Data, W> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.state
}
}
impl<State: HasUi, Data, W: Widget> EventIdCtx<'_, State, Data, W> {
pub fn widget(&mut self) -> &mut W {
&mut self.state.get_mut()[self.widget]
}
}
impl<State: HasUi, Data, W: Widget> HasUi for EventIdCtx<'_, State, Data, W> {
fn get(&self) -> &Ui {
self.state.ui()
}
fn get_mut(&mut self) -> &mut Ui {
self.state.ui_mut()
}
}
impl<State: HasUi, Data, W: Widget> HasState for EventIdCtx<'_, State, Data, W> {
type State = State;
}
impl<State: HasEvents<State = State>, Data, W: Widget> HasEvents
for EventIdCtx<'_, State, Data, W>
{
fn get(&self) -> &super::EventManager<Self::State> {
self.state.events()
}
fn get_mut(&mut self) -> &mut super::EventManager<Self::State> {
self.state.events_mut()
}
}
impl<State, Data, W: Widget> StateLike<State> for EventIdCtx<'_, State, Data, W> {
fn as_state(&mut self) -> &mut State {
self.state
}
}
// fn test() {
// use crate::*;
// struct ClientRsc;
// impl<State, Data> HasUi for EventCtx<'_, State, Data> {
// fn get(&self) -> &Ui {
// todo!()
// }
//
// fn get_mut(&mut self) -> &mut Ui {
// todo!()
// }
// }
// fn on(_: impl for<'a> EventFn<ClientRsc, &'a mut i32>) {}
//
// pub trait WidgetLike<State: HasUi, Tag>: Sized {
// type Widget: Widget + ?Sized + std::marker::Unsize<dyn Widget>;
//
// fn add(self, state: &mut State) -> WidgetHandle<Self::Widget>;
//
// fn with_id<W2>(
// self,
// f: impl FnOnce(&mut State, WidgetHandle<Self::Widget>) -> WidgetHandle<W2>,
// ) -> impl WidgetIdFn<State, W2> {
// move |state| {
// let id = self.add(state);
// f(state, id)
// }
// }
//
// fn set_root(self, state: &mut State) {
// state.get_mut().root = Some(self.add(state));
// }
//
// fn handles(self, state: &mut State) -> WidgetHandles<Self::Widget> {
// self.add(state).handles()
// }
// }
//
// pub struct WidgetTag;
// impl<State: HasUi, W: Widget> WidgetLike<State, WidgetTag> for W {
// type Widget = W;
// fn add(self, state: &mut State) -> WidgetHandle<W> {
// state.get_mut().add_widget(self)
// }
// }
//
// on(move |ctx| {
// ().add(ctx);
// });
// }
+42 -36
View File
@@ -1,16 +1,16 @@
use crate::{
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, IdLike, LayerId, Widget,
WidgetEventFn, WidgetId, WidgetRef,
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
WeakWidget, WidgetEventFn, WidgetId,
util::{HashMap, HashSet, TypeMap},
};
use std::{any::TypeId, rc::Rc};
pub struct EventManager<State> {
pub struct EventManager<Rsc> {
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
types: TypeMap<dyn EventManagerLike<State>>,
types: TypeMap<dyn EventManagerLike<Rsc>>,
}
impl<State> Default for EventManager<State> {
impl<Rsc> Default for EventManager<Rsc> {
fn default() -> Self {
Self {
widget_to_types: Default::default(),
@@ -19,26 +19,27 @@ impl<State> Default for EventManager<State> {
}
}
impl<State: 'static> EventManager<State> {
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<State, E::Event> {
impl<Rsc: HasEvents + 'static> EventManager<Rsc> {
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<Rsc, E::Event> {
self.types.type_or_default()
}
pub fn register<W: Widget + ?Sized, E: EventLike>(
pub fn register<I: IdLike + 'static, E: EventLike>(
&mut self,
id: WidgetRef<W>,
id: I,
event: E,
f: impl for<'a> WidgetEventFn<State, <E::Event as Event>::Data<'a>, W>,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, I::Widget>,
) {
let i = id.id();
self.get_type::<E>().register(id, event, f);
self.widget_to_types
.entry(id.id())
.entry(i)
.or_default()
.insert(Self::type_key::<E>());
}
pub fn type_key<E: EventLike>() -> TypeId {
TypeId::of::<TypeEventManager<State, E::Event>>()
TypeId::of::<TypeEventManager<Rsc, E::Event>>()
}
}
@@ -48,7 +49,7 @@ pub trait EventsLike {
fn undraw(&mut self, active: &ActiveData);
}
impl<State: 'static> EventsLike for EventManager<State> {
impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
fn remove(&mut self, id: WidgetId) {
for t in self.widget_to_types.get(&id).into_flat_iter() {
self.types.get_mut(t).unwrap().remove(id);
@@ -74,14 +75,14 @@ pub trait EventManagerLike<State> {
fn undraw(&mut self, data: &ActiveData);
}
type EventData<State, E> = (E, Rc<dyn for<'a> EventFn<State, <E as Event>::Data<'a>>>);
pub struct TypeEventManager<State, E: Event> {
type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>>);
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
map: HashMap<WidgetId, Vec<EventData<State, E>>>,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
}
impl<State, E: Event> EventManagerLike<State> for TypeEventManager<State, E> {
impl<Rsc: HasEvents, E: Event> EventManagerLike<Rsc> for TypeEventManager<Rsc, E> {
fn remove(&mut self, id: WidgetId) {
self.map.remove(&id);
for layer in self.active.values_mut() {
@@ -102,7 +103,7 @@ impl<State, E: Event> EventManagerLike<State> for TypeEventManager<State, E> {
}
}
impl<State, E: Event> Default for TypeEventManager<State, E> {
impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self {
Self {
active: Default::default(),
@@ -111,23 +112,25 @@ impl<State, E: Event> Default for TypeEventManager<State, E> {
}
}
impl<State: 'static, E: Event> TypeEventManager<State, E> {
fn register<W: Widget + ?Sized>(
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
fn register<I: IdLike + 'static>(
&mut self,
widget: WidgetRef<W>,
widget: I,
event: impl EventLike<Event = E>,
f: impl for<'a> WidgetEventFn<State, E::Data<'a>, W>,
f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, I::Widget>,
) {
let event = event.into_event();
self.map.entry(widget.id()).or_default().push((
event,
Rc::new(move |ctx| {
let mut test = EventIdCtx {
widget,
state: ctx.state,
data: ctx.data,
};
f(&mut test);
Rc::new(move |ctx, rsc| {
f(
EventIdCtx {
widget: WeakWidget::new(widget.id()),
state: ctx.state,
data: ctx.data,
},
rsc,
);
}),
));
}
@@ -135,15 +138,18 @@ impl<State: 'static, E: Event> TypeEventManager<State, E> {
pub fn run_fn<'a>(
&mut self,
id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, State, E::Data<'b>>) + 'a {
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx| {
move |ctx, rsc| {
for (e, f) in fs {
if e.should_run(ctx.data) {
f(&mut EventCtx {
state: ctx.state,
data: ctx.data,
})
if let Some(data) = e.should_run(&ctx.data) {
f(
EventCtx {
state: ctx.state,
data,
},
rsc,
)
}
}
}
+12 -9
View File
@@ -7,11 +7,11 @@ pub use manager::*;
pub use rsc::*;
pub trait Event: Sized + 'static + Clone {
type Data<'a> = ();
type Data<'a>: Clone = ();
type State: Default = ();
#[allow(unused_variables)]
fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
true
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone())
}
}
@@ -28,14 +28,17 @@ impl<E: Event> EventLike for E {
}
}
pub trait EventFn<State, Data>: Fn(&mut EventCtx<State, Data>) + 'static {}
impl<State, F: Fn(&mut EventCtx<State, Data>) + 'static, Data> EventFn<State, Data> for F {}
pub trait EventFn<Rsc: HasEvents, Data>: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static {}
impl<Rsc: HasEvents, F: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static, Data> EventFn<Rsc, Data>
for F
{
}
pub trait WidgetEventFn<State, Data, W: ?Sized>:
Fn(&mut EventIdCtx<State, Data, W>) + 'static
pub trait WidgetEventFn<Rsc: HasEvents, Data, W: ?Sized>:
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static
{
}
impl<State, F: Fn(&mut EventIdCtx<State, Data, W>) + 'static, Data, W: ?Sized>
WidgetEventFn<State, Data, W> for F
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized>
WidgetEventFn<Rsc, Data, W> for F
{
}
+14 -21
View File
@@ -1,41 +1,34 @@
use crate::{
Event, EventCtx, EventLike, EventManager, HasUi, IdLike, Widget, WidgetEventFn, WidgetRef,
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
};
pub trait HasState {
type State: HasUi;
pub trait HasState: 'static {
type State;
}
pub trait HasEvents: Sized + HasState + HasUi {
fn get(&self) -> &EventManager<Self::State>;
fn get_mut(&mut self) -> &mut EventManager<Self::State>;
fn events(&self) -> &EventManager<Self::State> {
HasEvents::get(self)
}
fn events_mut(&mut self) -> &mut EventManager<Self::State> {
HasEvents::get_mut(self)
}
pub trait HasEvents: Sized + UiRsc + HasState {
fn events(&self) -> &EventManager<Self>;
fn events_mut(&mut self) -> &mut EventManager<Self>;
fn register_event<W: Widget + ?Sized, E: EventLike>(
&mut self,
id: WidgetRef<W>,
id: WeakWidget<W>,
event: E,
f: impl for<'a> WidgetEventFn<Self::State, <E::Event as Event>::Data<'a>, W>,
) where
Self::State: 'static,
{
f: impl for<'a> WidgetEventFn<Self, <E::Event as Event>::Data<'a>, W>,
) {
self.events_mut().register(id, event, f);
}
}
pub trait RunEvents: HasEvents + HasState<State = Self> + 'static {
pub trait RunEvents: HasEvents {
fn run_event<E: EventLike>(
&mut self,
id: impl IdLike,
data: &mut <E::Event as Event>::Data<'_>,
data: <E::Event as Event>::Data<'_>,
state: &mut Self::State,
) {
let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state: self, data })
f(EventCtx { state, data }, self)
}
}
impl<T: HasEvents + HasState<State = Self> + 'static> RunEvents for T {}
impl<T: HasEvents> RunEvents for T {}
-3
View File
@@ -2,12 +2,9 @@
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(const_cmp)]
#![feature(const_destruct)]
#![feature(portable_simd)]
#![feature(associated_type_defaults)]
#![feature(unsize)]
#![feature(coerce_unsized)]
+5 -5
View File
@@ -5,19 +5,19 @@ pub const trait UiNum {
fn to_f32(self) -> f32;
}
impl const UiNum for f32 {
const impl UiNum for f32 {
fn to_f32(self) -> f32 {
self
}
}
impl const UiNum for u32 {
const impl UiNum for u32 {
fn to_f32(self) -> f32 {
self as f32
}
}
impl const UiNum for i32 {
const impl UiNum for i32 {
fn to_f32(self) -> f32 {
self as f32
}
@@ -27,7 +27,7 @@ pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
Vec2::new(x.to_f32(), y.to_f32())
}
impl<T: const UiNum + Copy> const From<T> for Vec2 {
const impl<T: const UiNum + Copy> From<T> for Vec2 {
fn from(v: T) -> Self {
Self {
x: v.to_f32(),
@@ -36,7 +36,7 @@ impl<T: const UiNum + Copy> const From<T> for Vec2 {
}
}
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for Vec2
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2
where
(T, U): const Destruct,
{
+1 -1
View File
@@ -187,7 +187,7 @@ impl From<CardinalAlign> for Align {
}
}
impl const From<RegionAlign> for UiVec2 {
const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self {
Self::rel(align.rel())
}
+2 -2
View File
@@ -74,14 +74,14 @@ pub const trait AxisT {
}
pub struct XAxis;
impl const AxisT for XAxis {
const impl AxisT for XAxis {
fn get() -> Axis {
Axis::X
}
}
pub struct YAxis;
impl const AxisT for YAxis {
const impl AxisT for YAxis {
fn get() -> Axis {
Axis::Y
}
+2 -2
View File
@@ -124,13 +124,13 @@ impl Display for UiVec2 {
impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y);
impl const From<Vec2> for UiVec2 {
const impl From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self {
Self::abs(abs)
}
}
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for UiVec2
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where
(T, U): const Destruct,
{
+8 -2
View File
@@ -10,6 +10,12 @@ 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);
@@ -144,7 +150,7 @@ impl ColorNum for f32 {
unsafe impl bytemuck::Pod for Color<u8> {}
impl const F32Conversion for f32 {
const impl F32Conversion for f32 {
fn to(self) -> f32 {
self
}
@@ -153,7 +159,7 @@ impl const F32Conversion for f32 {
}
}
impl const F32Conversion for u8 {
const impl F32Conversion for u8 {
fn to(self) -> f32 {
self as f32 / 255.0
}
+3 -3
View File
@@ -1,7 +1,7 @@
use std::ops::{Index, IndexMut};
use crate::{
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::to_mut,
};
@@ -39,7 +39,7 @@ struct Child {
tail: usize,
}
pub type PrimitiveLayers = Layers<Primitives>;
pub type DrawLayers = Layers<LayerDraws>;
impl<T: Default> Layers<T> {
pub fn new() -> Layers<T> {
@@ -119,7 +119,7 @@ impl<T: Default> Layers<T> {
}
}
impl PrimitiveLayers {
impl DrawLayers {
pub fn write<P: Primitive>(
&mut self,
layer: LayerId,
+230 -135
View File
@@ -1,60 +1,66 @@
use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
use cosmic_text::{
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
SwashContent,
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 image::{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_system: FontSystem,
pub swash_cache: SwashCache,
glyph_cache: Vec<(Placement, CacheKey, Color)>,
pub font_ctx: FontContext,
pub layout_ctx: LayoutContext<UiColor>,
scale_ctx: ScaleContext,
pub atlas: GlyphAtlas,
}
impl Default for TextData {
fn default() -> Self {
Self {
font_system: FontSystem::new(),
swash_cache: SwashCache::new(),
glyph_cache: Default::default(),
font_ctx: FontContext::new(),
layout_ctx: LayoutContext::new(),
scale_ctx: ScaleContext::new(),
atlas: GlyphAtlas::default(),
}
}
}
#[derive(Clone, Copy)]
#[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)]
pub struct TextAttrs {
pub color: UiColor,
pub font_size: f32,
pub line_height: f32,
pub family: Family<'static>,
pub family: Family,
pub wrap: bool,
/// inner alignment of text region (within where it's drawn)
pub align: RegionAlign,
}
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;
pub const LINE_HEIGHT_MULT: f32 = 1.1;
impl Default for TextAttrs {
fn default() -> Self {
@@ -70,118 +76,207 @@ impl Default for TextAttrs {
}
}
pub const LINE_HEIGHT_MULT: f32 = 1.1;
/// 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);
}
}
impl TextData {
pub fn draw(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
textures: &mut Textures,
) -> RenderedText {
// 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,
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();
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);
}
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
}
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();
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
if let Some(entry) = self.atlas.get(&glyph.key) {
return entry;
}
RenderedText {
handle: textures.add(image),
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height),
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
}
}
}
#[derive(Clone)]
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 handle: TextureHandle,
pub top_left_offset: Vec2,
pub glyphs: Vec<PlacedGlyph>,
pub size: Vec2,
pub color: UiColor,
}
impl TextData {
pub fn render(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer);
RenderedText {
glyphs,
size: buffer.size(),
color: attrs.color,
}
}
}
+41 -16
View File
@@ -1,7 +1,4 @@
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use crate::util::{RefCounter, Vec2};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
@@ -10,7 +7,7 @@ use std::{
#[derive(Debug, Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
slot: u32,
size: Vec2,
counter: RefCounter,
send: Sender<u32>,
@@ -29,14 +26,26 @@ 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),
}
@@ -54,14 +63,8 @@ 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 {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
slot: self.push(image),
size,
counter: RefCounter::new(),
send: self.send.clone(),
@@ -81,6 +84,23 @@ 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;
@@ -99,14 +119,19 @@ 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 {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
/// Index into `Textures`, and into the renderer's parallel slots.
pub fn slot(&self) -> u32 {
self.slot
}
pub fn size(&self) -> Vec2 {
self.size
@@ -116,7 +141,7 @@ impl TextureHandle {
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx);
let _ = self.send.send(self.slot);
}
}
}
@@ -125,7 +150,7 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.inner.view_idx as usize].as_ref().unwrap()
self.images[index.slot as usize].as_ref().unwrap()
}
}
+245
View File
@@ -0,0 +1,245 @@
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,
}
+1 -5
View File
@@ -12,20 +12,16 @@ 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; 7] = vertex_attr_array![
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x2,
3 => Float32x2,
4 => Uint32,
5 => Uint32,
6 => Uint32,
];
pub fn desc() -> VertexBufferLayout<'static> {
+223 -203
View File
@@ -1,204 +1,232 @@
use std::num::NonZero;
use crate::{
Ui,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::HashMap,
UiData, UiRenderState,
render::{data::PrimitiveInstance, util::ArrBuf},
util::{HashMap, Vec2},
};
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 SHAPE_SHADER: &str = include_str!("./shader.wgsl");
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
pub struct UiRenderNode {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
shared_layout: BindGroupLayout,
shared_group: BindGroup,
format: TextureFormat,
pipeline: RenderPipeline,
/// One per registered primitive, in id order.
primitives: Vec<PrimitivePipeline>,
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>,
primitives: PrimitiveBuffers,
primitive_group: BindGroup,
data: ArrBuf<u8>,
group: Option<BindGroup>,
/// What the primitive asked to keep per instance, if anything.
bindings: Vec<u32>,
}
impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
pass.set_bind_group(0, &self.shared_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
if layer.instance.len() == 0 {
continue;
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,
},
);
}
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);
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) {
pub fn update(
&mut self,
device: &Device,
queue: &Queue,
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, primitives) in ui.layers.iter_mut() {
for (i, draws) in ui_render.layers.iter_mut() {
self.active.push(i);
for change in primitives.apply_free() {
if let Some(inst) = ui.active.get_mut(&change.id) {
for change in draws.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old {
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
h.inst_idx = change.new;
break;
}
}
}
}
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,
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);
}
});
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;
draws.updated = false;
}
}
let mut changed = false;
changed |= self.textures.update(&mut ui.textures);
for primitive in &mut self.primitives {
primitive.render.update(ui);
}
if ui.masks.changed {
ui.masks.changed = false;
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);
if self.masks.update(device, queue, &ui.masks[..]) {
self.shared_group = Self::shared_group(
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
);
}
}
}
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform {
width: size.width as f32,
height: size.height as f32,
width: size.x,
height: size.y,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
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();
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform {
width: config.width as f32,
height: config.height as f32,
};
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]),
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
});
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 shared_layout = Self::shared_layout(device);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
Self {
shared_layout,
shared_group,
format: config.format,
primitives: Vec::new(),
window_buffer,
layers: HashMap::default(),
active: Vec::new(),
masks,
}
}
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
push_constant_ranges: &[],
/// 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 &registry.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 = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
layout: Some(&pipeline_layout),
device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: VertexState {
module: &shader,
module: &module,
entry_point: Some("vs_main"),
buffers: &[PrimitiveInstance::desc()],
buffers: &[Some(PrimitiveInstance::desc())],
compilation_options: Default::default(),
},
fragment: Some(FragmentState {
module: &shader,
module: &module,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: config.format,
format,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
@@ -219,92 +247,44 @@ impl UiRenderNode {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
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"),
})
}
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 {
/// What every draw in the ui is given: the window and the masks.
fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
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),
},
count: Some(NonZero::new(limits.max_textures).unwrap()),
count: None,
},
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: None,
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
},
count: None,
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
fn rsc_group(
fn shared_group(
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
window: &Buffer,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
@@ -312,45 +292,85 @@ impl UiRenderNode {
entries: &[
BindGroupEntry {
binding: 0,
resource: BindingResource::TextureViewArray(&tex_manager.views()),
resource: window.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(),
},
],
label: Some("ui rsc"),
label: Some("ui shared"),
})
}
pub fn view_count(&self) -> usize {
self.textures.view_count()
/// 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 struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
impl RenderLayer {
fn new() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
primitives: Vec::new(),
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
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(),
}
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
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"),
}));
}
}
}
+156
View File
@@ -0,0 +1,156 @@
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: &[],
})
}
+308 -195
View File
@@ -1,114 +1,247 @@
use std::ops::{Deref, DerefMut};
use std::{any::TypeId, marker::PhantomData};
use crate::{
Color, UiRegion, WidgetId,
Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
page::GlyphRender,
texture::ImageRender,
},
util::{HashMap, Vec2},
};
use bytemuck::Pod;
use wgpu::*;
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
pub struct Primitives {
/// 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 {
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 Primitives {
impl Default for LayerDraws {
fn default() -> Self {
Self {
instances: Default::default(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
primitives: Vec::new(),
updated: true,
}
}
}
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 {
impl LayerDraws {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
kind,
id,
primitive,
region,
@@ -116,65 +249,67 @@ impl Primitives {
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
self.updated = true;
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)
// 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
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
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 })
})
self.primitives
.iter_mut()
.enumerate()
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
.flat_map(|(kind, list)| list.apply_free(kind))
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.updated = true;
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
self.list(h).free(h.inst_idx)
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
&mut self.list(h).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,
}
@@ -182,29 +317,12 @@ 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)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
@@ -212,6 +330,10 @@ 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 {
@@ -223,60 +345,51 @@ impl RectPrimitive {
}
}
#[repr(C)]
/// `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)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
pub slot: u32,
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
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))
}
}
impl<T> PrimitiveVec<T> {
pub fn new() -> Self {
impl From<&TextureHandle> for TexturePrimitive {
fn from(handle: &TextureHandle) -> Self {
Self {
vec: Vec::new(),
free: Vec::new(),
slot: handle.slot(),
}
}
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
}
}
-178
View File
@@ -1,178 +0,0 @@
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) binding: u32,
@location(4) idx: u32,
@location(5) 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;
}
+33
View File
@@ -0,0 +1,33 @@
// 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);
}
+96
View File
@@ -0,0 +1,96 @@
// 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;
}
+39
View File
@@ -0,0 +1,39 @@
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;
}
+10
View File
@@ -0,0 +1,10 @@
// 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));
}
+203 -89
View File
@@ -1,59 +1,119 @@
use image::{DynamicImage, EncodableLayout};
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
use wgpu::{util::DeviceExt, *};
use crate::{TextureUpdate, Textures};
use crate::{
PatchRect, TextureUpdate, Textures, UiData,
render::{
TexturePrimitive,
primitive::{ListDraw, PrimitiveRender},
},
};
/// 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,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>,
slots: Vec<Option<ImageGpu>>,
}
struct ImageGpu {
/// Kept for `patch`, which needs the texture rather than the view.
texture: Texture,
group: BindGroup,
}
impl GpuTextures {
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) => 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(),
}
pub fn new(device: &Device, queue: &Queue) -> Self {
Self {
device: device.clone(),
queue: queue.clone(),
slots: Vec::new(),
}
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());
}
fn create_view(&self, image: &DynamicImage) -> TextureView {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
for update in textures.updates() {
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,
}
}
}
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();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: None,
label: Some("image"),
size: Extent3d {
width,
height,
@@ -63,65 +123,119 @@ impl GpuTextures {
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
rgba.as_bytes(),
);
texture.create_view(&TextureViewDescriptor::default())
let view = texture.create_view(&TextureViewDescriptor::default());
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
ImageGpu { texture, group }
}
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,
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 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
}
}
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,
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
if rect.width == 0 || rect.height == 0 {
return;
}
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),
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
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),
})
}
pub fn default_sampler(device: &Device) -> Sampler {
+10 -6
View File
@@ -21,17 +21,25 @@ impl<T: Pod> ArrBuf<T> {
_pd: PhantomData,
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) {
if self.len != data.len() {
/// 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 {
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 {
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
usage,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.len
}
}
-232
View File
@@ -1,232 +0,0 @@
use crate::{
ActiveData, Axis, EventsLike, Painter, SizeCtx, Ui, UiRegion, UiVec2, WidgetId,
render::MaskIdx,
util::{HashSet, forget_ref},
};
use std::ops::{Deref, DerefMut};
/// state maintained between widgets during painting
pub struct DrawState<'a> {
pub(super) ui: &'a mut Ui,
pub(super) events: &'a mut dyn EventsLike,
draw_started: HashSet<WidgetId>,
}
impl<'a> DrawState<'a> {
pub fn new(ui: &'a mut Ui, events: &'a mut dyn EventsLike) -> Self {
Self {
ui,
events,
draw_started: Default::default(),
}
}
pub fn redraw_updates(&mut self) {
while let Some(&id) = self.widgets.needs_redraw.iter().next() {
self.redraw(id);
}
self.ui.free(self.events);
}
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId) {
self.widgets.needs_redraw.remove(&id);
self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first
for axis in [Axis::X, Axis::Y] {
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
&& let Some(current) = self.active.get(&id)
&& let Some(pid) = current.parent
{
self.cache.size.axis_dyn(axis).remove(&id);
let new = self.size_ctx(id, outer).len_axis(id, axis);
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
if new != old {
self.redraw(pid);
}
}
}
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.remove(id, false) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
);
}
pub(super) fn size_ctx<'b>(&'b mut self, source: WidgetId, outer: UiVec2) -> SizeCtx<'b> {
SizeCtx {
source,
cache: &mut self.ui.cache,
text: &mut self.ui.text,
textures: &mut self.ui.textures,
widgets: &self.ui.widgets,
outer,
output_size: self.ui.output_size,
id: source,
}
}
pub fn redraw_all(&mut self) {
// free all resources & cache
for (_, active) in self.ui.active.drain() {
self.events.undraw(&active);
}
self.ui.cache.clear();
self.ui.free(self.events);
self.layers.clear();
self.widgets.needs_redraw.clear();
if let Some(id) = &self.ui.root {
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None);
}
}
pub(super) fn draw_inner(
&mut self,
layer: usize,
id: WidgetId,
region: UiRegion,
parent: Option<WidgetId>,
mask: MaskIdx,
old_children: Option<Vec<WidgetId>>,
) {
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.ui.active.get_mut(&id)
&& !self.ui.widgets.needs_redraw.contains(&id)
{
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false).unwrap();
old_children = active.children;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
state: self,
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
};
let mut widget = painter.state.widgets.get_dyn_dynamic(id);
widget.draw(&mut painter);
drop(widget);
let Painter {
state: _,
region,
mask,
textures,
primitives,
children,
layer,
id,
} = painter;
// add to active
let active = ActiveData {
id,
region,
parent,
textures,
primitives,
children,
mask,
layer,
};
// remove old children that weren't kept
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c);
}
}
// update modules
self.events.draw(&active);
self.active.insert(id, active);
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.ui.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.ui.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
self.masks.remove(mask);
}
}
active.textures.clear();
self.textures.free();
if undraw {
self.events.undraw(active);
}
}
active
}
fn remove_rec(&mut self, id: WidgetId) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c);
}
}
inst
}
}
impl Deref for DrawState<'_> {
type Target = Ui;
fn deref(&self) -> &Self::Target {
self.ui
}
}
impl DerefMut for DrawState<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ui
}
}
+27 -193
View File
@@ -1,217 +1,51 @@
use crate::{
EventsLike, IdLike, Mask, PixelRegion, PrimitiveLayers, TextData, TextureHandle, Textures,
Widget, WidgetHandle, WidgetId, Widgets,
ui::draw_state::DrawState,
util::{HashMap, TrackedArena, Vec2},
};
use image::DynamicImage;
use std::{
ops::{Index, IndexMut},
sync::mpsc::{Receiver, channel},
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
};
mod active;
mod cache;
mod draw_state;
mod painter;
mod render_state;
mod size;
mod state;
pub use active::*;
use cache::*;
pub use painter::Painter;
pub use painter::{Painter, PrimitiveLike};
pub use render_state::*;
pub use size::*;
pub struct Ui {
// TODO: edit visibilities
#[derive(Default)]
pub struct UiData {
pub widgets: Widgets,
// retained painter state
pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers,
/// Every primitive this ui can draw.
pub primitives: PrimitiveRegistry,
pub textures: Textures,
pub text: TextData,
output_size: Vec2,
pub masks: TrackedArena<Mask, u32>,
pub cache: Cache,
pub root: Option<WidgetHandle>,
old_root: Option<WidgetId>,
recv: Receiver<WidgetId>,
resized: bool,
}
pub trait HasUi: Sized {
fn get(&self) -> &Ui;
fn get_mut(&mut self) -> &mut Ui;
fn ui(&self) -> &Ui {
self.get()
}
fn ui_mut(&mut self) -> &mut Ui {
self.get_mut()
}
}
pub trait UiRsc {
fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData;
impl HasUi for Ui {
fn get(&self) -> &Ui {
self
}
#[allow(unused_variables)]
fn on_add(&mut self, id: WeakWidget) {}
#[allow(unused_variables)]
fn on_remove(&mut self, id: WidgetId) {}
#[allow(unused_variables)]
fn on_draw(&mut self, active: &ActiveData) {}
#[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {}
fn get_mut(&mut self) -> &mut Ui {
self
fn widgets(&self) -> &Widgets {
&self.ui().widgets
}
}
impl Ui {
/// useful for debugging
pub fn set_label(&mut self, id: impl IdLike, label: String) {
self.widgets.data_mut(id.id()).unwrap().label = label;
fn widgets_mut(&mut self) -> &mut Widgets {
&mut self.ui_mut().widgets
}
pub fn label(&self, id: impl IdLike) -> &String {
&self.widgets.data(id.id()).unwrap().label
}
pub fn new() -> Self {
Self::default()
}
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
where
I::Widget: Sized + Widget,
{
self.widgets.get(id)
}
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
where
I::Widget: Sized + Widget,
{
self.widgets.get_mut(id)
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.textures.add(image)
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.output_size = size.into();
self.resized = true;
}
pub fn update(&mut self, events: &mut dyn EventsLike) {
if !self.widgets.waiting.is_empty() {
let len = self.widgets.waiting.len();
let all: Vec<_> = self
.widgets
.waiting
.iter()
.map(|&w| format!("'{}' ({w:?})", self.label(w)))
.collect();
panic!(
"{len} widget(s) were never upgraded\n\
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
weak widgets: {all:#?}"
);
}
if self.root_changed() {
DrawState::new(self, events).redraw_all();
self.old_root = self.root.as_ref().map(|r| r.id());
} else if self.widgets.has_updates() {
DrawState::new(self, events).redraw_updates();
}
if self.resized {
self.resized = false;
DrawState::new(self, events).redraw_all();
}
}
/// free any resources that don't have references anymore
fn free(&mut self, events: &mut dyn EventsLike) {
for id in self.recv.try_iter() {
events.remove(id);
self.widgets.delete(id);
}
self.textures.free();
}
pub fn root_changed(&self) -> bool {
self.root.as_ref().map(|r| r.id()) != self.old_root
}
pub fn needs_redraw(&self) -> bool {
self.root_changed() || self.widgets.has_updates()
}
pub fn num_widgets(&self) -> usize {
self.widgets.len()
}
pub fn active_widgets(&self) -> usize {
self.active.len()
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
}
}
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let region = self.active.get(&id.id())?.region;
Some(region.to_px(self.output_size))
}
pub fn debug(&self, label: &str) -> impl Iterator<Item = &ActiveData> {
self.active.iter().filter_map(move |(&id, inst)| {
let l = self.widgets.label(id);
if l == label { Some(inst) } else { None }
})
}
}
impl<I: IdLike> Index<I> for Ui
where
I::Widget: Sized + Widget,
{
type Output = I::Widget;
fn index(&self, id: I) -> &Self::Output {
self.get(&id).unwrap()
}
}
impl<I: IdLike> IndexMut<I> for Ui
where
I::Widget: Sized + Widget,
{
fn index_mut(&mut self, id: I) -> &mut Self::Output {
self.get_mut(&id).unwrap()
}
}
impl Default for Ui {
fn default() -> Self {
let (send, recv) = channel();
Self {
widgets: Widgets::new(send),
active: Default::default(),
layers: Default::default(),
masks: Default::default(),
text: Default::default(),
textures: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO,
root: None,
old_root: None,
recv,
resized: false,
fn free(&mut self) {
while let Some(id) = self.widgets_mut().free_next() {
self.on_remove(id);
}
self.ui_mut().textures.free();
}
}
+99 -40
View File
@@ -1,14 +1,17 @@
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, Widget, WidgetHandle, WidgetId,
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
ui::draw_state::DrawState,
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,
},
util::Vec2,
};
/// makes your surfaces look pretty
pub struct Painter<'a, 'b> {
pub(super) state: &'a mut DrawState<'b>,
pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
pub(super) region: UiRegion,
pub(super) mask: MaskIdx,
@@ -19,87 +22,118 @@ pub struct Painter<'a, 'b> {
pub(super) id: WidgetId,
}
impl<'a, 'c> Painter<'a, 'c> {
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.state.masks.push_ref(self.mask);
self.rsc.ui_mut().masks.push_ref(self.mask);
}
self.primitives.push(h);
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
let primitive = primitive.into_primitive(self);
self.primitive_at(primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE);
self.mask = self.state.masks.push(Mask { region });
self.mask = self.rsc.ui_mut().masks.push(Mask { region });
}
/// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &WidgetHandle<W>) {
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.widget_at(id, self.region);
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &WidgetHandle<W>, region: UiRegion) {
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region));
}
fn widget_at<W: ?Sized>(&mut self, id: &WidgetHandle<W>, region: UiRegion) {
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.children.push(id.id());
self.state
.draw_inner(self.layer, id.id(), region, Some(self.id), self.mask, None);
self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
self.mask,
None,
self.rsc,
);
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region.within(&self.region));
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(&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 {
self.state
.ui
.text
.draw(buffer, attrs, &mut self.state.ui.textures)
// 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 region(&self) -> UiRegion {
self.region
}
pub fn size<W: ?Sized + Widget>(&mut self, id: &WidgetHandle<W>) -> Size {
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
self.size_ctx().size(id)
}
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &WidgetHandle<W>, axis: Axis) -> Len {
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
@@ -115,7 +149,7 @@ impl<'a, 'c> Painter<'a, 'c> {
}
pub fn text_data(&mut self) -> &mut TextData {
&mut self.state.text
&mut self.rsc.ui_mut().text
}
pub fn child_layer(&mut self) {
@@ -127,7 +161,7 @@ impl<'a, 'c> Painter<'a, 'c> {
}
pub fn label(&self) -> &str {
&self.state.widgets.data(self.id).unwrap().label
&self.rsc.widgets().data(self.id).unwrap().label
}
pub fn id(&self) -> &WidgetId {
@@ -135,6 +169,31 @@ impl<'a, 'c> Painter<'a, 'c> {
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
self.state.size_ctx(self.id, self.region.size())
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()
}
}
+323
View File
@@ -0,0 +1,323 @@
use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, 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(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>,
resized: bool,
draw_started: HashSet<WidgetId>,
}
impl UiRenderState {
pub fn new() -> Self {
Self {
active: Default::default(),
layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO,
old_root: None,
resized: false,
draw_started: Default::default(),
}
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.output_size = size.into();
self.resized = true;
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
let widgets = rsc.widgets();
let len = widgets.waiting.len();
let all: Vec<_> = widgets
.waiting
.iter()
.map(|&w| format!("'{}' ({w:?})", widgets.label(w)))
.collect();
panic!(
"{len} widget(s) were never upgraded\n\
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
weak widgets: {all:#?}"
);
}
let root = root.into();
if self.needs_full_redraw(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
self.resized = false;
} else if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
}
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
self.clear(rsc);
// free all resources & cache
if let Some(id) = root {
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
}
}
// TODO: should prolly make a DrawInfo struct or smth for everything other than rsc
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_inner(
&mut self,
layer: usize,
id: WidgetId,
region: UiRegion,
parent: Option<WidgetId>,
mask: MaskIdx,
old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc,
) {
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id)
&& !rsc.widgets().needs_redraw.contains(&id)
{
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap();
old_children = active.children;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
state: self,
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
rsc,
};
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
widget.draw(&mut painter);
drop(widget);
let Painter {
state: _,
rsc: _,
region,
mask,
textures,
primitives,
children,
layer,
id,
} = painter;
// add to active
let active = ActiveData {
id,
region,
parent,
textures,
primitives,
children,
mask,
layer,
};
// remove old children that weren't kept
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c, rsc);
}
}
rsc.on_draw(&active);
self.active.insert(id, active);
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
rsc.ui_mut().masks.remove(mask);
}
}
active.textures.clear();
rsc.ui_mut().textures.free();
if undraw {
rsc.on_undraw(active);
}
}
active
}
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c, rsc);
}
}
inst
}
fn clear(&mut self, rsc: &mut dyn UiRsc) {
for (_, active) in self.active.drain() {
rsc.on_undraw(&active);
}
self.cache.clear();
self.layers.clear();
rsc.widgets_mut().needs_redraw.clear();
rsc.free();
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
self.redraw(id, rsc);
}
rsc.free();
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
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()
}
pub fn active_widgets(&self) -> usize {
self.active.len()
}
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
self.active.iter().filter_map(move |(&id, inst)| {
let l = widgets.label(id);
if l == label { Some(inst) } else { None }
})
}
pub fn debug_layers(&self) {
for ((idx, depth), draws) 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(", "));
}
}
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let region = self.active.get(&id.id())?.region;
Some(region.to_px(self.output_size))
}
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first
for axis in [Axis::X, Axis::Y] {
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
&& let Some(current) = self.active.get(&id)
&& let Some(pid) = current.parent
{
self.cache.size.axis_dyn(axis).remove(&id);
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
if new != old {
self.redraw(pid, rsc);
}
}
}
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.remove(id, false, rsc) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
rsc,
);
}
pub(super) fn size_ctx<'b>(
&'b mut self,
source: WidgetId,
outer: UiVec2,
rsc: &'b mut dyn UiRsc,
) -> SizeCtx<'b> {
let ui = rsc.ui_mut();
SizeCtx {
source,
cache: &mut self.cache,
text: &mut ui.text,
widgets: &ui.widgets,
outer,
output_size: self.output_size,
id: source,
}
}
}
impl Default for UiRenderState {
fn default() -> Self {
Self::new()
}
}
+9 -6
View File
@@ -1,11 +1,10 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, 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,
@@ -33,7 +32,6 @@ 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,
@@ -76,8 +74,13 @@ impl SizeCtx<'_> {
self.output_size
}
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
self.text.draw(buffer, attrs, self.textures)
pub fn draw_text(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width)
}
pub fn label(&self, id: WidgetId) -> &String {
-3
View File
@@ -1,3 +0,0 @@
// pub struct DynState {
//
// }
+9 -8
View File
@@ -9,15 +9,16 @@ pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self;
}
impl const DivOr for f32 {
const impl DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs;
if res.is_nan() { other } else { res }
}
}
impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const
LerpUtil for T
const impl<
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
> LerpUtil for T
{
/// linear interpolation
/// from * (1.0 - self) + to * self
@@ -37,7 +38,7 @@ macro_rules! impl_op {
use super::*;
#[allow(unused_imports)]
use std::ops::*;
impl const $op for $T {
const impl $op for $T {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
@@ -46,12 +47,12 @@ macro_rules! impl_op {
}
}
}
impl const $opa for $T {
const impl $opa for $T {
fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs);
}
}
impl const $op<f32> for $T {
const impl $op<f32> for $T {
type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output {
@@ -60,7 +61,7 @@ macro_rules! impl_op {
}
}
}
impl const $op<$T> for f32 {
const impl $op<$T> for f32 {
type Output = $T;
fn $fn(self, rhs: $T) -> Self::Output {
@@ -69,7 +70,7 @@ macro_rules! impl_op {
}
}
}
impl const $opa<f32> for $T {
const impl $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) {
*self = self.$fn(rhs);
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub use id::*;
pub use math::*;
pub use refcount::*;
pub use slot::*;
pub use trust::*;
pub(crate) use trust::*;
pub use typemap::*;
pub use vec2::*;
+3 -3
View File
@@ -1,15 +1,15 @@
#[allow(clippy::missing_safety_doc)]
pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)]
pub unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
pub(crate) 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 unsafe fn to_mut<T>(x: &T) -> &mut T {
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
#[allow(mutable_transmutes)]
unsafe {
std::mem::transmute::<&T, &mut T>(x)
+1 -1
View File
@@ -67,7 +67,7 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; x y);
impl_op!(Vec2 Div div; x y);
impl const DivOr for Vec2 {
const impl DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self {
Self {
x: self.x.div_or(rhs.x, other.x),
+29 -36
View File
@@ -1,7 +1,7 @@
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
use crate::{
HasUi, Widget,
UiRsc, Widget,
util::{RefCounter, SlotId},
};
@@ -12,7 +12,7 @@ pub type WidgetId = SlotId;
/// a signal is sent to the owning UI to clean up the resources.
///
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
pub struct WidgetHandle<W: ?Sized = dyn Widget> {
pub struct StrongWidget<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId,
counter: RefCounter,
send: Sender<WidgetId>,
@@ -21,24 +21,19 @@ pub struct WidgetHandle<W: ?Sized = dyn Widget> {
/// A weak handle to a widget.
/// Will not keep it alive, but can still be used for indexing like WidgetHandle.
pub struct WidgetRef<W: ?Sized = dyn Widget> {
pub struct WeakWidget<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId,
#[allow(unused)]
ty: *const W,
}
pub struct WidgetHandles<W: ?Sized = dyn Widget> {
pub h: WidgetHandle<W>,
pub r: WidgetRef<W>,
}
impl<W: Widget + ?Sized + Unsize<dyn Widget>> WidgetHandle<W> {
pub fn any(self) -> WidgetHandle<dyn Widget> {
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> {
pub fn any(self) -> StrongWidget<dyn Widget> {
self
}
}
impl<W: ?Sized> WidgetHandle<W> {
impl<W: ?Sized> StrongWidget<W> {
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
Self {
id,
@@ -56,18 +51,13 @@ impl<W: ?Sized> WidgetHandle<W> {
self.counter.refs()
}
pub fn weak(&self) -> WidgetRef<W> {
pub fn weak(&self) -> WeakWidget<W> {
let Self { ty, id, .. } = *self;
WidgetRef { ty, id }
}
pub fn handles(self) -> WidgetHandles<W> {
let r = self.weak();
WidgetHandles { h: self, r }
WeakWidget { ty, id }
}
}
impl<W: ?Sized> WidgetRef<W> {
impl<W: ?Sized> WeakWidget<W> {
pub(crate) fn new(id: WidgetId) -> Self {
Self { id, ty: null_ptr() }
}
@@ -77,12 +67,12 @@ impl<W: ?Sized> WidgetRef<W> {
}
#[track_caller]
pub fn upgrade(self, ui: &mut impl HasUi) -> WidgetHandle<W> {
ui.ui_mut().widgets.upgrade(self)
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> {
ui.widgets_mut().upgrade(self)
}
}
impl<W: ?Sized> Drop for WidgetHandle<W> {
impl<W: ?Sized> Drop for StrongWidget<W> {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.id);
@@ -90,29 +80,29 @@ impl<W: ?Sized> Drop for WidgetHandle<W> {
}
}
pub trait WidgetIdFn<State, W: ?Sized = dyn Widget>: FnOnce(&mut State) -> WidgetRef<W> {}
impl<State, W: ?Sized, F: FnOnce(&mut State) -> WidgetRef<W>> WidgetIdFn<State, W> for F {}
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
pub trait IdLike {
type Widget: ?Sized;
fn id(&self) -> WidgetId;
}
impl<W: ?Sized> IdLike for &WidgetHandle<W> {
impl<W: ?Sized> IdLike for &StrongWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
}
}
impl<W: ?Sized> IdLike for WidgetHandle<W> {
impl<W: ?Sized> IdLike for StrongWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
}
}
impl<W: ?Sized> IdLike for WidgetRef<W> {
impl<W: ?Sized> IdLike for WeakWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
@@ -126,38 +116,38 @@ impl IdLike for WidgetId {
}
}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetHandle<U>> for WidgetHandle<T> {}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetRef<U>> for WidgetRef<T> {}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<StrongWidget<U>> for StrongWidget<T> {}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WeakWidget<U>> for WeakWidget<T> {}
impl<W: ?Sized> Clone for WidgetRef<W> {
impl<W: ?Sized> Clone for WeakWidget<W> {
fn clone(&self) -> Self {
*self
}
}
impl<W: ?Sized> Copy for WidgetRef<W> {}
impl<W: ?Sized> PartialEq for WidgetRef<W> {
impl<W: ?Sized> Copy for WeakWidget<W> {}
impl<W: ?Sized> PartialEq for WeakWidget<W> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<W> PartialEq for WidgetHandle<W> {
impl<W> PartialEq for StrongWidget<W> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<W> std::fmt::Debug for WidgetHandle<W> {
impl<W> std::fmt::Debug for StrongWidget<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl<'a, W: Widget + 'a, State: HasUi> FnOnce<(&'a mut State,)> for WidgetRef<W> {
impl<'a, W: Widget + 'a, State: UiRsc> FnOnce<(&'a mut State,)> for WeakWidget<W> {
type Output = &'a mut W;
extern "rust-call" fn call_once(self, args: (&'a mut State,)) -> Self::Output {
&mut args.0.ui_mut()[self]
&mut args.0.widgets_mut()[self]
}
}
@@ -169,3 +159,6 @@ fn null_ptr<W: ?Sized>() -> *const W {
unsafe { std::mem::transmute_copy(&[0usize; 1]) }
}
}
unsafe impl<W: ?Sized> Send for WeakWidget<W> {}
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
+21 -32
View File
@@ -1,55 +1,44 @@
use crate::{HasUi, Ui};
use crate::UiRsc;
use super::*;
use std::marker::Unsize;
pub trait StateLike<State> {
fn as_state(&mut self) -> &mut State;
}
impl StateLike<Ui> for Ui {
fn as_state(&mut self) -> &mut Ui {
self
}
}
pub trait WidgetLike<State: HasUi + StateLike<State>, Tag>: Sized {
pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<Self::Widget>;
fn add(self, rsc: &mut Rsc) -> WeakWidget<Self::Widget>;
fn add_strong(self, state: &mut impl StateLike<State>) -> WidgetHandle<Self::Widget> {
self.add(state).upgrade(state.as_state().ui_mut())
fn add_strong(self, rsc: &mut Rsc) -> StrongWidget<Self::Widget> {
self.add(rsc).upgrade(rsc)
}
fn with_id<W2>(
self,
f: impl FnOnce(&mut State, WidgetRef<Self::Widget>) -> WidgetRef<W2>,
) -> impl WidgetIdFn<State, W2> {
f: impl FnOnce(&mut Rsc, WeakWidget<Self::Widget>) -> WeakWidget<W2>,
) -> impl WidgetIdFn<Rsc, W2> {
move |state| {
let id = self.add(state);
f(state, id)
}
}
fn set_root(self, state: &mut impl StateLike<State>) {
let id = self.add(state);
let ui = state.as_state().ui_mut();
ui.root = Some(id.upgrade(ui));
}
fn handles(self, state: &mut impl StateLike<State>) -> WidgetHandles<Self::Widget> {
self.add(state).upgrade(state.as_state().ui_mut()).handles()
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) {
let id = self.add_strong(rsc);
root.set_root(id);
}
}
pub trait WidgetArrLike<State, const LEN: usize, Tag> {
pub trait HasRoot {
fn set_root(&mut self, root: StrongWidget);
}
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
#[track_caller]
fn add(self, state: &mut impl StateLike<State>) -> WidgetArr<LEN>;
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>;
}
impl<State, const LEN: usize> WidgetArrLike<State, LEN, ArrTag> for WidgetArr<LEN> {
fn add(self, _: &mut impl StateLike<State>) -> WidgetArr<LEN> {
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> {
self
}
}
@@ -60,12 +49,12 @@ macro_rules! impl_widget_arr {
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
};
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
impl<State: HasUi + StateLike<State>, $($W: WidgetLike<State, $Tag>,$Tag,)*> WidgetArrLike<State, $n, ($($Tag,)*)> for ($($W,)*) {
fn add(self, state: &mut impl StateLike<State>) -> WidgetArr<$n> {
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> WidgetArrLike<Rsc, $n, ($($Tag,)*)> for ($($W,)*) {
fn add(self, rsc: &mut Rsc) -> WidgetArr<$n> {
#[allow(non_snake_case)]
let ($($W,)*) = self;
WidgetArr::new(
[$($W.add(state).upgrade(state.as_state().ui_mut()),)*],
[$($W.add(rsc).upgrade(rsc),)*],
)
}
}
+8 -6
View File
@@ -5,12 +5,14 @@ mod data;
mod handle;
mod like;
mod tag;
mod view;
mod widgets;
pub use data::*;
pub use handle::*;
pub use like::*;
pub use tag::*;
pub use view::*;
pub use widgets::*;
pub trait Widget: Any {
@@ -59,27 +61,27 @@ pub trait WidgetFn<State, W: Widget + ?Sized>: FnOnce(&mut State) -> W {}
impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {}
pub struct WidgetArr<const LEN: usize> {
pub arr: [WidgetHandle; LEN],
pub arr: [StrongWidget; LEN],
}
impl<const LEN: usize> WidgetArr<LEN> {
pub fn new(arr: [WidgetHandle; LEN]) -> Self {
pub fn new(arr: [StrongWidget; LEN]) -> Self {
Self { arr }
}
}
pub trait WidgetOption<State> {
fn get(self, state: &mut State) -> Option<WidgetHandle>;
fn get(self, state: &mut State) -> Option<StrongWidget>;
}
impl<State> WidgetOption<State> for () {
fn get(self, _: &mut State) -> Option<WidgetHandle> {
fn get(self, _: &mut State) -> Option<StrongWidget> {
None
}
}
impl<State, F: FnOnce(&mut State) -> Option<WidgetHandle>> WidgetOption<State> for F {
fn get(self, state: &mut State) -> Option<WidgetHandle> {
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F {
fn get(self, state: &mut State) -> Option<StrongWidget> {
self(state)
}
}
+31 -26
View File
@@ -1,58 +1,63 @@
use super::*;
use crate::HasUi;
use crate::UiRsc;
use std::marker::Unsize;
pub struct WidgetTag;
impl<State: HasUi + StateLike<State>, W: Widget> WidgetLike<State, WidgetTag> for W {
impl<Rsc: UiRsc, W: Widget> WidgetLike<Rsc, WidgetTag> for W {
type Widget = W;
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
state.as_state().get_mut().widgets.add_weak(self)
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
let w = rsc.ui_mut().widgets.add_weak(self);
rsc.on_add(w);
w
}
}
pub struct FnTag;
impl<State: HasUi + StateLike<State>, W: Widget, F: FnOnce(&mut State) -> W>
WidgetLike<State, FnTag> for F
{
impl<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F {
type Widget = W;
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
self(state.as_state()).add(state)
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
self(rsc).add(rsc)
}
}
pub trait WidgetFnTrait<State> {
pub trait WidgetFnTrait<Rsc> {
type Widget: Widget;
fn run(self, state: &mut State) -> Self::Widget;
fn run(self, rsc: &mut Rsc) -> Self::Widget;
}
pub struct FnTraitTag;
impl<State: HasUi + StateLike<State>, T: WidgetFnTrait<State>> WidgetLike<State, FnTraitTag> for T {
impl<Rsc: UiRsc, T: WidgetFnTrait<Rsc>> WidgetLike<Rsc, FnTraitTag> for T {
type Widget = T::Widget;
#[track_caller]
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<T::Widget> {
self.run(state.as_state()).add(state)
fn add(self, rsc: &mut Rsc) -> WeakWidget<T::Widget> {
self.run(rsc).add(rsc)
}
}
pub struct IdTag;
impl<State: HasUi + StateLike<State>, W: ?Sized + Widget + Unsize<dyn Widget>>
WidgetLike<State, IdTag> for WidgetRef<W>
pub struct RefTag;
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>> WidgetLike<Rsc, RefTag>
for WeakWidget<W>
{
type Widget = W;
fn add(self, _: &mut impl StateLike<State>) -> WidgetRef<W> {
fn add(self, _: &mut Rsc) -> WeakWidget<W> {
self
}
}
pub struct IdFnTag;
impl<
State: HasUi + StateLike<State>,
W: ?Sized + Widget + Unsize<dyn Widget>,
F: FnOnce(&mut State) -> WidgetRef<W>,
> WidgetLike<State, IdFnTag> for F
pub struct RefFnTag;
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>, F: FnOnce(&mut Rsc) -> WeakWidget<W>>
WidgetLike<Rsc, RefFnTag> for F
{
type Widget = W;
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
self(state.as_state())
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
self(rsc)
}
}
pub struct ViewTag;
impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
type Widget = V::Widget;
fn add(self, _: &mut Rsc) -> WeakWidget<Self::Widget> {
self.root()
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::marker::Unsize;
use crate::{IdLike, WeakWidget, Widget};
pub trait WidgetView {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
fn root(&self) -> WeakWidget<Self::Widget>;
}
pub trait HasWidget {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
}
impl<W: Widget + Unsize<dyn Widget> + ?Sized> HasWidget for WeakWidget<W> {
type Widget = W;
}
impl<WV: WidgetView> IdLike for WV {
type Widget = WV::Widget;
fn id(&self) -> super::WidgetId {
self.root().id
}
}
+50 -14
View File
@@ -1,7 +1,7 @@
use std::sync::mpsc::Sender;
use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{
IdLike, Widget, WidgetData, WidgetHandle, WidgetId, WidgetRef,
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
};
@@ -9,16 +9,19 @@ pub struct Widgets {
pub needs_redraw: HashSet<WidgetId>,
vec: SlotVec<WidgetData>,
send: Sender<WidgetId>,
recv: Receiver<WidgetId>,
pub(crate) waiting: HashSet<WidgetId>,
}
impl Widgets {
pub fn new(send: Sender<WidgetId>) -> Self {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
needs_redraw: Default::default(),
vec: Default::default(),
waiting: Default::default(),
send,
recv,
}
}
@@ -61,25 +64,27 @@ impl Widgets {
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
}
pub fn add_strong<W: Widget>(&mut self, widget: W) -> WidgetHandle<W> {
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
WidgetHandle::new(id, self.send.clone())
StrongWidget::new(id, self.send.clone())
}
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WidgetRef<W> {
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
self.waiting.insert(id);
WidgetRef::new(id)
WeakWidget::new(id)
}
#[track_caller]
pub fn upgrade<W: ?Sized>(&mut self, rf: WidgetRef<W>) -> WidgetHandle<W> {
pub fn upgrade<W: ?Sized>(&mut self, rf: WeakWidget<W>) -> StrongWidget<W> {
if !self.waiting.remove(&rf.id()) {
let label = self.label(rf);
let id = rf.id();
panic!("widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two")
panic!(
"widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two"
)
}
WidgetHandle::new(rf.id(), self.send.clone())
StrongWidget::new(rf.id(), self.send.clone())
}
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
@@ -90,14 +95,19 @@ impl Widgets {
&self.data(id.id()).unwrap().label
}
/// useful for debugging
pub fn set_label(&mut self, id: impl IdLike, label: String) {
self.data_mut(id.id()).unwrap().label = label;
}
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id())
}
pub fn delete(&mut self, id: impl IdLike) {
self.vec.free(id.id());
// not sure if there's any point in this
// self.updates.remove(&id);
pub fn free_next(&mut self) -> Option<WidgetId> {
let next = self.recv.try_recv().ok()?;
self.vec.free(next);
Some(next)
}
#[allow(clippy::len_without_is_empty)]
@@ -106,4 +116,30 @@ impl Widgets {
}
}
impl Default for Widgets {
fn default() -> Self {
Self::new()
}
}
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
impl<I: IdLike> std::ops::Index<I> for Widgets
where
I::Widget: Sized + Widget,
{
type Output = I::Widget;
fn index(&self, id: I) -> &Self::Output {
self.get(&id).unwrap()
}
}
impl<I: IdLike> std::ops::IndexMut<I> for Widgets
where
I::Widget: Sized + Widget,
{
fn index_mut(&mut self, id: I) -> &mut Self::Output {
self.get_mut(&id).unwrap()
}
}
+12 -11
View File
@@ -1,20 +1,21 @@
use iris::prelude::*;
fn main() {
App::<State>::run();
DefaultApp::<State>::run();
}
#[default_ui_state]
struct State {}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
let mut ui = Ui::new();
rect(Color::RED).set_root(&mut ui);
Self {
ui,
ui_state,
events: EventManager::default(),
}
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
File renamed without changes.
+57 -57
View File
@@ -1,26 +1,25 @@
use cosmic_text::Family;
use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent;
iris::state_prelude!(ClientRsc);
use iris::prelude::*;
type ClientRsc = DefaultRsc<Client>;
fn main() {
App::<Client>::run();
DefaultApp::<Client>::run();
}
#[default_ui_state]
#[derive(DefaultUiState)]
pub struct Client {
info: WidgetRef<Text>,
ui_state: DefaultUiState,
info: WeakWidget<Text>,
}
impl DefaultAppState for Client {
fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
let mut rsc = ClientRsc {
ui: Ui::new(),
ui_state,
events: EventManager::default(),
};
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
@@ -43,7 +42,7 @@ impl DefaultAppState for Client {
.width(rest(3)),
)
.span(Dir::RIGHT)
.add(&mut rsc);
.add(rsc);
let span_test = (
rrect.color(Color::GREEN).width(100),
@@ -54,30 +53,30 @@ impl DefaultAppState for Client {
rrect.color(Color::RED).width(100),
)
.span(Dir::LEFT)
.add(&mut rsc);
.add(rsc);
let span_add = Span::empty(Dir::RIGHT).add(&mut rsc);
let span_add = Span::empty(Dir::RIGHT).add(rsc);
let add_button = rect(Color::LIME)
.radius(30)
.on(CursorSense::click(), move |ctx| {
.on(CursorSense::click(), move |_, rsc| {
let child = image(include_bytes!("assets/sungals.png"))
.center()
.add_strong(ctx);
span_add(ctx).push(child);
.add_strong(rsc);
span_add(rsc).push(child);
})
.sized((150, 150))
.align(Align::BOT_RIGHT);
let del_button = rect(Color::RED)
.radius(30)
.on(CursorSense::click(), move |ctx| {
span_add(ctx).pop();
.on(CursorSense::click(), move |_, rsc| {
span_add(rsc).pop();
})
.sized((150, 150))
.align(Align::BOT_LEFT);
let span_add_test = (span_add, add_button, del_button).stack().add(&mut rsc);
let span_add_test = (span_add, add_button, del_button).stack().add(rsc);
let btext = |content| wtext(content).size(30);
@@ -100,18 +99,18 @@ impl DefaultAppState for Client {
wtext("pretty cool right?").size(50),
)
.span(Dir::DOWN)
.add(&mut rsc);
.add(rsc);
let texts = Span::empty(Dir::DOWN).gap(10).add(&mut rsc);
let texts = Span::empty(Dir::DOWN).gap(10).add(rsc);
let msg_area = texts.scrollable().masked().background(rect(Color::SKY));
let add_text = wtext("add")
.editable(EditMode::MultiLine)
.text_align(Align::LEFT)
.size(30)
.attr::<Selectable>(())
.on(Submit, move |ctx| {
.on(Submit, move |ctx, rsc| {
let w = ctx.widget;
let content = w.edit(ctx).take();
let content = w.edit(rsc).take();
let text = wtext(content)
.editable(EditMode::MultiLine)
.size(30)
@@ -120,10 +119,11 @@ impl DefaultAppState for Client {
.attr::<Selectable>(());
let msg_box = text
.background(rect(Color::WHITE.darker(0.5)))
.add_strong(ctx);
texts(ctx).push(msg_box);
.add_strong(rsc);
texts(rsc).push(msg_box);
})
.add(&mut rsc);
.add(rsc);
let text_edit_scroll = (
msg_area.height(rest(1)),
(
@@ -131,8 +131,8 @@ impl DefaultAppState for Client {
(
add_text.width(rest(1)),
Rect::new(Color::GREEN)
.on(CursorSense::click(), move |ctx| {
ctx.state.run_event::<Submit>(add_text, &mut ());
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
rsc.run_event::<Submit>(add_text, (), ctx.state);
})
.sized((40, 40)),
)
@@ -145,39 +145,39 @@ impl DefaultAppState for Client {
.align(Align::BOT),
)
.span(Dir::DOWN)
.add(&mut rsc);
.add(rsc);
let main = WidgetPtr::new().add(&mut rsc);
let main = WidgetPtr::new().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WidgetRef, label| {
let to = to.upgrade(&mut rsc);
let mut switch_button = |color, to: WeakWidget, label| {
let to = to.upgrade(rsc);
let vec = &mut vals.borrow_mut().1;
let i = vec.len();
if vec.is_empty() {
vec.push(None);
rsc.ui[main].set(to);
main(rsc).set(to);
} else {
vec.push(Some(to));
}
let vals = vals.clone();
let rect = rect(color)
.on(CursorSense::click(), move |ctx| {
.on(CursorSense::click(), move |ctx, rsc| {
let (prev, vec) = &mut *vals.borrow_mut();
if let Some(h) = vec[i].take() {
vec[*prev] = main(ctx).replace(h);
vec[*prev] = main(rsc).replace(h);
*prev = i;
}
ctx.widget().color = color.darker(0.3);
ctx.widget(rsc).color = color.darker(0.3);
})
.on(
CursorSense::HoverStart | CursorSense::unclick(),
move |ctx| {
ctx.widget().color = color.brighter(0.2);
move |ctx, rsc| {
ctx.widget(rsc).color = color.brighter(0.2);
},
)
.on(CursorSense::HoverEnd, move |ctx| {
ctx.widget().color = color;
.on(CursorSense::HoverEnd, move |ctx, rsc| {
ctx.widget(rsc).color = color;
});
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
};
@@ -195,30 +195,30 @@ impl DefaultAppState for Client {
)
.span(Dir::RIGHT);
let info = wtext("").add(&mut rsc);
let info = wtext("").add(rsc);
let info_sect = info.pad(10).align(Align::RIGHT);
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
.stack()
.set_root(&mut rsc);
.set_root(rsc, &mut ui_state);
Self {
ui: rsc.ui,
ui_state: rsc.ui_state,
events: rsc.events,
info,
}
Self { ui_state, info }
}
fn window_event(&mut self, _: WindowEvent) {
fn window_event(
&mut self,
_: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
let new = format!(
"widgets: {}\nactive:{}\nviews: {}",
self.ui.num_widgets(),
self.ui.active_widgets(),
self.ui_state.renderer.ui.view_count()
"widgets: {}\nactive: {}\ntextures: {}",
rsc.widgets().len(),
render.active_widgets(),
rsc.ui().textures.count(),
);
if new != *self.ui[self.info].content {
*self.ui[self.info].content = new;
if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use iris::prelude::*;
use std::time::Duration;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> 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;
ctx.update(move |_, rsc| {
let rect = rect(rsc);
if rect.color == Color::RED {
rect.color = Color::BLUE;
} else {
rect.color = Color::RED;
}
});
})
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+53
View File
@@ -0,0 +1,53 @@
use iris::prelude::*;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
type Rsc = DefaultRsc<State>;
#[derive(Clone, Copy, WidgetView)]
struct Test {
#[root]
root: WeakWidget<Rect>,
cur: WeakState<bool>,
}
impl Test {
pub fn new(rsc: &mut Rsc) -> Self {
let root = rect(Color::RED).add(rsc);
let cur = rsc.create_state(root, false);
Self { root, cur }
}
pub fn toggle(&self, rsc: &mut Rsc) {
let cur = &mut rsc[self.cur];
*cur = !*cur;
if *cur {
rsc[self.root].color = Color::BLUE;
} else {
rsc[self.root].color = Color::RED;
}
}
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
test.toggle(rsc);
})
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+83 -79
View File
@@ -2,8 +2,8 @@ extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Attribute, Block, Error, Fields, FieldsNamed, GenericParam, Generics, Ident, ItemStruct,
ItemTrait, Meta, Signature, Token, Visibility,
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
Token, Type, Visibility,
parse::{Parse, ParseStream, Result},
parse_macro_input, parse_quote,
spanned::Spanned,
@@ -89,104 +89,108 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
quote! {
#trai
impl #generics #name<State, WL, Tag> for WL {
impl #generics #name<Rsc, WL, Tag> for WL {
#(#impls)*
}
}
.into()
}
#[proc_macro_derive(UiState, attributes(rsc))]
pub fn derive_ui_state(input: TokenStream) -> TokenStream {
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))]
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input);
let sname = state.ident;
let rscname = Ident::new(&(sname.to_string() + "Rsc"), sname.span());
let mut rsc_fields = Vec::new();
for field in state.fields {
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("rsc")) else {
let mut found_attr = false;
let mut state_field = None;
for field in &state.fields {
if !found_attr
&& let Type::Path(path) = &field.ty
&& path.path.is_ident("DefaultUiState")
{
state_field = Some(field);
}
let Some(attr) = field
.attrs
.iter()
.find(|a| a.path().is_ident("default_ui_state"))
else {
continue;
};
let Meta::List(list) = &attr.meta else {
output.extend(Error::new(attr.span(), "invalid attr syntax").into_compile_error());
if found_attr {
output.extend(
Error::new(
attr.span(),
"cannot have more than one default_ui_state attribute",
)
.into_compile_error(),
);
continue;
};
let tname: Ident = match list.parse_args::<Ident>() {
Ok(ident) => ident,
Err(err) => {
output.extend(err.to_compile_error());
continue;
}
};
let fty = &field.ty;
let fname = &field.ident.unwrap();
rsc_fields.extend(quote! {#fname: #fty,});
output.extend(quote! {
impl #tname for #sname {
fn get(&self) -> &#fty {
&self.#fname
}
fn get_mut(&mut self) -> &mut #fty {
&mut self.#fname
}
}
impl #tname for #rscname {
fn get(&self) -> &#fty {
&self.#fname
}
fn get_mut(&mut self) -> &mut #fty {
&mut self.#fname
}
}
});
}
found_attr = true;
state_field = Some(field);
}
let vis = state.vis;
let Some(field) = state_field else {
output.extend(
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(),
);
return output.into();
};
let sname = &state.ident;
let fname = field.ident.as_ref().unwrap();
output.extend(quote! {
#vis struct #rscname {
#(#rsc_fields)*
}
impl HasState for #sname {
type State = #sname;
}
impl HasState for #rscname {
type State = #sname;
}
impl StateLike<#sname> for #sname {
fn as_state(&mut self) -> &mut Self {
self
impl iris::default::HasDefaultUiState for #sname {
fn default_state(&self) -> &iris::default::DefaultUiState {
&self.#fname
}
}
impl StateLike<#rscname> for #rscname {
fn as_state(&mut self) -> &mut Self {
self
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState {
&mut self.#fname
}
}
});
output.into()
}
#[proc_macro_attribute]
pub fn default_ui_state(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut state: ItemStruct = parse_macro_input!(input);
let Fields::Named(fields) = &mut state.fields else {
panic!("must be on named fields struct");
#[proc_macro_derive(WidgetView, attributes(root))]
pub fn derive_widget_view(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input);
let mut found_attr = false;
let mut state_field = None;
for field in &state.fields {
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("root")) else {
continue;
};
if found_attr {
output.extend(
Error::new(attr.span(), "cannot have more than one root widget")
.into_compile_error(),
);
continue;
}
found_attr = true;
state_field = Some(field);
}
let Some(field) = state_field else {
output.extend(
Error::new(state.ident.span(), "no root widget field found (#[root])")
.into_compile_error(),
);
return output.into();
};
let name = &state.ident;
state.attrs.push(parse_quote! {#[derive(UiState)]});
let new: FieldsNamed = parse_quote! {{
#[rsc(HasUi)]
pub ui: Ui,
#[rsc(HasDefaultUiState)]
pub ui_state: DefaultUiState,
#[rsc(HasEvents)]
pub events: iris::prelude::EventManager<#name>,
}};
fields.named.extend(new.named);
quote! {#state}.into()
let sname = &state.ident;
let fname = field.ident.as_ref().unwrap();
let fty = &field.ty;
output.extend(quote! {
impl iris::core::WidgetView for #sname {
type Widget = <#fty as iris::core::HasWidget>::Widget;
fn root(&self) -> #fty {
self.#fname
}
}
});
output.into()
}
+2 -2
View File
@@ -4,12 +4,12 @@ My experimental attempt at a rust ui library (also my first ui library).
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
There's a `main.rs` that runs a testing window, so you can just `cargo run` to see it working.
Examples are in `examples`, eg. `cargo run --example tabs`.
Goals, in general order:
1. does what I want it to (text, images, video, animations)
2. very easy to use ignoring ergonomic ref counting
3. reasonably fast / efficient (a lot faster than electron, save battery life)
3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem)
## dev details
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["clippy", "rustfmt"]
+7
View File
@@ -11,6 +11,13 @@ pub trait AppState {
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
fn exit(&mut self);
fn run()
where
Self: Sized,
{
App::<Self>::run();
}
}
pub struct App<State: AppState> {
+28 -16
View File
@@ -4,35 +4,45 @@ use winit::dpi::{LogicalPosition, LogicalSize};
pub struct Selector;
impl<State: HasEvents, W: Widget + 'static> WidgetAttr<State, W> for Selector
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector
where
State::State: HasDefaultUiState,
Rsc::State: HasDefaultUiState,
{
type Input = WidgetRef<TextEdit>;
type Input = WeakWidget<TextEdit>;
fn run(state: &mut State, container: WidgetRef<W>, id: Self::Input) {
state.register_event(container, CursorSense::click_or_drag(), move |ctx| {
let region = ctx.ui().window_region(&id).unwrap();
fn run(rsc: &mut Rsc, container: WeakWidget<W>, id: Self::Input) {
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| {
let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left;
let container_pos = ctx.state.ui().window_region(&container).unwrap().top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
let pos = ctx.data.pos + container_pos - id_pos;
let size = region.size();
select(ctx.state, id, pos, size, ctx.data.sense.is_dragging());
select(
rsc,
ctx.data.render,
ctx.state,
id,
pos,
size,
ctx.data.sense.is_dragging(),
);
});
}
}
pub struct Selectable;
impl<State: HasEvents> WidgetAttr<State, TextEdit> for Selectable
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
where
State::State: HasDefaultUiState,
Rsc::State: HasDefaultUiState,
{
type Input = ();
fn run(state: &mut State, id: WidgetRef<TextEdit>, _: Self::Input) {
state.register_event(id, CursorSense::click_or_drag(), move |ctx| {
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) {
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| {
select(
rsc,
ctx.data.render,
ctx.state,
id,
ctx.data.pos,
@@ -44,18 +54,20 @@ where
}
fn select(
rsc: &mut impl UiRsc,
render: &UiRenderState,
state: &mut impl HasDefaultUiState,
id: WidgetRef<TextEdit>,
id: WeakWidget<TextEdit>,
pos: Vec2,
size: Vec2,
dragging: bool,
) {
let (ui, state) = HasDefaultUiState::ui_with_state(state);
let state = state.default_state_mut();
let now = Instant::now();
let recent = (now - state.last_click) < Duration::from_millis(300);
state.last_click = now;
id.edit(ui).select(pos, size, dragging, recent);
if let Some(region) = ui.window_region(&id) {
id.edit(rsc).select(pos, size, dragging, recent);
if let Some(region) = render.window_region(&id) {
state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()),
+210 -46
View File
@@ -1,7 +1,6 @@
use crate::prelude::*;
use arboard::Clipboard;
use iris_core::util::forget_mut;
use std::{marker::Sized, sync::Arc, time::Instant};
use std::{marker::PhantomData, sync::Arc, time::Instant};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -14,6 +13,8 @@ mod event;
mod input;
mod render;
mod sense;
mod state;
mod task;
pub use app::*;
pub use attr::*;
@@ -21,23 +22,33 @@ pub use event::*;
pub use input::*;
pub use render::*;
pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
pub renderer: UiRenderer,
pub input: Input,
pub focus: Option<WidgetRef<TextEdit>>,
pub focus: Option<WeakWidget<TextEdit>>,
pub clipboard: Clipboard,
pub window: Arc<Window>,
pub ime: usize,
pub last_click: Instant,
}
impl HasRoot for DefaultUiState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
impl DefaultUiState {
pub fn new(window: impl Into<Arc<Window>>) -> Self {
let window = window.into();
Self {
root: None,
renderer: UiRenderer::new(window.clone()),
window,
input: Input::default(),
@@ -49,53 +60,162 @@ impl DefaultUiState {
}
}
pub trait HasDefaultUiState: Sized + 'static + HasUi {
fn get(&self) -> &DefaultUiState;
fn get_mut(&mut self) -> &mut DefaultUiState;
fn ui_state(&self) -> &DefaultUiState {
HasDefaultUiState::get(self)
}
fn ui_state_mut(&mut self) -> &mut DefaultUiState {
HasDefaultUiState::get_mut(self)
}
fn ui_with_state(&mut self) -> (&mut Ui, &mut DefaultUiState) {
// as long as you're not doing anything actually unhinged this should always work safely
(
unsafe { forget_mut(self.ui_mut()) },
HasDefaultUiState::get_mut(self),
)
}
pub trait HasDefaultUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState;
}
pub trait DefaultAppState: RunEvents + HasDefaultUiState {
type Event: 'static = ();
fn new(ui_state: DefaultUiState, proxy: Proxy<Self::Event>) -> Self;
pub trait DefaultAppState: HasDefaultUiState {
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
#[allow(unused_variables)]
fn event(&mut self, event: Self::Event) {}
fn event(
&mut self,
event: Self::Event,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
#[allow(unused_variables)]
fn exit(&mut self) {}
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {}
#[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent) {}
fn window_event(
&mut self,
event: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
fn window_attributes() -> WindowAttributes {
Default::default()
}
}
impl<State: DefaultAppState> AppState for State {
pub struct DefaultRsc<State: 'static> {
pub ui: UiData,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> DefaultRsc<State> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(window);
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: Default::default(),
},
recv,
)
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
}
impl<State> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl<State: 'static> HasState for DefaultRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State: 'static> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
render: UiRenderState,
state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
}
impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop.create_window(Self::window_attributes()).unwrap();
Self::new(DefaultUiState::new(window), proxy)
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 render = UiRenderState::new();
Self {
rsc,
state,
render,
task_recv,
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.event(event);
self.state.event(event, &mut self.rsc, &mut self.render);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let events = unsafe { forget_mut(self.events_mut()) };
let ui_state = HasDefaultUiState::get_mut(self);
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
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;
@@ -104,43 +224,43 @@ impl<State: DefaultAppState> AppState for State {
}
if input_changed {
let window_size = ui_state.window_size();
self.run_sensors(&cursor_state, window_size);
render.run_sensors(rsc, state, cursor_state, window_size);
}
let (mut ui, mut ui_state) = self.ui_with_state();
let ui_state = state.default_state_mut();
if old != ui_state.focus
&& let Some(old) = old
{
old.edit(ui).deselect();
old.edit(rsc).deselect();
}
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
ui.update(events);
ui_state.renderer.update(ui);
render.update(&ui_state.root, rsc);
ui_state.renderer.update(&mut rsc.ui, render);
ui_state.renderer.draw();
}
WindowEvent::Resized(size) => {
ui.resize((size.width, size.height));
render.resize((size.width, size.height));
ui_state.renderer.resize(size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = ui_state.focus
&& event.state.is_pressed()
{
let mut text = sel.edit(ui);
let mut text = sel.edit(rsc);
match text.apply_event(event, &ui_state.input.modifiers) {
TextInputResult::Unfocus => {
ui_state.focus = None;
ui_state.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
self.run_event::<Submit>(sel, &mut ());
rsc.run_event::<Submit>(sel, (), state);
}
TextInputResult::Paste => {
if let Ok(t) = ui_state.clipboard.get_text() {
text.insert(&t);
}
self.run_event::<Edited>(sel, &mut ());
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Copy(text) => {
if let Err(err) = ui_state.clipboard.set_text(text) {
@@ -148,7 +268,7 @@ impl<State: DefaultAppState> AppState for State {
}
}
TextInputResult::Used => {
self.run_event::<Edited>(sel, &mut ());
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Unused => {}
}
@@ -156,7 +276,7 @@ impl<State: DefaultAppState> AppState for State {
}
WindowEvent::Ime(ime) => {
if let Some(sel) = ui_state.focus {
let mut text = sel.edit(ui);
let mut text = sel.edit(rsc);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
@@ -172,15 +292,59 @@ impl<State: DefaultAppState> AppState for State {
}
_ => (),
}
self.window_event(event);
(ui, ui_state) = self.ui_with_state();
if ui.needs_redraw() {
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();
}
fn exit(&mut self) {
self.exit();
self.state.exit(&mut self.rsc, &mut self.render);
}
}
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> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, 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)
}
}
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
type Output = W;
fn get(self, rsc: &Rsc) -> &Self::Output {
&rsc.ui().widgets[self]
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
&mut rsc.ui_mut().widgets[self]
}
}
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
type Output = T;
fn get(self, rsc: &Rsc) -> &Self::Output {
rsc.widget_state().get(self)
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
rsc.widget_state_mut().get_mut(self)
}
}
+29 -26
View File
@@ -1,7 +1,7 @@
use iris_core::{Ui, UiLimits, UiRenderNode};
use iris_core::{UiData, UiRenderNode, UiRenderState};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::{util::StagingBelt, *};
use wgpu::*;
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK;
@@ -13,17 +13,29 @@ pub struct UiRenderer {
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderNode,
}
impl UiRenderer {
pub fn update(&mut self, ui: &mut Ui) {
self.ui.update(&self.device, &self.queue, ui);
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) {
self.ui.update(&self.device, &self.queue, ui, render);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
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,
};
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
@@ -46,16 +58,15 @@ impl UiRenderer {
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
output.present();
self.staging_belt.recall();
self.window.pre_present_notify();
self.queue.present(output);
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui.resize(size, &self.queue);
self.ui.resize((size.width, size.height), &self.queue);
}
fn create_encoder(device: &Device) -> CommandEncoder {
@@ -67,9 +78,10 @@ impl UiRenderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
let instance = Instance::new(InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
display: Some(Box::new(window.clone())),
..InstanceDescriptor::new_without_display_handle()
});
let surface = instance
@@ -81,22 +93,14 @@ impl UiRenderer {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.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()
},
@@ -116,9 +120,10 @@ 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::AutoNoVsync,
present_mode: PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
@@ -126,10 +131,9 @@ impl UiRenderer {
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UiRenderNode::new(&device, &queue, &config, ui_limits);
let ui = UiRenderNode::new(&device, &config);
Self {
surface,
@@ -137,8 +141,7 @@ impl UiRenderer {
queue,
config,
encoder,
staging_belt,
ui: shape_pipeline,
ui,
window,
}
}
+38 -18
View File
@@ -28,12 +28,13 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses {
type Data<'a> = CursorData<'a>;
type State = SensorState;
fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
if let Some(sense) = should_run(self, data.cursor, data.hover) {
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone();
data.sense = sense;
true
Some(data)
} else {
false
None
}
}
}
@@ -115,7 +116,7 @@ pub enum ActivationState {
/// or basically have some way to have custom senses
/// that depend on active widget positions
/// but I'm not sure how or if worth it
pub struct Sensor<Ctx, Data> {
pub struct Sensor<Ctx: HasEvents, Data> {
pub senses: CursorSenses,
pub f: Rc<dyn EventFn<Ctx, Data>>,
}
@@ -127,29 +128,46 @@ pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)]
pub struct CursorData<'a> {
/// where this widget was hit
pub pos: Vec2,
pub size: Vec2,
pub scroll_delta: Vec2,
pub hover: ActivationState,
pub cursor: &'a CursorState,
pub cursor: CursorState,
/// the first sense that triggered this
pub sense: CursorSense,
pub render: &'a UiRenderState,
}
pub trait SensorUi<State> {
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2);
pub trait SensorUi {
fn run_sensors<Rsc: HasEvents>(
&self,
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
);
}
impl<State: RunEvents> SensorUi<State> for State {
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) {
let layers = std::mem::take(&mut self.ui_mut().layers);
let mut active = std::mem::take(&mut self.events_mut().get_type::<CursorSense>().active);
for layer in layers.indices().rev() {
impl SensorUi for UiRenderState {
fn run_sensors<Rsc: HasEvents>(
&self,
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
) {
// in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there?
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
for layer in self.layers.indices().rev() {
let mut sensed = false;
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let shape = self.ui().active.get(id).unwrap().region;
let shape = self.active.get(id).unwrap().region;
let region = shape.to_px(window_size);
let in_shape = cursor.exists && region.contains(cursor.pos);
sensor.hover.update(in_shape);
@@ -158,7 +176,9 @@ impl<State: RunEvents> SensorUi<State> for State {
}
sensed = true;
let mut data = CursorData {
let cursor = cursor.clone();
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
@@ -167,15 +187,15 @@ impl<State: RunEvents> SensorUi<State> for State {
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
};
self.run_event::<CursorSense>(*id, &mut data);
rsc.run_event::<CursorSense>(*id, data, state);
}
if sensed {
break;
}
}
self.events_mut().get_type::<CursorSense>().active = active;
self.ui_mut().layers = layers;
rsc.events_mut().get_type::<CursorSense>().active = active;
}
}
+75
View File
@@ -0,0 +1,75 @@
use iris_core::{
WidgetId,
util::{HashMap, HashSet},
};
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Key {
id: WidgetId,
ty: TypeId,
i: usize,
}
#[derive(Default)]
pub struct WidgetState {
widgets: HashMap<WidgetId, HashSet<(TypeId, usize)>>,
counts: HashMap<(WidgetId, TypeId), usize>,
map: HashMap<Key, Box<dyn Any>>,
}
impl WidgetState {
pub fn new() -> Self {
Self::default()
}
pub fn add<T: 'static>(&mut self, id: WidgetId, data: T) -> WeakState<T> {
let ty = TypeId::of::<T>();
let count = self.counts.entry((id, ty)).or_default();
let i = *count;
let key = Key { ty, i, id };
self.map.insert(key, Box::new(data));
self.widgets.entry(id).or_default().insert((ty, i));
*count += 1;
WeakState {
key,
_pd: PhantomData,
}
}
pub fn remove(&mut self, id: WidgetId) {
for &(ty, i) in self.widgets.get(&id).into_iter().flatten() {
self.map.remove(&Key { id, ty, i });
}
}
pub fn get<T: 'static>(&self, state: WeakState<T>) -> &T {
self.map.get(&state.key).unwrap().downcast_ref().unwrap()
}
pub fn get_mut<T: 'static>(&mut self, state: WeakState<T>) -> &mut T {
self.map
.get_mut(&state.key)
.unwrap()
.downcast_mut()
.unwrap()
}
}
#[derive(Clone, Copy)]
pub struct WeakState<T> {
key: Key,
_pd: PhantomData<T>,
}
pub trait HasWidgetState {
fn widget_state(&self) -> &WidgetState;
fn widget_state_mut(&mut self) -> &mut WidgetState;
}
impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState<T> {
type Output = &'a mut T;
extern "rust-call" fn call_once(self, (state,): (&'a mut WidgetState,)) -> Self::Output {
state.get_mut(self)
}
}
+82
View File
@@ -0,0 +1,82 @@
use iris_core::HasState;
use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{
runtime::Runtime,
sync::mpsc::{
UnboundedReceiver as AsyncReceiver, UnboundedSender as AsyncSender,
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>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>,
window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
}
pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>,
}
impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv))
});
(
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
}
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
let send = self.msg_send.clone();
let window = self.window.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await;
window.request_redraw();
}));
}
}
async fn listen(mut recv: AsyncReceiver<BoxTask>) {
while let Some(task) = recv.recv().await {
tokio::spawn(task);
}
}
+80 -18
View File
@@ -1,25 +1,87 @@
use crate::prelude::*;
use iris_core::*;
use iris_macro::*;
use std::sync::Arc;
pub mod eventable {
use super::*;
widget_trait! {
pub trait Eventable<State: HasEvents + StateLike<State> + 'static>;
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<State::State, <E::Event as Event>::Data<'a>, WL::Widget>,
) -> impl WidgetIdFn<State, WL::Widget> {
move |state| {
let id = self.add(state);
state.register_event(id, event.into_event(), move |ctx| {
f(&mut EventIdCtx {
use crate::default::{TaskCtx, TaskUpdate, Tasks};
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, Self::Widget>,
) -> impl WidgetIdFn<Rsc, Self::Widget> {
move |rsc| {
let id = self.add(rsc);
rsc.register_event(id, event.into_event(), move |ctx, rsc| {
f(
EventIdCtx {
widget: id,
state: ctx.state,
data: ctx.data,
});
});
id
}
},
rsc,
);
});
id
}
}
}
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
widget_trait! {
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
fn task_on<'a, E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
self,
event: E,
f: F,
) -> impl WidgetIdFn<Rsc, WL::Widget>
where <E::Event as Event>::Data<'a>: Send,
for<'b> F::CallRefFuture<'b>: Send,
{
let f = Arc::new(f);
move |rsc| {
let id = self.add(rsc);
rsc.register_event(id, event.into_event(), move |_, rsc| {
let f = f.clone();
rsc.tasks_mut().spawn(async move |task| {
f(AsyncEventIdCtx {
widget: id,
task,
}).await;
});
});
id
}
}
}
pub trait HasTasks: Sized + HasState + HasEvents {
fn tasks_mut(&mut self) -> &mut Tasks<Self>;
fn spawn_task<F: AsyncFnOnce(TaskCtx<Self>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
self.tasks_mut().spawn(task);
}
}
pub trait AsyncWidgetEventFn<Rsc: HasEvents, W: ?Sized>:
AsyncFn(AsyncEventIdCtx<Rsc, W>) + Send + Sync + 'static
{
}
impl<Rsc: HasEvents, F: AsyncFn(AsyncEventIdCtx<Rsc, W>) + Send + Sync + 'static, W: ?Sized>
AsyncWidgetEventFn<Rsc, W> for F
{
}
pub struct AsyncEventIdCtx<Rsc: HasEvents, W: ?Sized> {
pub widget: WeakWidget<W>,
task: TaskCtx<Rsc>,
}
impl<Rsc: HasEvents, W: ?Sized> AsyncEventIdCtx<Rsc, W> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
self.task.update(f);
}
}
+2 -16
View File
@@ -1,31 +1,17 @@
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)]
#![feature(unsize)]
#![feature(option_into_flat_iter)]
#![feature(async_fn_traits)]
pub mod default;
pub mod event;
pub mod typed;
pub mod widget;
pub use iris_core as core;
pub use iris_macro as macros;
#[macro_export]
macro_rules! state_prelude {
($vis:vis $state:ty) => {
iris::event_state!($vis $state);
$vis use iris::{
default::*,
core::{len_fns::*, util::Vec2, *},
macros::*,
widget::*,
};
};
}
pub mod prelude {
use super::*;
pub use default::*;
@@ -35,5 +21,5 @@ pub mod prelude {
pub use widget::*;
pub use iris_core::util::Vec2;
pub use typed::*;
pub use len_fns::*;
}
-37
View File
@@ -1,37 +0,0 @@
#[macro_export]
macro_rules! event_state {
($vis:vis $state:ty) => {
mod local_event_trait {
use super::*;
#[allow(unused_imports)]
use $crate::prelude::*;
pub trait EventableCtx<WL: WidgetLike<$state, Tag>, Tag> {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<
<$state as HasState>::State,
<E::Event as Event>::Data<'a>,
WL::Widget
>,
) -> impl WidgetIdFn<$state, WL::Widget>;
}
impl<WL: WidgetLike<$state, Tag>, Tag> EventableCtx<WL, Tag> for WL {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<
<$state as HasState>::State,
<E::Event as Event>::Data<'a>,
WL::Widget
>,
) -> impl WidgetIdFn<$state, WL::Widget> {
eventable::Eventable::on(self, event, f)
}
}
}
$vis type EventManager = $crate::prelude::EventManager<<$state as HasState>::State>;
$vis use local_event_trait::*;
};
}
pub use event_state;
+3 -3
View File
@@ -7,7 +7,7 @@ pub struct Image {
impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) {
painter.texture(&self.handle);
painter.primitive(&self.handle);
}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
@@ -19,10 +19,10 @@ impl Widget for Image {
}
}
pub fn image<State: HasUi>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
let image = image.get_image().expect("Failed to load image");
move |state| Image {
handle: state.get_mut().add_texture(image),
handle: state.ui_mut().textures.add(image),
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Masked {
pub inner: WidgetHandle,
pub inner: StrongWidget,
}
impl Widget for Masked {
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: WidgetHandle,
pub inner: StrongWidget,
pub align: Align,
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct LayerOffset {
pub inner: WidgetHandle,
pub inner: StrongWidget,
pub offset: usize,
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct MaxSize {
pub inner: WidgetHandle,
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
+2 -2
View File
@@ -4,7 +4,7 @@ mod max_size;
mod offset;
mod pad;
mod scroll;
mod sized;
mod set_size;
mod span;
mod stack;
@@ -14,6 +14,6 @@ pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use sized::*;
pub use set_size::*;
pub use span::*;
pub use stack::*;
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Offset {
pub inner: WidgetHandle,
pub inner: StrongWidget,
pub amt: UiVec2,
}
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::prelude::*;
pub struct Pad {
pub padding: Padding,
pub inner: WidgetHandle,
pub inner: StrongWidget,
}
impl Widget for Pad {
+2 -2
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Scroll {
inner: WidgetHandle,
inner: StrongWidget,
axis: Axis,
amt: f32,
snap_end: bool,
@@ -41,7 +41,7 @@ impl Widget for Scroll {
}
impl Scroll {
pub fn new(inner: WidgetHandle, axis: Axis) -> Self {
pub fn new(inner: StrongWidget, axis: Axis) -> Self {
Self {
inner,
axis,
@@ -1,12 +1,12 @@
use crate::prelude::*;
pub struct Sized {
pub inner: WidgetHandle,
pub struct SetSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Sized {
impl SetSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
@@ -17,7 +17,7 @@ impl Sized {
}
}
impl Widget for Sized {
impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
+8 -8
View File
@@ -2,7 +2,7 @@ use crate::prelude::*;
use std::marker::PhantomData;
pub struct Span {
pub children: Vec<WidgetHandle>,
pub children: Vec<StrongWidget>,
pub dir: Dir,
pub gap: f32,
}
@@ -62,11 +62,11 @@ impl Span {
self
}
pub fn push(&mut self, w: WidgetHandle) {
pub fn push(&mut self, w: StrongWidget) {
self.children.push(w);
}
pub fn pop(&mut self) -> Option<WidgetHandle> {
pub fn pop(&mut self) -> Option<StrongWidget> {
self.children.pop()
}
@@ -159,15 +159,15 @@ pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Ta
_pd: PhantomData<(State, Tag)>,
}
impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
WidgetFnTrait<State> for SpanBuilder<State, LEN, Wa, Tag>
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
for SpanBuilder<Rsc, LEN, Wa, Tag>
{
type Widget = Span;
#[track_caller]
fn run(self, state: &mut State) -> Self::Widget {
fn run(self, rsc: &mut Rsc) -> Self::Widget {
Span {
children: self.children.add(state).arr.into_iter().collect(),
children: self.children.add(rsc).arr.into_iter().collect(),
dir: self.dir,
gap: self.gap,
}
@@ -193,7 +193,7 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
}
impl std::ops::Deref for Span {
type Target = Vec<WidgetHandle>;
type Target = Vec<StrongWidget>;
fn deref(&self) -> &Self::Target {
&self.children
+7 -6
View File
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
use crate::prelude::*;
pub struct Stack {
pub children: Vec<WidgetHandle>,
pub children: Vec<StrongWidget>,
pub size: StackSize,
}
@@ -48,14 +48,15 @@ pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, T
_pd: PhantomData<(State, Tag)>,
}
impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> FnOnce<(&mut State,)>
for StackBuilder<State, LEN, Wa, Tag>
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
for StackBuilder<Rsc, LEN, Wa, Tag>
{
type Output = Stack;
type Widget = Stack;
extern "rust-call" fn call_once(self, args: (&mut State,)) -> Self::Output {
#[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget {
Stack {
children: self.children.add(args.0).arr.into_iter().collect(),
children: self.children.add(rsc).arr.into_iter().collect(),
size: self.size,
}
}
+5 -5
View File
@@ -1,8 +1,8 @@
use crate::prelude::*;
use std::marker::{Sized, Unsize};
use std::marker::Unsize;
pub struct WidgetPtr {
pub inner: Option<WidgetHandle>,
pub inner: Option<StrongWidget>,
}
impl Widget for WidgetPtr {
@@ -38,14 +38,14 @@ impl WidgetPtr {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: WidgetHandle<W>) {
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to)
}
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
&mut self,
to: WidgetHandle<W>,
) -> Option<WidgetHandle> {
to: StrongWidget<W>,
) -> Option<StrongWidget> {
self.inner.replace(to)
}
}
+15 -30
View File
@@ -1,6 +1,5 @@
use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics};
use std::marker::{PhantomData, Sized};
use std::marker::PhantomData;
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
pub content: String,
@@ -20,7 +19,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
self.attrs.color = color;
self
}
pub fn family(mut self, family: Family<'static>) -> Self {
pub fn family(mut self, family: Family) -> Self {
self.attrs.family = family;
self
}
@@ -51,15 +50,15 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
}
}
impl<State: HasUi + StateLike<State>, O> TextBuilder<State, O> {
pub fn hint<W: WidgetLike<State, Tag>, Tag>(
impl<Rsc: UiRsc, O> TextBuilder<Rsc, O> {
pub fn hint<W: WidgetLike<Rsc, Tag>, Tag>(
self,
hint: W,
) -> TextBuilder<State, O, impl WidgetOption<State>> {
) -> TextBuilder<Rsc, O, impl WidgetOption<Rsc>> {
TextBuilder {
content: self.content,
attrs: self.attrs,
hint: move |ui: &mut State| Some(hint.add_strong(ui).any()),
hint: move |rsc: &mut Rsc| Some(hint.add_strong(rsc).any()),
output: self.output,
state: PhantomData,
}
@@ -75,26 +74,20 @@ pub trait TextBuilderOutput<State>: Sized {
}
pub struct TextOutput;
impl<State: HasUi> TextBuilderOutput<State> for TextOutput {
impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
type Output = Text;
fn run<H: WidgetOption<State>>(
state: &mut State,
builder: TextBuilder<State, Self, H>,
fn run<H: WidgetOption<Rsc>>(
state: &mut Rsc,
builder: TextBuilder<Rsc, Self, H>,
) -> Self::Output {
let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let buf = TextBuffer::new(&builder.content);
let hint = builder.hint.get(state);
let font_system = &mut state.get_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,26 +96,18 @@ pub struct TextEditOutput {
mode: EditMode,
}
impl<State: HasUi> TextBuilderOutput<State> for TextEditOutput {
impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
type Output = TextEdit;
fn run<H: WidgetOption<State>>(
state: &mut State,
builder: TextBuilder<State, Self, H>,
) -> Self::Output {
let buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
let buf = TextBuffer::new(&builder.content);
TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)),
builder.output.mode,
);
let font_system = &mut state.get_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
)
}
}
+244 -395
View File
@@ -1,17 +1,30 @@
use crate::prelude::*;
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use iris_core::{TextData, UiColor};
use parley::{Affinity, Layout, Selection};
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,
selection: TextSelection,
history: Vec<(String, TextSelection)>,
double_hit: Option<Cursor>,
/// `None` represents unfocused, which Parley's `Selection` cannot express.
selection: Option<Selection>,
history: Vec<(String, Option<Selection>)>,
double_hit: Option<usize>,
pub mode: EditMode,
}
@@ -25,27 +38,19 @@ impl TextEdit {
pub fn new(view: TextView, mode: EditMode) -> Self {
Self {
view,
selection: Default::default(),
selection: None,
history: Default::default(),
double_hit: None,
mode,
}
}
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
pub fn selected_text(&self) -> Option<String> {
let sel = self.selection?;
if sel.is_collapsed() {
return None;
}
Some(self.buf.text()[sel.text_range()].to_string())
}
}
@@ -57,39 +62,29 @@ impl Widget for TextEdit {
painter.layer = base;
let region = self.region();
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(&region),
);
}
}
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(&region),
);
}
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(&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(&region),
);
}
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(&region),
);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
@@ -101,154 +96,58 @@ impl Widget for TextEdit {
}
}
/// 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
}
const CARET_WIDTH: f32 = 1.0;
pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit,
pub font_system: &'a mut FontSystem,
pub data: &'a mut TextData,
}
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 = 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();
let text = std::mem::take(self.text.view.buf.edit());
self.text.selection = None;
text
}
pub fn set(&mut self, text: &str) {
let text = self.string(text);
self.text
.buf
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
self.text.view.buf.set_text(text);
self.text.view.buf.changed = true;
self.text.selection = None;
}
pub fn motion(&mut self, motion: Motion, select: bool) {
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);
}
}
}
}
}
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);
}
/// 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.delete(false);
self.backspace(false);
}
self.insert_inner(&text, false);
self.insert_str(&text);
}
fn string(&self, text: &str) -> String {
@@ -261,202 +160,173 @@ impl<'a> TextEditCtx<'a> {
pub fn insert(&mut self, text: &str) {
let text = self.string(text);
let mut lines = text.split('\n');
let Some(first) = lines.next() else {
self.insert_str(&text);
}
fn insert_str(&mut self, text: &str) {
if text.is_empty() {
return;
};
self.insert_inner(first, true);
for line in lines {
self.newline();
self.insert_inner(line, true);
}
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());
}
pub fn clear_span(&mut self) -> bool {
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 Some(sel) = self.text.selection else {
return false;
};
if sel.is_collapsed() {
return 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
}
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);
}
}
}
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 newline(&mut self) {
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;
if self.text.mode == EditMode::MultiLine {
self.insert_str("\n");
}
}
pub fn backspace(&mut self, word: bool) {
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);
if self.clear_span() {
return;
}
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()
&& 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);
}
}
if self.clear_span() {
return;
}
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 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)
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);
}
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),
pub fn select_all(&mut self) {
let len = self.text.view.buf.text().len();
if len == 0 {
return;
}
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 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)
}
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)
}
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);
}
};
self.text.selection = Some(selection);
self.text.double_hit = double_hit;
}
pub fn deselect(&mut self) {
self.text.selection = TextSelection::None;
self.text.selection = None;
self.text.double_hit = None;
}
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.content(), self.text.selection);
let old = (self.text.view.buf.text().to_string(), self.text.selection);
let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo && let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
} else if self.text.content() != old.0 {
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 {
self.text.history.push(old);
}
res
@@ -481,21 +351,25 @@ impl<'a> TextEditCtx<'a> {
}
}
NamedKey::ArrowRight => {
if modifiers.control {
self.motion(Motion::RightWord, modifiers.shift)
let motion = if modifiers.control {
Motion::RightWord
} else {
self.motion(Motion::Right, modifiers.shift)
}
Motion::Right
};
self.motion(motion, modifiers.shift);
}
NamedKey::ArrowLeft => {
if modifiers.control {
self.motion(Motion::LeftWord, modifiers.shift)
let motion = if modifiers.control {
Motion::LeftWord
} else {
self.motion(Motion::Left, modifiers.shift)
}
Motion::Left
};
self.motion(motion, 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;
@@ -507,34 +381,18 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() {
"v" => return TextInputResult::Paste,
"c" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
if let Some(content) = self.text.selected_text() {
return TextInputResult::Copy(content);
}
}
"x" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
if let Some(content) = self.text.selected_text() {
self.clear_span();
return TextInputResult::Copy(content);
}
}
"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;
}
"a" => self.select_all(),
"z" => *undo = true,
_ => self.insert(text),
}
} else {
@@ -547,6 +405,24 @@ 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,
@@ -569,33 +445,6 @@ 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)
@@ -617,15 +466,15 @@ impl DerefMut for TextEdit {
}
pub trait TextEditable {
fn edit<'a>(&self, ui: &'a mut impl HasUi) -> TextEditCtx<'a>;
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a>;
}
impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
fn edit<'a>(&self, ui: &'a mut impl HasUi) -> TextEditCtx<'a> {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> {
let ui = ui.ui_mut();
TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(),
font_system: &mut ui.text.font_system,
data: &mut ui.text,
}
}
}
+41 -73
View File
@@ -6,11 +6,8 @@ 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,11 +19,21 @@ pub struct TextView {
// cache
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<WidgetHandle>,
pub hint: Option<StrongWidget>,
}
impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<WidgetHandle>) -> Self {
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 {
attrs: attrs.into(),
buf: buf.into(),
@@ -45,45 +52,26 @@ impl TextView {
.align(self.align)
}
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 {
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
&& let Some(tex) = &self.tex
&& !self.attrs.changed
&& !self.buf.changed
{
return tex.clone();
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;
}
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
self.tex.as_ref().unwrap()
}
pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref()
}
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.width(hint)
} else {
@@ -91,9 +79,8 @@ impl TextView {
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(hint) = &self.hint
&& let [line] = &self.buf.lines[..]
&& line.text().is_empty()
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
} else {
@@ -101,48 +88,39 @@ impl TextView {
}
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx());
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);
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);
region
}
pub fn content(&self) -> String {
self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
self.buf.text().to_string()
}
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
let content: String = content.into();
Self {
content: content.into().into(),
view: TextView::new(buf, attrs, None),
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None),
content: content.into(),
}
}
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(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
self.view.buf.set_text(self.content.as_str());
}
}
}
@@ -164,16 +142,6 @@ 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;
+28 -29
View File
@@ -3,44 +3,44 @@ use crate::prelude::*;
// these methods should "not require any context" (require unit) because they're in core
widget_trait! {
pub trait CoreWidget<State: HasUi + StateLike<State> + 'static>;
pub trait CoreWidget<Rsc: UiRsc + 'static>;
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<State, Pad> {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<Rsc, Pad> {
|state| Pad {
padding: padding.into(),
inner: self.add_strong(state),
}
}
fn align(self, align: impl Into<Align>) -> impl WidgetFn<State, Aligned> {
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> {
move |state| Aligned {
inner: self.add_strong(state),
align: align.into(),
}
}
fn center(self) -> impl WidgetFn<State, Aligned> {
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
self.align(Align::CENTER)
}
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<State, WL::Widget> {
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<Rsc, WL::Widget> {
|state| {
let id = self.add(state);
state.get_mut().set_label(id, label.into());
state.ui_mut().widgets.set_label(id, label.into());
id
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<State, Sized> {
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
let size = size.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(size.x),
y: Some(size.y),
}
}
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<State, MaxSize> {
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
@@ -49,7 +49,7 @@ widget_trait! {
}
}
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<State, MaxSize> {
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
@@ -58,83 +58,82 @@ widget_trait! {
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<State, Sized> {
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<State, Sized> {
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| Sized {
move |state| SetSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
}
}
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<State, Offset> {
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Rsc, Offset> {
move |state| Offset {
inner: self.add_strong(state),
amt: amt.into(),
}
}
fn scrollable(self) -> impl WidgetIdFn<State, Scroll> where State: HasEvents {
use eventable::*;
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents {
move |state| {
Scroll::new(self.add_strong(state), Axis::Y)
.on(CursorSense::Scroll, |ctx: &mut EventIdCtx<'_, State::State, CursorData<'_>, Scroll>| {
.on(CursorSense::Scroll, |ctx, rsc| {
let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget().scroll(delta);
ctx.widget(rsc).scroll(delta);
})
.add(state)
}
}
fn masked(self) -> impl WidgetFn<State, Masked> {
fn masked(self) -> impl WidgetFn<Rsc, Masked> {
move |state| Masked {
inner: self.add_strong(state),
}
}
fn background<T>(self, w: impl WidgetLike<State, T>) -> impl WidgetFn<State, Stack> {
fn background<T>(self, w: impl WidgetLike<Rsc, T>) -> impl WidgetFn<Rsc, Stack> {
move |state| Stack {
children: vec![w.add_strong(state), self.add_strong(state)],
size: StackSize::Child(1),
}
}
fn foreground<T>(self, w: impl WidgetLike<State, T>) -> impl WidgetFn<State, Stack> {
fn foreground<T>(self, w: impl WidgetLike<Rsc, T>) -> impl WidgetFn<Rsc, Stack> {
move |state| Stack {
children: vec![self.add_strong(state), w.add_strong(state)],
size: StackSize::Child(0),
}
}
fn layer_offset(self, offset: usize) -> impl WidgetFn<State, LayerOffset> {
fn layer_offset(self, offset: usize) -> impl WidgetFn<Rsc, LayerOffset> {
move |state| LayerOffset {
inner: self.add_strong(state),
offset,
}
}
fn to_any(self) -> impl WidgetIdFn<State> {
fn to_any(self) -> impl WidgetIdFn<Rsc> {
|state| self.add(state)
}
fn set_ptr(self, ptr: WidgetRef<WidgetPtr>, state: &mut State) {
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) {
let id = self.add_strong(state);
state.get_mut()[ptr].inner = Some(id);
state.ui_mut().widgets[ptr].inner = Some(id);
}
}
pub trait CoreWidgetArr<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag>;
fn stack(self) -> StackBuilder<State, LEN, Wa, Tag>;
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>;
}
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
+205
View File
@@ -0,0 +1,205 @@
//! 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
);
}
}
+67
View File
@@ -0,0 +1,67 @@
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("", 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");
}