Compare commits
271
Commits
tuple_attempt
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2b4b2173 | ||
|
|
f9423855e1 | ||
|
|
43ce8c7d02 | ||
|
|
c8ac669f95 | ||
|
|
32b10383d8 | ||
|
|
00d2230b84 | ||
|
|
b234497d21 | ||
|
|
0f6a28b4dd | ||
|
|
b90c855cf5 | ||
|
|
3b96324333 | ||
|
|
4767384b08 | ||
|
|
0191f2081b | ||
|
|
472736a292 | ||
|
|
6e271e8aee | ||
|
|
a1ff76776c | ||
|
|
cb9cad38f2 | ||
|
|
db9b0f21d5 | ||
|
|
2b6a6ab378 | ||
|
|
ec2b5d4c1d | ||
|
|
780ac82b27 | ||
|
|
465e43075e | ||
|
|
0c9a39fd06 | ||
|
|
936fbdd8ce | ||
|
|
3eaded125e | ||
|
|
23270e49fb | ||
|
|
bc6cdd13c9 | ||
|
|
072f1e31ad | ||
|
|
42753141b7 | ||
|
|
6884160bfe | ||
|
|
fae21a1991 | ||
|
|
7b54aaf3c4 | ||
|
|
17c436d944 | ||
|
|
a592318a6f | ||
|
|
796bc41752 | ||
|
|
7bafb04a34 | ||
|
|
06dd015092 | ||
|
|
79813db3ba | ||
|
|
a9c76e4326 | ||
|
|
32e45e9238 | ||
|
|
2fadfe4b82 | ||
|
|
d11107f965 | ||
|
|
07de7c8722 | ||
|
|
f2ac6f195f | ||
|
|
59901b6580 | ||
|
|
5da1e9e767 | ||
|
|
462c0e6416 | ||
|
|
54534c4c34 | ||
|
|
f0fe671bd8 | ||
|
|
1ccf947220 | ||
|
|
c00ded78c0 | ||
|
|
32ca4ec5a6 | ||
|
|
fabc7d0b90 | ||
|
|
bae17235c6 | ||
|
|
30bc55c78e | ||
|
|
7e6369029f | ||
|
|
70ac0fbcb2 | ||
|
|
1363f31fcd | ||
|
|
ecbb9e56e2 | ||
|
|
ac9571b29f | ||
|
|
71f3beaf94 | ||
|
|
2183fbd3cb | ||
|
|
486ed0ffd7 | ||
|
|
8d1a810483 | ||
|
|
0b8a93c5ce | ||
|
|
dc2be7f688 | ||
|
|
9d8ca8fa72 | ||
|
|
a2a32b4322 | ||
|
|
37b1987aa8 | ||
|
|
a708813ce7 | ||
|
|
a70d09e162 | ||
|
|
966b6a2ac2 | ||
|
|
2dad409300 | ||
|
|
36668c82f4 | ||
|
|
a85e129026 | ||
|
|
174c447706 | ||
|
|
2dc5b0f62c | ||
|
|
38266debb6 | ||
|
|
62aa02847a | ||
|
|
f6b1143665 | ||
|
|
28d17c49c6 | ||
|
|
23ae5b246e | ||
|
|
db888416b6 | ||
|
|
f7b100e00c | ||
|
|
e5d0a7e592 | ||
|
|
84c460a91f | ||
|
|
d6a9711ceb | ||
|
|
ee0616885f | ||
|
|
14a9da0553 | ||
|
|
8e08f67627 | ||
|
|
84b3bf9078 | ||
|
|
bf3ade840b | ||
|
|
2aa5719166 | ||
|
|
90c579d734 | ||
|
|
d757e805e8 | ||
|
|
9deba3d9d7 | ||
|
|
c24c517c60 | ||
|
|
fc89826794 | ||
|
|
140be50baa | ||
|
|
1c6fc99f57 | ||
|
|
1cec56e847 | ||
|
|
246caffb34 | ||
|
|
31ff17c21a | ||
|
|
23c5abe5a9 | ||
|
|
97b284e81e | ||
|
|
c428de8fd5 | ||
|
|
5785352ac0 | ||
|
|
172e7157be | ||
|
|
e3b1ddc993 | ||
|
|
5aef8c2201 | ||
|
|
acd67179b7 | ||
|
|
dff72d2c43 | ||
|
|
f6f9ebbe51 | ||
|
|
6251c23d37 | ||
|
|
96ef0c529b | ||
|
|
a952b34a72 | ||
|
|
db248de8f4 | ||
|
|
9febd03067 | ||
|
|
38d7ca3090 | ||
|
|
126c442706 | ||
|
|
6b7719539e | ||
|
|
bc829397c8 | ||
|
|
4981bd739a | ||
|
|
7e257fd042 | ||
|
|
c7b255be4f | ||
|
|
955e6b7588 | ||
|
|
f5f4547537 | ||
|
|
3425eb7b80 | ||
|
|
681efe1e2b | ||
|
|
ef448ec870 | ||
|
|
f74c4dc6e2 | ||
|
|
b3d0dc3871 | ||
|
|
b6ece4a5ee | ||
|
|
2914d7968f | ||
|
|
8896c64445 | ||
|
|
bd0805dbac | ||
|
|
ed87b7c336 | ||
|
|
448f348356 | ||
|
|
182b1d4729 | ||
|
|
b4947db850 | ||
|
|
218b3f14ed | ||
|
|
e2690fa611 | ||
|
|
125fca4075 | ||
|
|
73afea8c35 | ||
|
|
8755c04feb | ||
|
|
afabdc52a2 | ||
|
|
deaf730901 | ||
|
|
92db1264a6 | ||
|
|
379eec771a | ||
|
|
ebff93bec9 | ||
|
|
1c49db1b89 | ||
|
|
5c2022396a | ||
|
|
db0d11cacb | ||
|
|
337af3e18c | ||
|
|
628840d5cd | ||
|
|
c98a43f94d | ||
|
|
61df088cc7 | ||
|
|
b2950566af | ||
|
|
dc9340b26c | ||
|
|
8afe2c68e8 | ||
|
|
5445008528 | ||
|
|
95f049acb4 | ||
|
|
5f2dffc189 | ||
|
|
06cfeaac6b | ||
|
|
6d829dbe81 | ||
|
|
273a92d1f7 | ||
|
|
552d66d90f | ||
|
|
fe42092556 | ||
|
|
cfd5cda0b2 | ||
|
|
21f15fb9c5 | ||
|
|
4deeabe611 | ||
|
|
51f9908103 | ||
|
|
6e5cce2617 | ||
|
|
055aaf757c | ||
|
|
b14aafca30 | ||
|
|
8829878f2e | ||
|
|
57bfd2d348 | ||
|
|
443e13f094 | ||
|
|
3463682d62 | ||
|
|
719bee4b31 | ||
|
|
26c248dcba | ||
|
|
2adf7a43a1 | ||
|
|
70d3027bfb | ||
|
|
c1f0b16f20 | ||
|
|
bc9a273831 | ||
|
|
01cec31da0 | ||
|
|
20b044865c | ||
|
|
3653f24e06 | ||
|
|
e35e72402f | ||
|
|
949c9df0a0 | ||
|
|
2d7484a631 | ||
|
|
fee03fddc8 | ||
|
|
8ecd8bb171 | ||
|
|
7651699743 | ||
|
|
e880acca66 | ||
|
|
1162ba4c10 | ||
|
|
f9097807a2 | ||
|
|
b48acccb8d | ||
|
|
21aa2b3501 | ||
|
|
90cbc2524a | ||
|
|
2700c31c13 | ||
|
|
9d659b6afd | ||
|
|
e9853120ce | ||
|
|
242c3b992e | ||
|
|
709a2d0e17 | ||
|
|
15cc91d92a | ||
|
|
2b5965e2e9 | ||
|
|
09f4de619e | ||
|
|
d4690401eb | ||
|
|
42f5a8d01b | ||
|
|
4b1ee21e94 | ||
|
|
55bee4b25e | ||
|
|
3df76d926c | ||
|
|
97f2f67dee | ||
|
|
1204e3728e | ||
|
|
46c7d8ba26 | ||
|
|
a0e6623abe | ||
|
|
d7d67e4ed3 | ||
|
|
28935e33e9 | ||
|
|
834182ffe8 | ||
|
|
d4d0b3b580 | ||
|
|
d0bed07ee0 | ||
|
|
e85b503127 | ||
|
|
94a3ba5837 | ||
|
|
9780724126 | ||
|
|
e9037cdc14 | ||
|
|
e8b255c8f9 | ||
|
|
d4b1a56467 | ||
|
|
325e13c01f | ||
|
|
7b21b0714d | ||
|
|
41103f2732 | ||
|
|
9e751d4161 | ||
|
|
880d7eca50 | ||
|
|
5cb84047b9 | ||
|
|
8f02a358a4 | ||
|
|
44a8b1cbeb | ||
|
|
74d01d14d4 | ||
|
|
6bb6db32a6 | ||
|
|
50ccf7393d | ||
|
|
5ce6fca275 | ||
|
|
abcbc267b5 | ||
|
|
2ffb09bef0 | ||
|
|
6fbdf9fbc8 | ||
|
|
5fe63e311c | ||
|
|
7dbdcbba42 | ||
|
|
bde929b05a | ||
|
|
b7f83b58a9 | ||
|
|
1482e5d67c | ||
|
|
368826fe05 | ||
|
|
b2acbcc189 | ||
|
|
b0fe7310eb | ||
|
|
166394d8d9 | ||
|
|
dd39db847c | ||
|
|
11188f2951 | ||
|
|
f4aef3a983 | ||
|
|
a7dfacb83e | ||
|
|
9f1802f497 | ||
|
|
78ea738b8e | ||
|
|
c5aa0a02e2 | ||
|
|
e41970287d | ||
|
|
4d68fa476d | ||
|
|
9e80a32a4b | ||
|
|
f4975df57b | ||
|
|
c7e3225c5f | ||
|
|
23a5ccd05e | ||
|
|
fa930180c1 | ||
|
|
95a07786bb | ||
|
|
132113f09e | ||
|
|
f2cbf90d1d | ||
|
|
848347e6b3 | ||
|
|
577d62b1da | ||
|
|
b6e43c157b |
No files matched your search
@@ -1 +1,2 @@
|
||||
/target
|
||||
perf.data*
|
||||
Generated
+1952
-552
File diff suppressed because it is too large.
Load diff
+38
-7
@@ -1,13 +1,44 @@
|
||||
[package]
|
||||
name = "gui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
name = "iris"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pollster = "0.4.0"
|
||||
winit = "0.30.11"
|
||||
wgpu = "26.0.1"
|
||||
bytemuck = "1.23.1"
|
||||
iris-core = { workspace = true }
|
||||
iris-macro = { 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", "rig-input"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
pollster = "0.4.0"
|
||||
winit = "0.30.12"
|
||||
wgpu = "30.0.1"
|
||||
bytemuck = "1.23.1"
|
||||
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"
|
||||
wayland-client = "0.31.15"
|
||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||
@@ -0,0 +1,16 @@
|
||||
images
|
||||
settings (sampler)
|
||||
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
|
||||
painter takes them in instead of (or in addition to) id
|
||||
then type wrapper widgets to contain them
|
||||
allows for compile time optimization if a widget wrapper's inner is known at compile time
|
||||
and the id of inner is not needed anywhere
|
||||
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
|
||||
|
||||
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..??
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "iris-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wgpu = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
log = { workspace = true }
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
|
||||
|
||||
pub trait WidgetAttr<Rsc, W: ?Sized> {
|
||||
type Input;
|
||||
fn run(rsc: &mut Rsc, id: WeakWidget<W>, input: Self::Input);
|
||||
}
|
||||
|
||||
pub trait Attrable<Rsc, W: ?Sized, Tag> {
|
||||
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>;
|
||||
}
|
||||
|
||||
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<Rsc, WL::Widget> {
|
||||
|rsc| {
|
||||
let id = self.add(rsc);
|
||||
A::run(rsc, id, input);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use crate::{HasEvents, WeakWidget, Widget};
|
||||
|
||||
pub struct EventCtx<'a, Rsc: HasEvents, Data> {
|
||||
pub state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> {
|
||||
pub widget: WeakWidget<W>,
|
||||
pub state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::{
|
||||
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<Rsc> {
|
||||
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
||||
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
||||
}
|
||||
|
||||
impl<Rsc> Default for EventManager<Rsc> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_to_types: Default::default(),
|
||||
types: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<I: IdLike + 'static, E: EventLike>(
|
||||
&mut self,
|
||||
id: I,
|
||||
event: E,
|
||||
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(i)
|
||||
.or_default()
|
||||
.insert(Self::type_key::<E>());
|
||||
}
|
||||
|
||||
pub fn type_key<E: EventLike>() -> TypeId {
|
||||
TypeId::of::<TypeEventManager<Rsc, E::Event>>()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventsLike {
|
||||
fn remove(&mut self, id: WidgetId);
|
||||
fn draw(&mut self, active: &ActiveData);
|
||||
fn undraw(&mut self, active: &ActiveData);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, active: &ActiveData) {
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().draw(active);
|
||||
}
|
||||
}
|
||||
|
||||
fn undraw(&mut self, active: &ActiveData) {
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().undraw(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventManagerLike<State> {
|
||||
fn remove(&mut self, id: WidgetId);
|
||||
fn draw(&mut self, data: &ActiveData);
|
||||
fn undraw(&mut self, data: &ActiveData);
|
||||
}
|
||||
|
||||
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>>,
|
||||
pub global: E::Global,
|
||||
map: HashMap<WidgetId, Vec<EventData<Rsc, 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() {
|
||||
layer.remove(&id);
|
||||
}
|
||||
}
|
||||
fn draw(&mut self, data: &ActiveData) {
|
||||
self.active
|
||||
.entry(data.layer)
|
||||
.or_default()
|
||||
.entry(data.id)
|
||||
.or_default();
|
||||
}
|
||||
fn undraw(&mut self, data: &ActiveData) {
|
||||
if let Some(layer) = self.active.get_mut(&data.layer) {
|
||||
layer.remove(&data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
global: Default::default(),
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
||||
fn register<I: IdLike + 'static>(
|
||||
&mut self,
|
||||
widget: I,
|
||||
event: impl EventLike<Event = E>,
|
||||
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, rsc| {
|
||||
f(
|
||||
EventIdCtx {
|
||||
widget: WeakWidget::new(widget.id()),
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn run_fn<'a>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a {
|
||||
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
||||
move |ctx, rsc| {
|
||||
let mut consumed = false;
|
||||
for (e, f) in fs {
|
||||
if let Some(data) = e.should_run(&ctx.data) {
|
||||
consumed |= e.consumes(&data);
|
||||
f(
|
||||
EventCtx {
|
||||
state: ctx.state,
|
||||
data,
|
||||
},
|
||||
rsc,
|
||||
)
|
||||
}
|
||||
}
|
||||
consumed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
mod ctx;
|
||||
mod manager;
|
||||
mod rsc;
|
||||
|
||||
pub use ctx::*;
|
||||
pub use manager::*;
|
||||
pub use rsc::*;
|
||||
|
||||
pub trait Event: Sized + 'static + Clone {
|
||||
type Data<'a>: Clone = ();
|
||||
type State: Default = ();
|
||||
/// State the whole event type keeps, rather than one copy per widget.
|
||||
type Global: Default = ();
|
||||
#[allow(unused_variables)]
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
Some(data.clone())
|
||||
}
|
||||
|
||||
/// Whether having run on this data uses up whatever triggered it, so
|
||||
/// nothing further should see it.
|
||||
#[allow(unused_variables)]
|
||||
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventLike {
|
||||
type Event: Event;
|
||||
fn into_event(self) -> Self::Event;
|
||||
}
|
||||
|
||||
impl<E: Event> EventLike for E {
|
||||
type Event = Self;
|
||||
|
||||
fn into_event(self) -> Self::Event {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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<Rsc: HasEvents, Data, W: ?Sized>:
|
||||
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static
|
||||
{
|
||||
}
|
||||
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized>
|
||||
WidgetEventFn<Rsc, Data, W> for F
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use crate::{
|
||||
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||
};
|
||||
|
||||
pub trait HasState: 'static {
|
||||
type State;
|
||||
}
|
||||
|
||||
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: WeakWidget<W>,
|
||||
event: E,
|
||||
f: impl for<'a> WidgetEventFn<Self, <E::Event as Event>::Data<'a>, W>,
|
||||
) {
|
||||
self.events_mut().register(id, event, f);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunEvents: HasEvents {
|
||||
/// Whether anything that ran used up what triggered it.
|
||||
fn run_event<E: EventLike>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
data: <E::Event as Event>::Data<'_>,
|
||||
state: &mut Self::State,
|
||||
) -> bool {
|
||||
let f = self.events_mut().get_type::<E>().run_fn(id);
|
||||
f(EventCtx { state, data }, self)
|
||||
}
|
||||
}
|
||||
impl<T: HasEvents> RunEvents for T {}
|
||||
@@ -0,0 +1,33 @@
|
||||
#![feature(macro_metavar_expr_concat)]
|
||||
#![feature(const_ops)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(const_convert)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_destruct)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
|
||||
mod attr;
|
||||
mod event;
|
||||
mod num;
|
||||
mod orientation;
|
||||
mod primitive;
|
||||
mod render;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
pub mod util;
|
||||
|
||||
pub use attr::*;
|
||||
pub use event::*;
|
||||
pub use num::*;
|
||||
pub use orientation::*;
|
||||
pub use primitive::*;
|
||||
pub use render::*;
|
||||
pub use ui::*;
|
||||
pub use widget::*;
|
||||
|
||||
pub type UiColor = primitive::Color<u8>;
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::util::Vec2;
|
||||
use std::marker::Destruct;
|
||||
|
||||
pub const trait UiNum {
|
||||
fn to_f32(self) -> f32;
|
||||
}
|
||||
|
||||
const impl UiNum for f32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for u32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for i32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
|
||||
Vec2::new(x.to_f32(), y.to_f32())
|
||||
}
|
||||
|
||||
const impl<T: const UiNum + Copy> From<T> for Vec2 {
|
||||
fn from(v: T) -> Self {
|
||||
Self {
|
||||
x: v.to_f32(),
|
||||
y: v.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2
|
||||
where
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
fn from((x, y): (T, U)) -> Self {
|
||||
Self {
|
||||
x: x.to_f32(),
|
||||
y: y.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use crate::vec2;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
}
|
||||
|
||||
impl Align {
|
||||
pub const TOP_LEFT: RegionAlign = RegionAlign::TOP_LEFT;
|
||||
pub const TOP_CENTER: RegionAlign = RegionAlign::TOP_CENTER;
|
||||
pub const TOP_RIGHT: RegionAlign = RegionAlign::TOP_RIGHT;
|
||||
pub const CENTER_LEFT: RegionAlign = RegionAlign::CENTER_LEFT;
|
||||
pub const CENTER: RegionAlign = RegionAlign::CENTER;
|
||||
pub const CENTER_RIGHT: RegionAlign = RegionAlign::CENTER_RIGHT;
|
||||
pub const BOT_LEFT: RegionAlign = RegionAlign::BOT_LEFT;
|
||||
pub const BOT_CENTER: RegionAlign = RegionAlign::BOT_CENTER;
|
||||
pub const BOT_RIGHT: RegionAlign = RegionAlign::BOT_RIGHT;
|
||||
pub const LEFT: CardinalAlign = CardinalAlign::LEFT;
|
||||
pub const H_CENTER: CardinalAlign = CardinalAlign::H_CENTER;
|
||||
pub const RIGHT: CardinalAlign = CardinalAlign::RIGHT;
|
||||
pub const TOP: CardinalAlign = CardinalAlign::TOP;
|
||||
pub const V_CENTER: CardinalAlign = CardinalAlign::V_CENTER;
|
||||
pub const BOT: CardinalAlign = CardinalAlign::BOT;
|
||||
|
||||
pub fn tuple(&self) -> (Option<AxisAlign>, Option<AxisAlign>) {
|
||||
(self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AxisAlign {
|
||||
Neg,
|
||||
Center,
|
||||
Pos,
|
||||
}
|
||||
|
||||
impl AxisAlign {
|
||||
pub const fn rel(&self) -> f32 {
|
||||
match self {
|
||||
Self::Neg => 0.0,
|
||||
Self::Center => 0.5,
|
||||
Self::Pos => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CardinalAlign {
|
||||
axis: Axis,
|
||||
align: AxisAlign,
|
||||
}
|
||||
|
||||
impl CardinalAlign {
|
||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
||||
|
||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||
Self { axis, align }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegionAlign {
|
||||
pub x: AxisAlign,
|
||||
pub y: AxisAlign,
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
|
||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
||||
|
||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
pub const fn rel(&self) -> Vec2 {
|
||||
vec2(self.x.rel(), self.y.rel())
|
||||
}
|
||||
}
|
||||
|
||||
impl UiVec2 {
|
||||
pub fn partial_align(&self, align: Align) -> UiRegion {
|
||||
UiRegion {
|
||||
x: if let Some(align) = align.x {
|
||||
self.x.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
y: if let Some(align) = align.y {
|
||||
self.y.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align(&self, align: RegionAlign) -> UiRegion {
|
||||
UiRegion {
|
||||
x: self.x.align(align.x),
|
||||
y: self.y.align(align.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn partial_align(&self, align: Align) -> UiRegion {
|
||||
let s = UiVec2::from(*self);
|
||||
UiRegion {
|
||||
x: if let Some(align) = align.x {
|
||||
s.x.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
y: if let Some(align) = align.y {
|
||||
s.y.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align(&self, align: RegionAlign) -> UiRegion {
|
||||
let s = UiVec2::from(*self);
|
||||
UiRegion {
|
||||
x: s.x.align(align.x),
|
||||
y: s.y.align(align.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiScalar {
|
||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||
let rel = align.rel();
|
||||
let mut start = UiScalar::rel(rel);
|
||||
start.abs -= self.abs * rel;
|
||||
start.rel -= self.rel * rel;
|
||||
let mut end = UiScalar::rel(rel);
|
||||
end.abs += self.abs * (1.0 - rel);
|
||||
end.rel += self.rel * (1.0 - rel);
|
||||
UiSpan { start, end }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegionAlign> for Align {
|
||||
fn from(region: RegionAlign) -> Self {
|
||||
Self {
|
||||
x: Some(region.x),
|
||||
y: Some(region.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Align> for RegionAlign {
|
||||
fn from(align: Align) -> Self {
|
||||
Self {
|
||||
x: align.x.unwrap_or(AxisAlign::Center),
|
||||
y: align.y.unwrap_or(AxisAlign::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardinalAlign> for RegionAlign {
|
||||
fn from(align: CardinalAlign) -> Self {
|
||||
Align::from(align).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardinalAlign> for Align {
|
||||
fn from(cardinal: CardinalAlign) -> Self {
|
||||
let align = Some(cardinal.align);
|
||||
match cardinal.axis {
|
||||
Axis::X => Self { x: align, y: None },
|
||||
Axis::Y => Self { x: None, y: align },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl From<RegionAlign> for UiVec2 {
|
||||
fn from(align: RegionAlign) -> Self {
|
||||
Self::rel(align.rel())
|
||||
}
|
||||
}
|
||||
|
||||
impl RegionAlign {
|
||||
pub const fn pos(self) -> UiVec2 {
|
||||
UiVec2::from(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Axis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
impl std::ops::Not for Axis {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
Self::X => Self::Y,
|
||||
Self::Y => Self::X,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub struct Dir {
|
||||
pub axis: Axis,
|
||||
pub sign: Sign,
|
||||
}
|
||||
|
||||
impl Dir {
|
||||
pub const fn new(axis: Axis, dir: Sign) -> Self {
|
||||
Self { axis, sign: dir }
|
||||
}
|
||||
|
||||
pub const LEFT: Self = Self::new(Axis::X, Sign::Neg);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, Sign::Pos);
|
||||
pub const UP: Self = Self::new(Axis::Y, Sign::Neg);
|
||||
pub const DOWN: Self = Self::new(Axis::Y, Sign::Pos);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Sign {
|
||||
Neg,
|
||||
Pos,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn axis(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
Axis::X => aligned,
|
||||
Axis::Y => ortho,
|
||||
},
|
||||
y: match axis {
|
||||
Axis::Y => aligned,
|
||||
Axis::X => ortho,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait AxisT {
|
||||
fn get() -> Axis;
|
||||
}
|
||||
|
||||
pub struct XAxis;
|
||||
const impl AxisT for XAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::X
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YAxis;
|
||||
const impl AxisT for YAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::Y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BothAxis<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> BothAxis<T> {
|
||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
||||
match A::get() {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
||||
match A::get() {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
use super::*;
|
||||
use crate::{UiNum, util::impl_op};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct Size {
|
||||
pub x: Len,
|
||||
pub y: Len,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Len {
|
||||
pub abs: f32,
|
||||
pub rel: f32,
|
||||
pub rest: f32,
|
||||
}
|
||||
|
||||
impl<N: UiNum> From<N> for Len {
|
||||
fn from(value: N) -> Self {
|
||||
Len::abs(value.to_f32())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
|
||||
fn from((x, y): (Nx, Ny)) -> Self {
|
||||
Self {
|
||||
x: x.into(),
|
||||
y: y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Len> for Size {
|
||||
fn from(value: Len) -> Self {
|
||||
Self { x: value, y: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub const ZERO: Self = Self {
|
||||
x: Len::ZERO,
|
||||
y: Len::ZERO,
|
||||
};
|
||||
|
||||
pub const REST: Self = Self {
|
||||
x: Len::REST,
|
||||
y: Len::REST,
|
||||
};
|
||||
|
||||
pub fn abs(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: Len::abs(v.x),
|
||||
y: Len::abs(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rel(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: Len::rel(v.x),
|
||||
y: Len::rel(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rest(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: Len::rest(v.x),
|
||||
y: Len::rest(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_uivec2(self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.apply_rest(),
|
||||
y: self.y.apply_rest(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self {
|
||||
x: aligned,
|
||||
y: ortho,
|
||||
},
|
||||
Axis::Y => Self {
|
||||
x: ortho,
|
||||
y: aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Len {
|
||||
pub const ZERO: Self = Self {
|
||||
abs: 0.0,
|
||||
rel: 0.0,
|
||||
rest: 0.0,
|
||||
};
|
||||
|
||||
pub const REST: Self = Self {
|
||||
abs: 0.0,
|
||||
rel: 0.0,
|
||||
rest: 1.0,
|
||||
};
|
||||
|
||||
pub fn apply_rest(&self) -> UiScalar {
|
||||
UiScalar {
|
||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
||||
abs: self.abs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn abs(abs: impl UiNum) -> Self {
|
||||
Self {
|
||||
abs: abs.to_f32(),
|
||||
rel: 0.0,
|
||||
rest: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn rel(rel: impl UiNum) -> Self {
|
||||
Self {
|
||||
abs: 0.0,
|
||||
rel: rel.to_f32(),
|
||||
rest: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn rest(ratio: impl UiNum) -> Self {
|
||||
Self {
|
||||
abs: 0.0,
|
||||
rel: 0.0,
|
||||
rest: ratio.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod len_fns {
|
||||
use super::*;
|
||||
|
||||
pub fn abs(abs: impl UiNum) -> Len {
|
||||
Len {
|
||||
abs: abs.to_f32(),
|
||||
rel: 0.0,
|
||||
rest: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn rel(rel: impl UiNum) -> Len {
|
||||
Len {
|
||||
abs: 0.0,
|
||||
rel: rel.to_f32(),
|
||||
rest: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn rest(ratio: impl UiNum) -> Len {
|
||||
Len {
|
||||
abs: 0.0,
|
||||
rel: 0.0,
|
||||
rest: ratio.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(Len Add add; abs rel rest);
|
||||
impl_op!(Len Sub sub; abs rel rest);
|
||||
|
||||
impl_op!(Size Add add; x y);
|
||||
impl_op!(Size Sub sub; x y);
|
||||
|
||||
impl Default for Len {
|
||||
fn default() -> Self {
|
||||
Self::rest(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Len {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.abs != 0.0 {
|
||||
write!(f, "{} abs;", self.abs)?;
|
||||
}
|
||||
if self.rel != 0.0 {
|
||||
write!(f, "{} rel;", self.rel)?;
|
||||
}
|
||||
if self.rest != 0.0 {
|
||||
write!(f, "{} rest;", self.rest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod align;
|
||||
mod axis;
|
||||
mod len;
|
||||
mod pos;
|
||||
|
||||
use crate::util::Vec2;
|
||||
|
||||
pub use align::*;
|
||||
pub use axis::*;
|
||||
pub use len::*;
|
||||
pub use pos::*;
|
||||
@@ -0,0 +1,496 @@
|
||||
use std::{fmt::Display, hash::Hash, marker::Destruct};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
UiNum,
|
||||
util::{LerpUtil, impl_op},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct UiVec2 {
|
||||
pub x: UiScalar,
|
||||
pub y: UiScalar,
|
||||
}
|
||||
|
||||
impl UiVec2 {
|
||||
pub const ZERO: Self = Self {
|
||||
x: UiScalar::ZERO,
|
||||
y: UiScalar::ZERO,
|
||||
};
|
||||
|
||||
pub const fn new(x: UiScalar, y: UiScalar) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
|
||||
let abs = abs.into();
|
||||
Self {
|
||||
x: UiScalar::abs(abs.x),
|
||||
y: UiScalar::abs(abs.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
||||
let rel = rel.into();
|
||||
Self {
|
||||
x: UiScalar::rel(rel.x),
|
||||
y: UiScalar::rel(rel.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn shift(&mut self, offset: impl const Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
*self += offset;
|
||||
}
|
||||
|
||||
pub const fn offset(mut self, offset: impl const Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn within(&self, region: &UiRegion) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.within(®ion.x),
|
||||
y: self.y.within(®ion.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> UiScalar {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x.to_abs(rel.x),
|
||||
y: self.y.to_abs(rel.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self {
|
||||
x: aligned,
|
||||
y: ortho,
|
||||
},
|
||||
Axis::Y => Self {
|
||||
x: ortho,
|
||||
y: aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_abs(&self) -> Vec2 {
|
||||
(self.x.abs, self.y.abs).into()
|
||||
}
|
||||
|
||||
pub fn get_rel(&self) -> Vec2 {
|
||||
(self.x.rel, self.y.rel).into()
|
||||
}
|
||||
|
||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
||||
Vec2View {
|
||||
x: &mut self.x.abs,
|
||||
y: &mut self.y.abs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UiVec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(UiVec2 Add add; x y);
|
||||
impl_op!(UiVec2 Sub sub; x y);
|
||||
|
||||
const impl From<Vec2> for UiVec2 {
|
||||
fn from(abs: Vec2) -> Self {
|
||||
Self::abs(abs)
|
||||
}
|
||||
}
|
||||
|
||||
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
|
||||
where
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
fn from(abs: (T, U)) -> Self {
|
||||
Self::abs(abs)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||
pub struct UiScalar {
|
||||
pub rel: f32,
|
||||
pub abs: f32,
|
||||
}
|
||||
|
||||
impl Eq for UiScalar {}
|
||||
impl Hash for UiScalar {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write_u32(self.rel.to_bits());
|
||||
state.write_u32(self.abs.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(UiScalar Add add; rel abs);
|
||||
impl_op!(UiScalar Sub sub; rel abs);
|
||||
|
||||
impl UiScalar {
|
||||
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
|
||||
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
|
||||
|
||||
pub const fn new(rel: f32, abs: f32) -> Self {
|
||||
Self { rel, abs }
|
||||
}
|
||||
|
||||
pub const fn rel(rel: f32) -> Self {
|
||||
Self { rel, abs: 0.0 }
|
||||
}
|
||||
|
||||
pub const fn abs(abs: f32) -> Self {
|
||||
Self { rel: 0.0, abs }
|
||||
}
|
||||
|
||||
pub const fn rel_min() -> Self {
|
||||
Self::new(0.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn rel_max() -> Self {
|
||||
Self::new(1.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn max(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.max(other.rel),
|
||||
abs: self.abs.max(other.abs),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn min(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.min(other.rel),
|
||||
abs: self.abs.min(other.abs),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn offset(mut self, amt: f32) -> Self {
|
||||
self.abs += amt;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn within(&self, span: &UiSpan) -> Self {
|
||||
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
||||
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
|
||||
Self {
|
||||
rel: anchor,
|
||||
abs: offset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Undoes `within`, and `None` where the span has a fixed length: every
|
||||
/// fraction of it lands on the same `rel`, so none can be told apart.
|
||||
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
|
||||
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
|
||||
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
||||
Some(Self { rel, abs })
|
||||
}
|
||||
|
||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||
self.within(&UiSpan {
|
||||
start: UiScalar::ZERO,
|
||||
end: len,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_len(&self, len: UiScalar) -> Self {
|
||||
len.within_len(*self)
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.rel = 1.0 - self.rel;
|
||||
self.abs = -self.abs;
|
||||
}
|
||||
|
||||
pub const fn to(&self, end: Self) -> UiSpan {
|
||||
UiSpan { start: *self, end }
|
||||
}
|
||||
|
||||
pub const fn to_abs(&self, rel: f32) -> f32 {
|
||||
self.rel * rel + self.abs
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct UiSpan {
|
||||
pub start: UiScalar,
|
||||
pub end: UiScalar,
|
||||
}
|
||||
|
||||
impl UiSpan {
|
||||
pub const FULL: Self = Self {
|
||||
start: UiScalar::ZERO,
|
||||
end: UiScalar::FULL,
|
||||
};
|
||||
|
||||
pub const fn rel(rel: f32) -> Self {
|
||||
Self {
|
||||
start: UiScalar::rel(rel),
|
||||
end: UiScalar::rel(rel),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn new(start: UiScalar, end: UiScalar) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.start.flip();
|
||||
self.end.flip();
|
||||
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
||||
std::mem::swap(&mut self.start.abs, &mut self.end.abs);
|
||||
}
|
||||
|
||||
pub const fn shift(&mut self, offset: UiScalar) {
|
||||
self.start += offset;
|
||||
self.end += offset;
|
||||
}
|
||||
|
||||
pub const fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
start: self.start.within(parent),
|
||||
end: self.end.within(parent),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outside(&self, parent: &Self) -> Option<Self> {
|
||||
Some(Self {
|
||||
start: self.start.outside(parent)?,
|
||||
end: self.end.outside(parent)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> UiScalar {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct UiRegion {
|
||||
pub x: UiSpan,
|
||||
pub y: UiSpan,
|
||||
}
|
||||
|
||||
impl UiRegion {
|
||||
pub const FULL: Self = Self {
|
||||
x: UiSpan::FULL,
|
||||
y: UiSpan::FULL,
|
||||
};
|
||||
|
||||
pub const fn new(x: UiSpan, y: UiSpan) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn rel(rel: Vec2) -> Self {
|
||||
Self {
|
||||
x: UiSpan::rel(rel.x),
|
||||
y: UiSpan::rel(rel.y),
|
||||
}
|
||||
}
|
||||
pub const fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
x: self.x.within(&parent.x),
|
||||
y: self.y.within(&parent.y),
|
||||
}
|
||||
}
|
||||
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||
match axis {
|
||||
Axis::X => &self.x,
|
||||
Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self, axis: Axis) {
|
||||
match axis {
|
||||
Axis::X => self.x.flip(),
|
||||
Axis::Y => self.y.flip(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
self.x.shift(offset.x);
|
||||
self.y.shift(offset.y);
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
||||
PixelRegion {
|
||||
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn center(&self) -> UiVec2 {
|
||||
Align::CENTER.pos().within(self)
|
||||
}
|
||||
|
||||
pub const fn size(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.len(),
|
||||
y: self.y.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn top_left(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.start,
|
||||
y: self.y.start,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn bot_right(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.end,
|
||||
y: self.y.end,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: UiSpan, ortho: UiSpan) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
Axis::X => aligned,
|
||||
Axis::Y => ortho,
|
||||
},
|
||||
y: match axis {
|
||||
Axis::X => ortho,
|
||||
Axis::Y => aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Taking a drawing out of one box and putting it in another, checked once
|
||||
/// for a whole subtree so that applying it cannot fail.
|
||||
///
|
||||
/// A box of a fixed length holds each part as an offset from its start rather
|
||||
/// than as a fraction of it, so those parts can be carried to a box of the
|
||||
/// same length but never stretched to a different one.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Remap {
|
||||
from: UiRegion,
|
||||
to: UiRegion,
|
||||
}
|
||||
|
||||
impl Remap {
|
||||
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
|
||||
[Axis::X, Axis::Y]
|
||||
.into_iter()
|
||||
.all(|axis| {
|
||||
let (from, to) = (from.axis(axis), to.axis(axis));
|
||||
from.start.rel != from.end.rel || from.len() == to.len()
|
||||
})
|
||||
.then_some(Self { from, to })
|
||||
}
|
||||
|
||||
pub fn apply(&self, region: UiRegion) -> UiRegion {
|
||||
UiRegion {
|
||||
x: Self::span(region.x, self.from.x, self.to.x),
|
||||
y: Self::span(region.y, self.from.y, self.to.y),
|
||||
}
|
||||
}
|
||||
|
||||
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
|
||||
match span.outside(&from) {
|
||||
Some(out) => out.within(&to),
|
||||
// `new` admits this only where the two are the same length, so
|
||||
// the difference between their starts is the whole move.
|
||||
None => {
|
||||
let mut span = span;
|
||||
span.shift(to.start - from.start);
|
||||
span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UiRegion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} -> {} (size: {})",
|
||||
self.top_left(),
|
||||
self.bot_right(),
|
||||
self.size()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PixelRegion {
|
||||
pub top_left: Vec2,
|
||||
pub bot_right: Vec2,
|
||||
}
|
||||
|
||||
impl PixelRegion {
|
||||
pub fn contains(&self, pos: Vec2) -> bool {
|
||||
pos.x >= self.top_left.x
|
||||
&& pos.x <= self.bot_right.x
|
||||
&& pos.y >= self.top_left.y
|
||||
&& pos.y <= self.bot_right.y
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.bot_right - self.top_left
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PixelRegion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Vec2View<'a> {
|
||||
pub x: &'a mut f32,
|
||||
pub y: &'a mut f32,
|
||||
}
|
||||
|
||||
impl Vec2View<'_> {
|
||||
pub fn set(&mut self, other: Vec2) {
|
||||
*self.x = other.x;
|
||||
*self.y = other.y;
|
||||
}
|
||||
|
||||
pub fn add(&mut self, other: Vec2) {
|
||||
*self.x += other.x;
|
||||
*self.y += other.y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::marker::Destruct;
|
||||
|
||||
/// stored in linear for sane manipulation
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
|
||||
pub struct Color<T> {
|
||||
pub r: T,
|
||||
pub g: T,
|
||||
pub b: 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);
|
||||
pub const GRAY: Self = Self::rgb(T::MID, T::MID, T::MID);
|
||||
|
||||
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
|
||||
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
|
||||
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
|
||||
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
|
||||
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
|
||||
pub const TURQUOISE: Self = Self::rgb(T::MIN, T::MAX, T::MID);
|
||||
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
|
||||
pub const SKY: Self = Self::rgb(T::MIN, T::MID, T::MAX);
|
||||
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
|
||||
pub const PURPLE: Self = Self::rgb(T::MID, T::MIN, T::MAX);
|
||||
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
|
||||
|
||||
pub const NONE: Self = Self::new(T::MIN, T::MIN, T::MIN, T::MIN);
|
||||
|
||||
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
pub const fn rgb(r: T, g: T, b: T) -> Self {
|
||||
Self { r, g, b, a: T::MAX }
|
||||
}
|
||||
pub fn alpha(mut self, a: T) -> Self {
|
||||
self.a = a;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_arr(self) -> [T; 4] {
|
||||
[self.r, self.g, self.b, self.a]
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait F32Conversion {
|
||||
fn to(self) -> f32;
|
||||
fn from(x: f32) -> Self;
|
||||
}
|
||||
|
||||
pub trait ColorNum {
|
||||
const MIN: Self;
|
||||
const MID: Self;
|
||||
const MAX: Self;
|
||||
}
|
||||
|
||||
macro_rules! map_rgb {
|
||||
($x:ident,$self:ident, $e:tt) => {
|
||||
#[allow(unused_braces)]
|
||||
Self {
|
||||
r: {
|
||||
let $x = $self.r;
|
||||
$e
|
||||
},
|
||||
g: {
|
||||
let $x = $self.g;
|
||||
$e
|
||||
},
|
||||
b: {
|
||||
let $x = $self.b;
|
||||
$e
|
||||
},
|
||||
a: $self.a,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<T: ColorNum + const F32Conversion> Color<T>
|
||||
where
|
||||
Self: const Destruct,
|
||||
{
|
||||
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||
let amt = amt.to();
|
||||
map_rgb!(x, self, { T::from(x.to() * amt) })
|
||||
}
|
||||
|
||||
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||
let amt = amt.to();
|
||||
map_rgb!(x, self, { T::from(x.to() + amt) })
|
||||
}
|
||||
|
||||
pub const fn darker(self, amt: f32) -> Self {
|
||||
self.mul_rgb(1.0 - amt)
|
||||
}
|
||||
|
||||
pub const fn brighter(self, amt: f32) -> Self {
|
||||
map_rgb!(x, self, {
|
||||
let x = x.to();
|
||||
T::from(x + (1.0 - x) * amt)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_rgb(self, f: impl Fn(T) -> T) -> Self {
|
||||
Self {
|
||||
r: f(self.r),
|
||||
g: f(self.g),
|
||||
b: f(self.b),
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn srgb(r: T, g: T, b: T) -> Self {
|
||||
Self {
|
||||
r: s_to_l(r),
|
||||
g: s_to_l(g),
|
||||
b: s_to_l(b),
|
||||
a: T::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn s_to_l<T: F32Conversion>(x: T) -> T {
|
||||
let x = x.to();
|
||||
T::from(if x <= 0.0405 {
|
||||
x / 12.92
|
||||
} else {
|
||||
((x + 0.055) / 1.055).powf(2.4)
|
||||
})
|
||||
}
|
||||
|
||||
impl ColorNum for u8 {
|
||||
const MIN: Self = u8::MIN;
|
||||
const MID: Self = u8::MAX / 2;
|
||||
const MAX: Self = u8::MAX;
|
||||
}
|
||||
|
||||
impl ColorNum for f32 {
|
||||
const MIN: Self = 0.0;
|
||||
const MID: Self = 0.5;
|
||||
const MAX: Self = 1.0;
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for Color<u8> {}
|
||||
|
||||
const impl F32Conversion for f32 {
|
||||
fn to(self) -> f32 {
|
||||
self
|
||||
}
|
||||
fn from(x: f32) -> Self {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
const impl F32Conversion for u8 {
|
||||
fn to(self) -> f32 {
|
||||
self as f32 / 255.0
|
||||
}
|
||||
fn from(x: f32) -> Self {
|
||||
(x * 255.0).clamp(0.0, 255.0) as Self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{
|
||||
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
util::to_mut,
|
||||
};
|
||||
|
||||
pub type LayerId = usize;
|
||||
|
||||
struct LayerNode<T> {
|
||||
next: Ptr,
|
||||
prev: Ptr,
|
||||
child: Option<Child>,
|
||||
depth: usize,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Ptr {
|
||||
/// continue on same level
|
||||
Next(usize),
|
||||
/// go back to parent
|
||||
Parent(usize),
|
||||
/// end
|
||||
None,
|
||||
}
|
||||
|
||||
/// TODO: currently this does not ever free layers
|
||||
/// is that realistically desired?
|
||||
pub struct Layers<T> {
|
||||
vec: Vec<LayerNode<T>>,
|
||||
/// index of last layer at top level (start at first = 0)
|
||||
last: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Child {
|
||||
head: usize,
|
||||
tail: usize,
|
||||
}
|
||||
|
||||
pub type DrawLayers = Layers<LayerDraws>;
|
||||
|
||||
impl<T: Default> Layers<T> {
|
||||
pub fn new() -> Layers<T> {
|
||||
Self {
|
||||
vec: vec![LayerNode::head()],
|
||||
last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.vec.clear();
|
||||
self.vec.push(LayerNode::head());
|
||||
}
|
||||
|
||||
fn push(&mut self, node: LayerNode<T>) -> LayerId {
|
||||
let i = self.vec.len();
|
||||
self.vec.push(node);
|
||||
i
|
||||
}
|
||||
|
||||
pub fn next(&mut self, i: LayerId) -> LayerId {
|
||||
if let Ptr::Next(i) = self.vec[i].next {
|
||||
return i;
|
||||
}
|
||||
let i_new = self.push(LayerNode::new(
|
||||
T::default(),
|
||||
self.vec[i].next,
|
||||
Ptr::Next(i),
|
||||
self.vec[i].depth,
|
||||
));
|
||||
self.vec[i].next = Ptr::Next(i_new);
|
||||
self.vec[i_new].prev = Ptr::Next(i);
|
||||
match self.vec[i_new].next {
|
||||
Ptr::Next(i) => self.vec[i].prev = Ptr::Next(i_new),
|
||||
Ptr::Parent(i) => self.vec[i].child.as_mut().unwrap().tail = i_new,
|
||||
Ptr::None => self.last = i_new,
|
||||
}
|
||||
i_new
|
||||
}
|
||||
|
||||
pub fn child(&mut self, i: LayerId) -> LayerId {
|
||||
if let Some(c) = self.vec[i].child {
|
||||
return c.head;
|
||||
}
|
||||
let i_child = self.push(LayerNode::new(
|
||||
T::default(),
|
||||
Ptr::Parent(i),
|
||||
Ptr::Parent(i),
|
||||
self.vec[i].depth + 1,
|
||||
));
|
||||
self.vec[i].child = Some(Child {
|
||||
head: i_child,
|
||||
tail: i_child,
|
||||
});
|
||||
i_child
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> LayerIteratorMut<'_, T> {
|
||||
LayerIteratorMut::new(&mut self.vec, self.last)
|
||||
}
|
||||
|
||||
pub fn iter_orderless_mut(&mut self) -> impl Iterator<Item = (usize, &mut T)> {
|
||||
self.vec.iter_mut().map(|n| &mut n.data).enumerate()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (LayerId, &T)> {
|
||||
self.indices().map(|i| (i, &self.vec[i].data))
|
||||
}
|
||||
|
||||
pub fn iter_depth(&self) -> impl Iterator<Item = ((LayerId, usize), &T)> {
|
||||
self.indices()
|
||||
.map(|i| ((i, self.vec[i].depth), &self.vec[i].data))
|
||||
}
|
||||
|
||||
pub fn indices(&self) -> LayerIndexIterator<'_, T> {
|
||||
LayerIndexIterator::new(&self.vec, self.last)
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawLayers {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: LayerId,
|
||||
info: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self[layer].write(layer, info)
|
||||
}
|
||||
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self[h.layer].free(h)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Layers<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<LayerId> for Layers<T> {
|
||||
type Output = T;
|
||||
|
||||
fn index(&self, index: LayerId) -> &Self::Output {
|
||||
&self.vec[index].data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<LayerId> for Layers<T> {
|
||||
fn index_mut(&mut self, index: LayerId) -> &mut Self::Output {
|
||||
&mut self.vec[index].data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> LayerNode<T> {
|
||||
pub fn new(data: T, next: Ptr, prev: Ptr, depth: usize) -> Self {
|
||||
Self {
|
||||
next,
|
||||
prev,
|
||||
child: None,
|
||||
data,
|
||||
depth,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn head() -> Self {
|
||||
Self::new(T::default(), Ptr::None, Ptr::None, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LayerIteratorMut<'a, T> {
|
||||
inner: LayerIndexIterator<'a, T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LayerIteratorMut<'a, T> {
|
||||
type Item = (usize, &'a mut T);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let i = self.inner.next()?;
|
||||
// SAFETY: requires index iterator to work properly
|
||||
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
|
||||
Some((i, layer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DoubleEndedIterator for LayerIteratorMut<'a, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let i = self.inner.next_back()?;
|
||||
// SAFETY: requires index iterator to work properly
|
||||
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
|
||||
Some((i, layer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> LayerIteratorMut<'a, T> {
|
||||
fn new(vec: &'a mut Vec<LayerNode<T>>, last: usize) -> Self {
|
||||
Self {
|
||||
inner: LayerIndexIterator::new(vec, last),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LayerIndexIterator<'a, T> {
|
||||
next: Option<usize>,
|
||||
next_back: Option<usize>,
|
||||
vec: &'a Vec<LayerNode<T>>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LayerIndexIterator<'a, T> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let ret_i = self.next?;
|
||||
let node = &self.vec[ret_i];
|
||||
self.next = if let Some(c) = node.child {
|
||||
Some(c.head)
|
||||
} else if let Ptr::Next(i) = node.next {
|
||||
Some(i)
|
||||
} else if let Ptr::Parent(i) = node.next {
|
||||
let mut node = &self.vec[i];
|
||||
while let Ptr::Parent(i) = node.next {
|
||||
node = &self.vec[i];
|
||||
}
|
||||
if let Ptr::Next(i) = node.next {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if self.next_back.unwrap() == ret_i {
|
||||
self.next = None;
|
||||
self.next_back = None;
|
||||
}
|
||||
Some(ret_i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DoubleEndedIterator for LayerIndexIterator<'a, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let ret_i = self.next_back?;
|
||||
let node = &self.vec[ret_i];
|
||||
self.next_back = if let Ptr::Next(mut i) = node.prev {
|
||||
while let Some(c) = self.vec[i].child {
|
||||
i = c.tail
|
||||
}
|
||||
Some(i)
|
||||
} else if let Ptr::Parent(i) = node.prev {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if self.next.unwrap() == ret_i {
|
||||
self.next = None;
|
||||
self.next_back = None;
|
||||
}
|
||||
Some(ret_i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> LayerIndexIterator<'a, T> {
|
||||
fn new(vec: &'a Vec<LayerNode<T>>, last: usize) -> Self {
|
||||
let mut last = last;
|
||||
while let Some(c) = vec[last].child {
|
||||
last = c.tail;
|
||||
}
|
||||
Self {
|
||||
next: Some(0),
|
||||
next_back: Some(last),
|
||||
vec,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod color;
|
||||
mod layer;
|
||||
mod text;
|
||||
mod texture;
|
||||
|
||||
pub use color::*;
|
||||
pub use layer::*;
|
||||
pub use text::*;
|
||||
pub use texture::*;
|
||||
@@ -0,0 +1,282 @@
|
||||
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},
|
||||
};
|
||||
|
||||
pub struct TextData {
|
||||
pub font_ctx: FontContext,
|
||||
pub layout_ctx: LayoutContext<UiColor>,
|
||||
scale_ctx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_ctx: FontContext::new(),
|
||||
layout_ctx: LayoutContext::new(),
|
||||
scale_ctx: ScaleContext::new(),
|
||||
atlas: GlyphAtlas::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Family {
|
||||
fn family(&self) -> FontFamily<'_> {
|
||||
let name = match self {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
pub wrap: bool,
|
||||
pub align: RegionAlign,
|
||||
}
|
||||
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl Default for TextAttrs {
|
||||
fn default() -> Self {
|
||||
let size = 16.0;
|
||||
Self {
|
||||
color: UiColor::WHITE,
|
||||
font_size: size,
|
||||
line_height: size * LINE_HEIGHT_MULT,
|
||||
family: Family::SansSerif,
|
||||
wrap: false,
|
||||
align: Align::CENTER_LEFT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 place(&mut self, buffer: &TextBuffer) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
||||
continue;
|
||||
};
|
||||
let font = run.run().font();
|
||||
let font_size = run.run().font_size();
|
||||
let coords = run.run().normalized_coords();
|
||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
let font_id = font.data.id();
|
||||
|
||||
for glyph in run.positioned_glyphs() {
|
||||
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
|
||||
let key = GlyphKey {
|
||||
font: font_id,
|
||||
glyph: glyph.id,
|
||||
size: glyph_size_key(font_size),
|
||||
subpixel,
|
||||
coords: coords_hash,
|
||||
};
|
||||
let Some(entry) = self.glyph_entry(GlyphRaster {
|
||||
key,
|
||||
font: font_ref,
|
||||
font_size,
|
||||
coords,
|
||||
subpixel,
|
||||
glyph_id: glyph.id,
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
placed.push(PlacedGlyph {
|
||||
entry,
|
||||
offset: Vec2::new(
|
||||
glyph.x.floor() + entry.left as f32,
|
||||
glyph.y.floor() - entry.top as f32,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
placed
|
||||
}
|
||||
|
||||
fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option<GlyphEntry> {
|
||||
if let Some(entry) = self.atlas.get(&glyph.key) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
let mut scaler = self
|
||||
.scale_ctx
|
||||
.builder(glyph.font)
|
||||
.size(glyph.font_size)
|
||||
.hint(true)
|
||||
.normalized_coords(glyph.coords)
|
||||
.build();
|
||||
let image = Render::new(&[
|
||||
Source::ColorOutline(0),
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
Source::Outline,
|
||||
])
|
||||
.format(Format::Alpha)
|
||||
.offset(Vector::new(glyph.subpixel as f32 / 4.0, 0.0))
|
||||
.render(&mut scaler, glyph.glyph_id as u16);
|
||||
|
||||
if let Some(image) = image {
|
||||
self.atlas.insert(glyph.key, &image)
|
||||
} else {
|
||||
self.atlas.insert_empty(glyph.key);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GlyphRaster<'a> {
|
||||
key: GlyphKey,
|
||||
font: FontRef<'a>,
|
||||
font_size: f32,
|
||||
coords: &'a [i16],
|
||||
subpixel: u8,
|
||||
glyph_id: u32,
|
||||
}
|
||||
|
||||
fn hash_coords(coords: &[i16]) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
coords.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
const GLYPH_SIZE_STEPS_PER_PIXEL: f32 = 16.0;
|
||||
|
||||
fn glyph_size_key(font_size: f32) -> u32 {
|
||||
(font_size * GLYPH_SIZE_STEPS_PER_PIXEL).round() as u32
|
||||
}
|
||||
|
||||
pub struct RenderedText {
|
||||
pub glyphs: Vec<PlacedGlyph>,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn render(
|
||||
&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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use crate::util::{RefCounter, Vec2};
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use std::{
|
||||
ops::Index,
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextureHandle {
|
||||
slot: u32,
|
||||
size: Vec2,
|
||||
counter: RefCounter,
|
||||
send: Sender<u32>,
|
||||
}
|
||||
|
||||
/// a texture manager for a ui
|
||||
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
|
||||
pub struct Textures {
|
||||
free: Vec<u32>,
|
||||
images: Vec<Option<DynamicImage>>,
|
||||
updates: Vec<Update>,
|
||||
send: Sender<u32>,
|
||||
recv: Receiver<u32>,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
impl Textures {
|
||||
pub fn new() -> Self {
|
||||
let (send, recv) = channel();
|
||||
Self {
|
||||
free: Vec::new(),
|
||||
images: Vec::new(),
|
||||
updates: Vec::new(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||
let image = image.into();
|
||||
let size = image.dimensions().into();
|
||||
TextureHandle {
|
||||
slot: self.push(image),
|
||||
size,
|
||||
counter: RefCounter::new(),
|
||||
send: self.send.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, image: DynamicImage) -> u32 {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.images[i as usize] = Some(image);
|
||||
self.updates.push(Update::Set(i));
|
||||
i
|
||||
} else {
|
||||
let i = self.images.len() as u32;
|
||||
self.images.push(Some(image));
|
||||
self.updates.push(Update::Push(i));
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
self.updates.push(Update::Free(idx));
|
||||
self.free.push(idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
||||
self.updates.drain(..).map(|u| match u {
|
||||
Update::Push(i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(TextureUpdate::Push)
|
||||
.unwrap_or(TextureUpdate::PushFree),
|
||||
Update::Set(i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Patch(i, rect, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Free(i) => TextureUpdate::Free(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureHandle {
|
||||
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||
pub fn slot(&self) -> u32 {
|
||||
self.slot
|
||||
}
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TextureHandle {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<&TextureHandle> for Textures {
|
||||
type Output = DynamicImage;
|
||||
|
||||
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
||||
self.images[index.slot as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Textures {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::{UiRegion, util::Id};
|
||||
use wgpu::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct WindowUniform {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
3 => Float32x2,
|
||||
4 => Uint32,
|
||||
];
|
||||
|
||||
pub fn desc() -> VertexBufferLayout<'static> {
|
||||
VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Self>() as BufferAddress,
|
||||
step_mode: VertexStepMode::Instance,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type MaskIdx = Id<u32>;
|
||||
|
||||
impl MaskIdx {
|
||||
pub const NONE: Self = Self::preset(u32::MAX);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Mask {
|
||||
pub region: UiRegion,
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
use crate::{
|
||||
UiData, UiRenderState,
|
||||
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
};
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod page;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||
|
||||
pub struct UiRenderNode {
|
||||
shared_layout: BindGroupLayout,
|
||||
shared_group: BindGroup,
|
||||
format: TextureFormat,
|
||||
|
||||
/// One per registered primitive, in id order.
|
||||
primitives: Vec<PrimitivePipeline>,
|
||||
|
||||
layers: HashMap<usize, RenderLayer>,
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
masks: ArrBuf<Mask>,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
/// One per registered primitive, `None` where this layer draws none.
|
||||
primitives: Vec<Option<ListBuffers>>,
|
||||
}
|
||||
|
||||
/// What draws one registered primitive.
|
||||
struct PrimitivePipeline {
|
||||
data_layout: BindGroupLayout,
|
||||
pipeline: RenderPipeline,
|
||||
render: Box<dyn PrimitiveRender>,
|
||||
}
|
||||
|
||||
/// One list's vertex buffer and the data its shader reads.
|
||||
struct ListBuffers {
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
data: ArrBuf<u8>,
|
||||
group: Option<BindGroup>,
|
||||
/// What the primitive asked to keep per instance, if anything.
|
||||
bindings: Vec<u32>,
|
||||
}
|
||||
|
||||
impl UiRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||
for i in &self.active {
|
||||
let layer = &self.layers[i];
|
||||
for (id, list) in layer.primitives.iter().enumerate() {
|
||||
let Some(list) = list else { continue };
|
||||
let Some(group) = &list.group else { continue };
|
||||
let primitive = &self.primitives[id];
|
||||
pass.set_pipeline(&primitive.pipeline);
|
||||
pass.set_bind_group(1, group, &[]);
|
||||
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||
primitive.render.draw(
|
||||
pass,
|
||||
ListDraw {
|
||||
instances: list.instance.len() as u32,
|
||||
bindings: &list.bindings,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, draws) in ui_render.layers.iter_mut() {
|
||||
self.active.push(i);
|
||||
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.kind == change.kind && h.inst_idx == change.old {
|
||||
h.inst_idx = change.new;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
||||
if draws.updated {
|
||||
let lists = draws.primitives();
|
||||
// The zip would otherwise skip a list with no pipeline.
|
||||
assert!(lists.len() <= self.primitives.len());
|
||||
rlayer.primitives.resize_with(lists.len(), || None);
|
||||
for ((buffers, list), primitive) in rlayer
|
||||
.primitives
|
||||
.iter_mut()
|
||||
.zip(lists)
|
||||
.zip(&self.primitives)
|
||||
{
|
||||
let Some(list) = list else {
|
||||
continue;
|
||||
};
|
||||
buffers
|
||||
.get_or_insert_with(|| ListBuffers::new(device))
|
||||
.update(device, queue, primitive, list);
|
||||
}
|
||||
draws.updated = false;
|
||||
}
|
||||
}
|
||||
for primitive in &mut self.primitives {
|
||||
primitive.render.update(ui);
|
||||
}
|
||||
if ui.masks.changed {
|
||||
ui.masks.changed = false;
|
||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||
self.shared_group = Self::shared_group(
|
||||
device,
|
||||
&self.shared_layout,
|
||||
&self.window_buffer,
|
||||
&self.masks,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
let size = size.into();
|
||||
let slice = &[WindowUniform {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||
let window_uniform = WindowUniform {
|
||||
width: config.width as f32,
|
||||
height: config.height as f32,
|
||||
};
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("window"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let shared_layout = Self::shared_layout(device);
|
||||
let masks = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||
|
||||
Self {
|
||||
shared_layout,
|
||||
shared_group,
|
||||
format: config.format,
|
||||
primitives: Vec::new(),
|
||||
window_buffer,
|
||||
layers: HashMap::default(),
|
||||
active: Vec::new(),
|
||||
masks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compiles a pipeline for every primitive registered since the last call.
|
||||
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
|
||||
for source in ®istry.sources()[self.primitives.len()..] {
|
||||
let render = (source.render)(device, queue);
|
||||
let data_layout = Self::data_layout(device, source.stride);
|
||||
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
||||
groups.extend(render.layout().map(Some));
|
||||
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some(source.label),
|
||||
bind_group_layouts: &groups,
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||
self.primitives.push(PrimitivePipeline {
|
||||
data_layout,
|
||||
pipeline,
|
||||
render,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn pipeline(
|
||||
device: &Device,
|
||||
layout: &PipelineLayout,
|
||||
format: TextureFormat,
|
||||
wgsl: &str,
|
||||
label: &str,
|
||||
) -> RenderPipeline {
|
||||
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some(label),
|
||||
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
|
||||
});
|
||||
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(layout),
|
||||
vertex: VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(PrimitiveInstance::desc())],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Cw,
|
||||
cull_mode: Some(Face::Back),
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
}
|
||||
|
||||
fn shared_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window: &Buffer,
|
||||
masks: &ArrBuf<Mask>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui shared"),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderLayer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: &[],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
use std::{any::TypeId, marker::PhantomData};
|
||||
|
||||
use crate::{
|
||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||
render::{
|
||||
data::{MaskIdx, PrimitiveInstance},
|
||||
page::GlyphRender,
|
||||
texture::ImageRender,
|
||||
},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
use wgpu::{BindGroupLayout, Device, Queue, RenderPass};
|
||||
|
||||
/// 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>,
|
||||
free: Vec<usize>,
|
||||
/// `stride` bytes of the primitive's own data per instance.
|
||||
data: Vec<u8>,
|
||||
/// From the type the list was made for, so a write is never checked.
|
||||
stride: usize,
|
||||
}
|
||||
|
||||
impl InstanceList {
|
||||
fn new<P: Primitive>() -> Self {
|
||||
Self {
|
||||
instances: Vec::new(),
|
||||
assoc: Vec::new(),
|
||||
free: Vec::new(),
|
||||
data: Vec::new(),
|
||||
stride: size_of::<P>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> &[PrimitiveInstance] {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn stride(&self) -> usize {
|
||||
self.stride
|
||||
}
|
||||
|
||||
fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize {
|
||||
if let Some(i) = self.free.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.assoc[i] = id;
|
||||
self.data[i * self.stride..][..self.stride].copy_from_slice(data);
|
||||
i
|
||||
} else {
|
||||
let i = self.instances.len();
|
||||
self.instances.push(inst);
|
||||
self.assoc.push(id);
|
||||
self.data.extend_from_slice(data);
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
fn free(&mut self, i: usize) -> MaskIdx {
|
||||
self.free.push(i);
|
||||
self.instances[i].mask_idx
|
||||
}
|
||||
|
||||
fn apply_free(&mut self, kind: u32) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.free.sort_by(|a, b| b.cmp(a));
|
||||
let instances = &mut self.instances;
|
||||
let assoc = &mut self.assoc;
|
||||
let data = &mut self.data;
|
||||
let stride = self.stride;
|
||||
self.free.drain(..).filter_map(move |i| {
|
||||
instances.swap_remove(i);
|
||||
assoc.swap_remove(i);
|
||||
let last = instances.len();
|
||||
data.copy_within(last * stride..(last + 1) * stride, i * stride);
|
||||
data.truncate(last * stride);
|
||||
if i == last {
|
||||
return None;
|
||||
}
|
||||
let id = assoc[i];
|
||||
Some(PrimitiveChange {
|
||||
id,
|
||||
kind,
|
||||
old: last,
|
||||
new: i,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything one layer draws, one list per registered primitive.
|
||||
pub struct LayerDraws {
|
||||
/// `None` until this layer draws that primitive, because only the write
|
||||
/// knows the type the list is for.
|
||||
primitives: Vec<Option<InstanceList>>,
|
||||
pub updated: bool,
|
||||
}
|
||||
|
||||
impl Default for LayerDraws {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
primitives: Vec::new(),
|
||||
updated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayerDraws {
|
||||
pub fn write<P: Primitive>(
|
||||
&mut self,
|
||||
layer: usize,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) -> PrimitiveHandle {
|
||||
self.updated = true;
|
||||
// Grown on first use rather than sized from the registry, which a
|
||||
// layer cannot see.
|
||||
if self.primitives.len() <= kind.id as usize {
|
||||
self.primitives.resize_with(kind.id as usize + 1, || None);
|
||||
}
|
||||
let inst_idx = self.primitives[kind.id as usize]
|
||||
.get_or_insert_with(InstanceList::new::<P>)
|
||||
.push(
|
||||
id,
|
||||
PrimitiveInstance { region, mask_idx },
|
||||
bytemuck::bytes_of(&primitive),
|
||||
);
|
||||
PrimitiveHandle {
|
||||
layer,
|
||||
kind: kind.id,
|
||||
inst_idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primitives(&self) -> &[Option<InstanceList>] {
|
||||
&self.primitives
|
||||
}
|
||||
|
||||
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
|
||||
self.primitives
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?)))
|
||||
.flat_map(|(kind, list)| list.apply_free(kind))
|
||||
}
|
||||
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
self.updated = true;
|
||||
self.list(h).free(h.inst_idx)
|
||||
}
|
||||
|
||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||
self.updated = true;
|
||||
&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,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PrimitiveHandle {
|
||||
pub layer: usize,
|
||||
pub kind: u32,
|
||||
pub inst_idx: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct RectPrimitive {
|
||||
pub color: Color<u8>,
|
||||
pub radius: f32,
|
||||
pub thickness: f32,
|
||||
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 {
|
||||
color,
|
||||
radius: 0.0,
|
||||
thickness: 0.0,
|
||||
inner_radius: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `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 slot: u32,
|
||||
}
|
||||
|
||||
impl Primitive for TexturePrimitive {
|
||||
const WGSL: &'static str = include_str!("shader/texture.wgsl");
|
||||
|
||||
fn render(device: &Device, queue: &Queue) -> Box<dyn PrimitiveRender> {
|
||||
Box::new(ImageRender::new(device, queue))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TextureHandle> for TexturePrimitive {
|
||||
fn from(handle: &TextureHandle) -> Self {
|
||||
Self {
|
||||
slot: handle.slot(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
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,
|
||||
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 new(device: &Device, queue: &Queue) -> Self {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
slots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||
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: Some("image"),
|
||||
size: Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
rgba.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
|
||||
ImageGpu { texture, group }
|
||||
}
|
||||
|
||||
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 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),
|
||||
},
|
||||
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 {
|
||||
device.create_sampler(&SamplerDescriptor::default())
|
||||
}
|
||||
@@ -21,18 +21,26 @@ 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) {
|
||||
size = size.max(1);
|
||||
// A binding cannot be empty or under the layout's minimum.
|
||||
size = size.max(std::mem::size_of::<T>() as u64);
|
||||
}
|
||||
device.create_buffer(&BufferDescriptor {
|
||||
label: Some(label),
|
||||
@@ -41,8 +49,4 @@ impl<T: Pod> ArrBuf<T> {
|
||||
usage,
|
||||
})
|
||||
}
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
|
||||
|
||||
/// important non rendering data for retained drawing
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
pub region: UiRegion,
|
||||
/// What the widget said it used of `region`, the last time it drew.
|
||||
pub size: Size,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
pub children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
/// Whether it read the output's size, and so is wrong when that changes.
|
||||
pub reads_output: bool,
|
||||
pub mask: MaskIdx,
|
||||
pub layer: LayerId,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::{
|
||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||
};
|
||||
|
||||
mod active;
|
||||
mod painter;
|
||||
mod render_state;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::{Painter, PrimitiveLike};
|
||||
pub use render_state::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
/// Every primitive this ui can draw.
|
||||
pub primitives: PrimitiveRegistry,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
}
|
||||
|
||||
pub trait UiRsc {
|
||||
fn ui(&self) -> &UiData;
|
||||
fn ui_mut(&mut self) -> &mut UiData;
|
||||
|
||||
#[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 widgets(&self) -> &Widgets {
|
||||
&self.ui().widgets
|
||||
}
|
||||
fn widgets_mut(&mut self) -> &mut Widgets {
|
||||
&mut self.ui_mut().widgets
|
||||
}
|
||||
fn free(&mut self) {
|
||||
while let Some(id) = self.widgets_mut().free_next() {
|
||||
self.on_remove(id);
|
||||
}
|
||||
self.ui_mut().textures.free();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
||||
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
},
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
/// makes your surfaces look pretty
|
||||
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,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
pub(super) reads_output: bool,
|
||||
pub layer: usize,
|
||||
pub(super) id: WidgetId,
|
||||
}
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
||||
self.write(kind, primitive, region);
|
||||
}
|
||||
|
||||
/// Takes the kind, for a caller writing many of one primitive.
|
||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||
let h = self.state.layers.write(
|
||||
self.layer,
|
||||
PrimitiveInst {
|
||||
kind,
|
||||
id: self.id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
},
|
||||
);
|
||||
self.push_primitive(h);
|
||||
}
|
||||
|
||||
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
self.primitives.push(h);
|
||||
}
|
||||
|
||||
/// Writes a primitive to be rendered
|
||||
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
||||
let primitive = primitive.into_primitive(self);
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
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.rsc.ui_mut().masks.push(Mask { region });
|
||||
}
|
||||
|
||||
/// Draws a widget within this widget's region.
|
||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||
self.widget_at(id, self.region)
|
||||
}
|
||||
|
||||
/// Draws a widget somewhere within this one. Drawing one a second time
|
||||
/// gives it a new box, keeping the drawing it already has where it can.
|
||||
pub fn widget_within<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let region = region.within(&self.region);
|
||||
self.widget_at(id, region)
|
||||
}
|
||||
|
||||
fn widget_at<'s, W: ?Sized>(
|
||||
&'s mut self,
|
||||
id: &'s StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
// A child listed twice would be moved twice.
|
||||
if !self.children.contains(&id.id()) {
|
||||
self.children.push(id.id());
|
||||
}
|
||||
let size = self.state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
self.mask,
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
DrawResult {
|
||||
child: id,
|
||||
painter: self,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a child says its length is without being drawn, if it can say.
|
||||
/// Asking counts as reading its size.
|
||||
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
||||
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
|
||||
self.depend_on_size(id);
|
||||
Some(hint)
|
||||
}
|
||||
|
||||
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
if !self.size_deps.contains(&child.id()) {
|
||||
self.size_deps.push(child.id());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/// The output's size in pixels. A widget that reads it draws again when
|
||||
/// the output changes, since nothing else can put that right.
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.reads_output = true;
|
||||
self.state.output_size
|
||||
}
|
||||
|
||||
/// This widget's box in pixels. Resolved against the output's size, so a
|
||||
/// widget that reads it draws again when the output changes.
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.reads_output = true;
|
||||
self.region.size().to_abs(self.state.output_size)
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
self.layer = self.state.layers.child(self.layer);
|
||||
}
|
||||
|
||||
pub fn next_layer(&mut self) {
|
||||
self.layer = self.state.layers.next(self.layer);
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &str {
|
||||
&self.rsc.widgets().data(self.id).unwrap().label
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// A child that has just been drawn. Reading its size records that this
|
||||
/// widget's own size depends on it; dropping it without reading draws the
|
||||
/// child and leaves the parent independent of what it came to.
|
||||
pub struct DrawResult<'p, 'a, W: ?Sized> {
|
||||
painter: &'p mut Painter<'a>,
|
||||
child: &'p StrongWidget<W>,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||
pub fn size(self) -> Size {
|
||||
self.painter.depend_on_size(self.child);
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn len(self, axis: Axis) -> Len {
|
||||
self.size().axis(axis)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, OnResize, Painter, PixelRegion, Remap, Size,
|
||||
StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||
};
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: DrawLayers,
|
||||
pub(super) output_size: Vec2,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
resized: bool,
|
||||
draw_started: HashSet<WidgetId>,
|
||||
}
|
||||
|
||||
impl UiRenderState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
layers: 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 output_size(&self) -> Vec2 {
|
||||
self.output_size
|
||||
}
|
||||
|
||||
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.root_changed(root) {
|
||||
self.redraw_all(root, rsc);
|
||||
self.old_root = root.map(|r| r.id());
|
||||
} else if self.resized {
|
||||
// A region is a fraction of the output plus an offset, resolved
|
||||
// against the window in the shader, so a resize moves the whole
|
||||
// drawing on its own. Only a widget that read pixels can be wrong.
|
||||
for (&id, active) in &self.active {
|
||||
if active.reads_output {
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resized = false;
|
||||
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,
|
||||
) -> Size {
|
||||
let mut old_children = old_children.unwrap_or_default();
|
||||
if self.active.contains_key(&id) {
|
||||
if let Some(size) = self.try_reuse(id, region, rsc) {
|
||||
return size;
|
||||
}
|
||||
// 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
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
self.draw_started.insert(id);
|
||||
|
||||
let mut painter = Painter {
|
||||
state: self,
|
||||
region,
|
||||
mask,
|
||||
layer,
|
||||
id,
|
||||
textures: Vec::new(),
|
||||
primitives: Vec::new(),
|
||||
children: Vec::new(),
|
||||
size_deps: Vec::new(),
|
||||
reads_output: false,
|
||||
rsc,
|
||||
};
|
||||
|
||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||
let size = widget.draw(&mut painter);
|
||||
drop(widget);
|
||||
|
||||
let Painter {
|
||||
state: _,
|
||||
rsc: _,
|
||||
region,
|
||||
mask,
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
reads_output,
|
||||
layer,
|
||||
id,
|
||||
} = painter;
|
||||
|
||||
debug_assert!(
|
||||
Self::hints_agree(id, size, rsc),
|
||||
"'{}' ({id:?}) drew a size its size_hint disagrees with",
|
||||
rsc.widgets().label(id)
|
||||
);
|
||||
|
||||
// add to active
|
||||
let active = ActiveData {
|
||||
id,
|
||||
region,
|
||||
size,
|
||||
parent,
|
||||
textures,
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
reads_output,
|
||||
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);
|
||||
size
|
||||
}
|
||||
|
||||
/// The drawing a widget already has, kept for a new box if the box has not
|
||||
/// changed in a way it depends on.
|
||||
fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
let (size, old) = (active.size, active.region);
|
||||
if old == region {
|
||||
return Some(size);
|
||||
}
|
||||
// TODO: epsilon?
|
||||
if old.size() != region.size() && !self.reusable(id, region, rsc) {
|
||||
return None;
|
||||
}
|
||||
// Its drawing stands, if the new box can be reached from the old one.
|
||||
self.mov(id, &Remap::new(old, region)?);
|
||||
Some(size)
|
||||
}
|
||||
|
||||
/// Whether the widget can keep the drawing it has and be given `region`
|
||||
/// instead, asked one axis at a time: a change on an axis it does not
|
||||
/// depend on costs nothing, whatever it depends on elsewhere.
|
||||
fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
|
||||
let Some(active) = self.active.get(&id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
||||
return false;
|
||||
};
|
||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
||||
let offered = region.axis(axis).len();
|
||||
let had = active.region.axis(axis).len();
|
||||
match widget.on_resize(axis) {
|
||||
OnResize::Scale => true,
|
||||
// `Translate` is not acted on yet, and cannot be until a
|
||||
// drawing can sit somewhere other than its box. `region` is
|
||||
// both the box a widget was given and the box its primitives
|
||||
// are in, and `mov` remaps from it -- so carrying a drawing at
|
||||
// its old size while the box grows makes the next move stretch
|
||||
// it. The offset chain is what separates the two.
|
||||
OnResize::Translate | OnResize::Redraw => offered == had,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
||||
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
||||
return true;
|
||||
};
|
||||
[Axis::X, Axis::Y].into_iter().all(|axis| {
|
||||
widget
|
||||
.size_hint(axis)
|
||||
.is_none_or(|hint| hint == size.axis(axis))
|
||||
})
|
||||
}
|
||||
|
||||
fn mov(&mut self, id: WidgetId, remap: &Remap) {
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
for h in &active.primitives {
|
||||
let region = self.layers[h.layer].region_mut(h);
|
||||
*region = remap.apply(*region);
|
||||
}
|
||||
active.region = remap.apply(active.region);
|
||||
// SAFETY: children cannot be recursive
|
||||
let children = unsafe { forget_ref(&active.children) };
|
||||
for child in children {
|
||||
self.mov(*child, remap);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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.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
|
||||
}
|
||||
|
||||
pub fn needs_redraw<'a>(
|
||||
&self,
|
||||
root: impl Into<Option<&'a StrongWidget>>,
|
||||
widgets: &Widgets,
|
||||
) -> bool {
|
||||
self.root_changed(root) || self.resized || 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) {
|
||||
self.draw_started.remove(&id);
|
||||
// Whoever read this widget's size may be a different size now, so the
|
||||
// highest reader is what draws. Everything between the two is marked
|
||||
// as well: their own boxes have not changed, so the mark is the only
|
||||
// thing stopping the draw reusing its way past this widget.
|
||||
if let Some(top) = self.mark_readers(id, rsc) {
|
||||
self.redraw(top, rsc);
|
||||
// Cleared by that draw if it reached here; if it did not, this is
|
||||
// no longer drawn and asking again would not end.
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
return;
|
||||
}
|
||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
/// The furthest ancestor that read this widget's size, directly or through
|
||||
/// widgets that did the same, marking everything below it on the way.
|
||||
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
|
||||
let mut top = None;
|
||||
let mut at = id;
|
||||
while let Some(active) = self.active.get(&at)
|
||||
&& let Some(parent) = active.parent
|
||||
&& self
|
||||
.active
|
||||
.get(&parent)
|
||||
.is_some_and(|p| p.size_deps.contains(&at))
|
||||
{
|
||||
rsc.widgets_mut().needs_redraw.insert(at);
|
||||
top = Some(parent);
|
||||
at = parent;
|
||||
}
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiRenderState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::util::{Id, IdNum, IdTracker};
|
||||
|
||||
pub struct Arena<T, I> {
|
||||
data: Vec<T>,
|
||||
tracker: IdTracker<I>,
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Arena<T, I> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Vec::new(),
|
||||
tracker: IdTracker::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Id<I> {
|
||||
let id = self.tracker.next();
|
||||
let i = id.idx();
|
||||
if i == self.data.len() {
|
||||
self.data.push(value);
|
||||
} else {
|
||||
self.data[i] = value;
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Id<I>) -> T
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
let i = id.idx();
|
||||
self.tracker.free(id);
|
||||
self.data[i]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Default for Arena<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TrackedArena<T, I> {
|
||||
inner: Arena<T, I>,
|
||||
refs: Vec<u32>,
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> TrackedArena<T, I> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arena::default(),
|
||||
refs: Vec::new(),
|
||||
changed: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Id<I> {
|
||||
self.changed = true;
|
||||
let id = self.inner.push(value);
|
||||
let i = id.idx();
|
||||
if i == self.refs.len() {
|
||||
self.refs.push(0);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_ref(&mut self, i: Id<I>) {
|
||||
self.refs[i.idx()] += 1;
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Id<I>) -> T
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
let i = id.idx();
|
||||
self.refs[i] -= 1;
|
||||
if self.refs[i] == 0 {
|
||||
self.changed = true;
|
||||
self.inner.remove(id)
|
||||
} else {
|
||||
self[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Default for TrackedArena<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I> Deref for TrackedArena<T, I> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I> Deref for Arena<T, I> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct DynBorrower<'a, T: ?Sized> {
|
||||
data: &'a mut T,
|
||||
borrowed: &'a mut bool,
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> DynBorrower<'a, T> {
|
||||
pub fn new(data: &'a mut T, borrowed: &'a mut bool) -> Self {
|
||||
if *borrowed {
|
||||
panic!("tried to mutably borrow the same thing twice");
|
||||
}
|
||||
Self { data, borrowed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Drop for DynBorrower<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
*self.borrowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for DynBorrower<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for DynBorrower<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct MutDetect<T> {
|
||||
inner: T,
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
impl<T> Deref for MutDetect<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for MutDetect<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.changed = true;
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MutDetect<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
MutDetect {
|
||||
inner,
|
||||
changed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#[repr(C)]
|
||||
#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy, bytemuck::Zeroable)]
|
||||
pub struct Id<I = u64>(I);
|
||||
|
||||
unsafe impl<I: Copy + bytemuck::Zeroable + 'static> bytemuck::Pod for Id<I> {}
|
||||
|
||||
pub struct IdTracker<I = u64> {
|
||||
free: Vec<Id<I>>,
|
||||
cur: Id<I>,
|
||||
}
|
||||
|
||||
impl<I: IdNum> IdTracker<I> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Id<I> {
|
||||
if let Some(id) = self.free.pop() {
|
||||
return id;
|
||||
}
|
||||
let next = self.cur.next();
|
||||
std::mem::replace(&mut self.cur, next)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn free(&mut self, id: Id<I>) {
|
||||
self.free.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: IdNum> Id<I> {
|
||||
#[allow(dead_code)]
|
||||
/// for debug purposes; should this be exposed?
|
||||
/// generally you want to use labels with widgets
|
||||
pub(crate) fn raw(id: I) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
pub fn idx(&self) -> usize {
|
||||
self.0.idx()
|
||||
}
|
||||
pub fn next(&self) -> Id<I> {
|
||||
Self(self.0.next())
|
||||
}
|
||||
pub const fn preset(value: I) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: IdNum> Default for IdTracker<I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
free: Vec::new(),
|
||||
cur: Id(I::first()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IdNum {
|
||||
fn first() -> Self;
|
||||
fn next(&self) -> Self;
|
||||
fn idx(&self) -> usize;
|
||||
}
|
||||
|
||||
impl IdNum for u64 {
|
||||
fn first() -> Self {
|
||||
0
|
||||
}
|
||||
|
||||
fn next(&self) -> Self {
|
||||
self + 1
|
||||
}
|
||||
|
||||
fn idx(&self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl IdNum for u32 {
|
||||
fn first() -> Self {
|
||||
0
|
||||
}
|
||||
|
||||
fn next(&self) -> Self {
|
||||
self + 1
|
||||
}
|
||||
|
||||
fn idx(&self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pub const trait LerpUtil: Sized {
|
||||
fn lerp(self, from: Self, to: Self) -> Self;
|
||||
fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
|
||||
}
|
||||
|
||||
const impl LerpUtil for f32 {
|
||||
/// linear interpolation
|
||||
/// from * (1.0 - self) + to * self
|
||||
fn lerp(self, from: Self, to: Self) -> Self {
|
||||
from + (to - from) * self
|
||||
}
|
||||
/// inverse of lerp, and `None` where `from` and `to` are the same point:
|
||||
/// every input lerps to it, so there is no one answer to come back to.
|
||||
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
|
||||
match to == from {
|
||||
true => None,
|
||||
false => Some((self - from) / (to - from)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_op {
|
||||
($T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
|
||||
#[allow(non_snake_case)]
|
||||
mod ${concat($T, _op_, $fn, _impl)} {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use std::ops::*;
|
||||
const impl $op for $T {
|
||||
type Output = Self;
|
||||
|
||||
fn $fn(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
$($field: self.$field.$fn(rhs.$field),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
const impl $opa for $T {
|
||||
fn $fna(&mut self, rhs: Self) {
|
||||
*self = self.$fn(rhs);
|
||||
}
|
||||
}
|
||||
const impl $op<f32> for $T {
|
||||
type Output = Self;
|
||||
|
||||
fn $fn(self, rhs: f32) -> Self::Output {
|
||||
Self {
|
||||
$($field: self.$field.$fn(rhs),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
const impl $op<$T> for f32 {
|
||||
type Output = $T;
|
||||
|
||||
fn $fn(self, rhs: $T) -> Self::Output {
|
||||
$T {
|
||||
$($field: self.$fn(rhs.$field),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
const impl $opa<f32> for $T {
|
||||
fn $fna(&mut self, rhs: f32) {
|
||||
*self = self.$fn(rhs);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||
};
|
||||
(impl $op:ident for $T:ident: $fn:ident $($field:ident)*) => {
|
||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_op;
|
||||
@@ -0,0 +1,24 @@
|
||||
mod arena;
|
||||
mod borrow;
|
||||
mod change;
|
||||
mod id;
|
||||
mod math;
|
||||
mod refcount;
|
||||
mod slot;
|
||||
mod trust;
|
||||
mod typemap;
|
||||
mod vec2;
|
||||
|
||||
pub use arena::*;
|
||||
pub use borrow::*;
|
||||
pub use change::*;
|
||||
pub use id::*;
|
||||
pub use math::*;
|
||||
pub use refcount::*;
|
||||
pub use slot::*;
|
||||
pub(crate) use trust::*;
|
||||
pub use typemap::*;
|
||||
pub use vec2::*;
|
||||
|
||||
pub type HashMap<K, V> = fxhash::FxHashMap<K, V>;
|
||||
pub type HashSet<K> = fxhash::FxHashSet<K>;
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RefCounter(Arc<AtomicU32>);
|
||||
|
||||
impl RefCounter {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(0.into()))
|
||||
}
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.0.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn drop(&mut self) -> bool {
|
||||
let refs = self.0.fetch_sub(1, Ordering::Release);
|
||||
refs == 0
|
||||
}
|
||||
pub fn quiet_clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RefCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RefCounter {
|
||||
fn clone(&self) -> Self {
|
||||
self.0.fetch_add(1, Ordering::Release);
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SlotId {
|
||||
idx: u32,
|
||||
genr: u32,
|
||||
}
|
||||
|
||||
pub struct SlotVec<T> {
|
||||
data: Vec<(u32, Option<T>)>,
|
||||
free: Vec<u32>,
|
||||
}
|
||||
|
||||
impl<T> SlotVec<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Default::default(),
|
||||
free: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, x: T) -> SlotId {
|
||||
if let Some(idx) = self.free.pop() {
|
||||
let (genr, data) = &mut self.data[idx as usize];
|
||||
*data = Some(x);
|
||||
SlotId { idx, genr: *genr }
|
||||
} else {
|
||||
let idx = self.data.len() as u32;
|
||||
let genr = 0;
|
||||
self.data.push((genr, Some(x)));
|
||||
SlotId { idx, genr }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free(&mut self, id: SlotId) {
|
||||
let (genr, data) = &mut self.data[id.idx as usize];
|
||||
*genr += 1;
|
||||
*data = None;
|
||||
self.free.push(id.idx);
|
||||
}
|
||||
|
||||
pub fn get(&self, id: SlotId) -> Option<&T> {
|
||||
let slot = &self.data[id.idx as usize];
|
||||
if slot.0 != id.genr {
|
||||
return None;
|
||||
}
|
||||
slot.1.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: SlotId) -> Option<&mut T> {
|
||||
let slot = &mut self.data[id.idx as usize];
|
||||
if slot.0 != id.genr {
|
||||
return None;
|
||||
}
|
||||
slot.1.as_mut()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len() - self.free.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for SlotVec<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
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(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(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
std::mem::transmute::<&T, &mut T>(x)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::util::HashMap;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
marker::Unsize,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
pub struct TypeMap<Trait: ?Sized> {
|
||||
map: HashMap<TypeId, Box<Trait>>,
|
||||
}
|
||||
|
||||
impl<Trait: ?Sized> TypeMap<Trait> {
|
||||
pub fn set_type<T: Unsize<Trait> + 'static>(&mut self, val: T) {
|
||||
self.map
|
||||
.insert(TypeId::of::<T>(), Box::new(val) as Box<Trait>);
|
||||
}
|
||||
|
||||
pub fn type_mut<T: Unsize<Trait> + 'static>(&mut self) -> Option<&mut T> {
|
||||
Some(Self::convert_mut(self.map.get_mut(&TypeId::of::<T>())?))
|
||||
}
|
||||
|
||||
pub fn type_or_default<T: Default + Unsize<Trait> + 'static>(&mut self) -> &mut T {
|
||||
Self::convert_mut(
|
||||
self.map
|
||||
.entry(TypeId::of::<T>())
|
||||
.or_insert(Box::new(T::default()) as Box<Trait>),
|
||||
)
|
||||
}
|
||||
|
||||
fn convert_mut<T: Unsize<Trait>>(entry: &mut Box<Trait>) -> &mut T {
|
||||
// allegedly this is just what Any does...
|
||||
unsafe { &mut *(entry.as_mut() as *mut Trait as *mut T) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for TypeMap<T> {
|
||||
type Target = HashMap<TypeId, Box<T>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for TypeMap<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for TypeMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use crate::util::impl_op;
|
||||
use std::{hash::Hash, ops::*};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vec2 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Eq for Vec2 {}
|
||||
|
||||
impl Hash for Vec2 {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write_u32(self.x.to_bits());
|
||||
state.write_u32(self.y.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub const ZERO: Self = Self::new(0.0, 0.0);
|
||||
pub const ONE: Self = Self::new(1.0, 1.0);
|
||||
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn round(self) -> Self {
|
||||
Self {
|
||||
x: self.x.round(),
|
||||
y: self.y.round(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn floor(self) -> Self {
|
||||
Self {
|
||||
x: self.x.floor(),
|
||||
y: self.y.floor(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn ceil(self) -> Self {
|
||||
Self {
|
||||
x: self.x.ceil(),
|
||||
y: self.y.ceil(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn tuple(&self) -> (f32, f32) {
|
||||
(self.x, self.y)
|
||||
}
|
||||
|
||||
pub const fn with_x(mut self, x: f32) -> Self {
|
||||
self.x = x;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn with_y(mut self, y: f32) -> Self {
|
||||
self.y = y;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// this version looks kinda cool... is it more readable? more annoying to copy and change though
|
||||
impl_op!(impl Add for Vec2: add x y);
|
||||
impl_op!(Vec2 Sub sub; x y);
|
||||
impl_op!(Vec2 Mul mul; x y);
|
||||
impl_op!(Vec2 Div div; x y);
|
||||
|
||||
impl Neg for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(mut self) -> Self::Output {
|
||||
self.x = -self.x;
|
||||
self.y = -self.y;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Vec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Vec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::Widget;
|
||||
|
||||
pub struct WidgetData {
|
||||
pub widget: Box<dyn Widget>,
|
||||
pub label: String,
|
||||
/// dynamic borrow checking
|
||||
pub borrowed: bool,
|
||||
}
|
||||
|
||||
impl WidgetData {
|
||||
pub fn new<W: Widget>(widget: W) -> Self {
|
||||
let mut label = std::any::type_name::<W>().to_string();
|
||||
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) {
|
||||
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1;
|
||||
}
|
||||
Self {
|
||||
widget: Box::new(widget),
|
||||
label,
|
||||
borrowed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
||||
|
||||
use crate::{
|
||||
UiRsc, Widget,
|
||||
util::{RefCounter, SlotId},
|
||||
};
|
||||
|
||||
pub type WidgetId = SlotId;
|
||||
|
||||
/// An identifier for a widget that can index a UI or event ctx to get it.
|
||||
/// This is a strong handle that does not impl Clone, and when it is dropped,
|
||||
/// 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 StrongWidget<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
counter: RefCounter,
|
||||
send: Sender<WidgetId>,
|
||||
ty: *const W,
|
||||
}
|
||||
|
||||
/// A weak handle to a widget.
|
||||
/// Will not keep it alive, but can still be used for indexing like WidgetHandle.
|
||||
pub struct WeakWidget<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
#[allow(unused)]
|
||||
ty: *const W,
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> {
|
||||
pub fn any(self) -> StrongWidget<dyn Widget> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> StrongWidget<W> {
|
||||
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
counter: RefCounter::new(),
|
||||
send,
|
||||
ty: null_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.counter.refs()
|
||||
}
|
||||
|
||||
pub fn weak(&self) -> WeakWidget<W> {
|
||||
let Self { ty, id, .. } = *self;
|
||||
WeakWidget { ty, id }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> WeakWidget<W> {
|
||||
pub(crate) fn new(id: WidgetId) -> Self {
|
||||
Self { id, ty: null_ptr() }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> {
|
||||
ui.widgets_mut().upgrade(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> Drop for StrongWidget<W> {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 &StrongWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for StrongWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for WeakWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl IdLike for WidgetId {
|
||||
type Widget = dyn Widget;
|
||||
fn id(&self) -> WidgetId {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
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 WeakWidget<W> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
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 StrongWidget<W> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
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: 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.widgets_mut()[self]
|
||||
}
|
||||
}
|
||||
|
||||
fn null_ptr<W: ?Sized>() -> *const W {
|
||||
if size_of::<&W>() == size_of::<*const dyn Widget>() {
|
||||
let w: *const dyn Widget = &();
|
||||
unsafe { std::mem::transmute_copy(&w) }
|
||||
} else {
|
||||
unsafe { std::mem::transmute_copy(&[0usize; 1]) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<W: ?Sized> Send for WeakWidget<W> {}
|
||||
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::UiRsc;
|
||||
|
||||
use super::*;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
|
||||
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
|
||||
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<Self::Widget>;
|
||||
|
||||
fn add_strong(self, rsc: &mut Rsc) -> StrongWidget<Self::Widget> {
|
||||
self.add(rsc).upgrade(rsc)
|
||||
}
|
||||
|
||||
fn with_id<W2>(
|
||||
self,
|
||||
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, rsc: &mut Rsc, root: &mut impl HasRoot) {
|
||||
let id = self.add_strong(rsc);
|
||||
root.set_root(id);
|
||||
}
|
||||
}
|
||||
|
||||
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 Rsc) -> WidgetArr<LEN>;
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
|
||||
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// variadic generics please save us
|
||||
macro_rules! impl_widget_arr {
|
||||
($n:expr;$($W:ident)*) => {
|
||||
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
|
||||
};
|
||||
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
|
||||
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(rsc).upgrade(rsc),)*],
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_widget_arr!(1;A);
|
||||
impl_widget_arr!(2;A B);
|
||||
impl_widget_arr!(3;A B C);
|
||||
impl_widget_arr!(4;A B C D);
|
||||
impl_widget_arr!(5;A B C D E);
|
||||
impl_widget_arr!(6;A B C D E F);
|
||||
impl_widget_arr!(7;A B C D E F G);
|
||||
impl_widget_arr!(8;A B C D E F G H);
|
||||
impl_widget_arr!(9;A B C D E F G H I);
|
||||
impl_widget_arr!(10;A B C D E F G H I J);
|
||||
impl_widget_arr!(11;A B C D E F G H I J K);
|
||||
impl_widget_arr!(12;A B C D E F G H I J K L);
|
||||
@@ -0,0 +1,100 @@
|
||||
use crate::{Axis, Len, Painter, Size};
|
||||
use std::any::Any;
|
||||
|
||||
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::*;
|
||||
|
||||
/// What may be done to a widget's drawing when the box it was given changes
|
||||
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
|
||||
/// text reads the width it is offered and not the height.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum OnResize {
|
||||
Scale,
|
||||
Translate,
|
||||
#[default]
|
||||
Redraw,
|
||||
}
|
||||
|
||||
pub trait Widget: Any {
|
||||
/// Draws the widget, and returns what it used of the box it was given.
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||
|
||||
/// An exact length the widget can give without a painter or its children.
|
||||
/// Optional, and saves a draw rather than changing one: a hint that
|
||||
/// disagrees with the eventual draw fails a debug assertion.
|
||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
||||
None
|
||||
}
|
||||
|
||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||
OnResize::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for () {
|
||||
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||
Size::default()
|
||||
}
|
||||
|
||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
||||
Some(Len::default())
|
||||
}
|
||||
|
||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||
OnResize::Scale
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Widget {
|
||||
pub fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A function that returns a widget given a UI.
|
||||
/// Useful for defining trait functions on widgets that create a parent widget so that the children
|
||||
/// don't need to be IDs yet
|
||||
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: [StrongWidget; LEN],
|
||||
}
|
||||
|
||||
impl<const LEN: usize> WidgetArr<LEN> {
|
||||
pub fn new(arr: [StrongWidget; LEN]) -> Self {
|
||||
Self { arr }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetOption<State> {
|
||||
fn get(self, state: &mut State) -> Option<StrongWidget>;
|
||||
}
|
||||
|
||||
impl<State> WidgetOption<State> for () {
|
||||
fn get(self, _: &mut State) -> Option<StrongWidget> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F {
|
||||
fn get(self, state: &mut State) -> Option<StrongWidget> {
|
||||
self(state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use super::*;
|
||||
use crate::UiRsc;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub struct WidgetTag;
|
||||
impl<Rsc: UiRsc, W: Widget> WidgetLike<Rsc, WidgetTag> for W {
|
||||
type Widget = W;
|
||||
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<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F {
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
self(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetFnTrait<Rsc> {
|
||||
type Widget: Widget;
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget;
|
||||
}
|
||||
pub struct FnTraitTag;
|
||||
impl<Rsc: UiRsc, T: WidgetFnTrait<Rsc>> WidgetLike<Rsc, FnTraitTag> for T {
|
||||
type Widget = T::Widget;
|
||||
#[track_caller]
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<T::Widget> {
|
||||
self.run(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
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 Rsc) -> WeakWidget<W> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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, 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()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArrTag;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use crate::{
|
||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||
};
|
||||
|
||||
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() -> Self {
|
||||
let (send, recv) = channel();
|
||||
Self {
|
||||
needs_redraw: Default::default(),
|
||||
vec: Default::default(),
|
||||
waiting: Default::default(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_updates(&self) -> bool {
|
||||
!self.needs_redraw.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||
Some(self.vec.get(id)?.widget.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_dyn_mut(&mut self, id: WidgetId) -> Option<&mut dyn Widget> {
|
||||
self.needs_redraw.insert(id);
|
||||
Some(self.vec.get_mut(id)?.widget.as_mut())
|
||||
}
|
||||
|
||||
/// get_dyn but dynamic borrow checking of widgets
|
||||
/// lets you do recursive (tree) operations, like the painter does
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||
// done through the borrow variable
|
||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||
if data.borrowed {
|
||||
panic!("tried to mutably borrow the same widget twice");
|
||||
}
|
||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
}
|
||||
|
||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
self.get_dyn(id.id())?.as_any().downcast_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
|
||||
}
|
||||
|
||||
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> {
|
||||
let id = self.vec.add(WidgetData::new(widget));
|
||||
StrongWidget::new(id, self.send.clone())
|
||||
}
|
||||
|
||||
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> {
|
||||
let id = self.vec.add(WidgetData::new(widget));
|
||||
self.waiting.insert(id);
|
||||
WeakWidget::new(id)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
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"
|
||||
)
|
||||
}
|
||||
StrongWidget::new(rf.id(), self.send.clone())
|
||||
}
|
||||
|
||||
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
|
||||
self.vec.get(id.id())
|
||||
}
|
||||
|
||||
pub fn label(&self, id: impl IdLike) -> &String {
|
||||
&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 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)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.vec.len()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use iris::prelude::*;
|
||||
|
||||
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>) -> Self {
|
||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
@@ -0,0 +1,220 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
use iris::prelude::*;
|
||||
type ClientRsc = DefaultRsc<Client>;
|
||||
|
||||
fn main() {
|
||||
DefaultApp::<Client>::run();
|
||||
}
|
||||
|
||||
#[derive(DefaultUiState)]
|
||||
pub struct Client {
|
||||
ui_state: DefaultUiState,
|
||||
info: WeakWidget<Text>,
|
||||
}
|
||||
|
||||
impl DefaultAppState for Client {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||
let rrect = rect(Color::WHITE).radius(20);
|
||||
let pad_test = (
|
||||
rrect.color(Color::BLUE),
|
||||
(
|
||||
rrect
|
||||
.color(Color::RED)
|
||||
.sized((100, 100))
|
||||
.center()
|
||||
.width(rest(2)),
|
||||
(
|
||||
rrect.color(Color::ORANGE),
|
||||
rrect.color(Color::LIME).pad(10.0),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.width(rest(2)),
|
||||
rrect.color(Color::YELLOW),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.pad(10)
|
||||
.width(rest(3)),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.add(rsc);
|
||||
|
||||
let span_test = (
|
||||
rrect.color(Color::GREEN).width(100),
|
||||
rrect.color(Color::ORANGE),
|
||||
rrect.color(Color::CYAN),
|
||||
rrect.color(Color::BLUE).width(rel(0.5)),
|
||||
rrect.color(Color::MAGENTA).width(100),
|
||||
rrect.color(Color::RED).width(100),
|
||||
)
|
||||
.span(Dir::LEFT)
|
||||
.add(rsc);
|
||||
|
||||
let span_add = Span::empty(Dir::RIGHT).add(rsc);
|
||||
|
||||
let add_button = rect(Color::LIME)
|
||||
.radius(30)
|
||||
.on(CursorSense::click(), move |_, rsc| {
|
||||
let child = image(include_bytes!("assets/sungals.png"))
|
||||
.center()
|
||||
.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 |_, rsc| {
|
||||
span_add(rsc).pop();
|
||||
})
|
||||
.sized((150, 150))
|
||||
.align(Align::BOT_LEFT);
|
||||
|
||||
let span_add_test = (span_add, add_button, del_button).stack().add(rsc);
|
||||
|
||||
let btext = |content| wtext(content).size(30);
|
||||
|
||||
let text_test = (
|
||||
btext("this is a").align(Align::LEFT),
|
||||
btext("teeeeeeeest").align(Align::RIGHT),
|
||||
btext("okkk\nokkkkkk!").align(Align::LEFT),
|
||||
btext("hmm"),
|
||||
btext("a"),
|
||||
(
|
||||
btext("'").family(Family::Monospace).align(Align::TOP),
|
||||
btext("'").family(Family::Monospace),
|
||||
btext(":gamer mode").family(Family::Monospace),
|
||||
rect(Color::CYAN).sized((10, 10)).center(),
|
||||
rect(Color::RED).sized((100, 100)).center(),
|
||||
rect(Color::PURPLE).sized((50, 50)).align(Align::TOP),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.center(),
|
||||
wtext("pretty cool right?").size(50),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.add(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, rsc| {
|
||||
let w = ctx.widget;
|
||||
let content = w.edit(rsc).take();
|
||||
let text = wtext(content)
|
||||
.editable(EditMode::MultiLine)
|
||||
.size(30)
|
||||
.text_align(Align::LEFT)
|
||||
.wrap(true)
|
||||
.attr::<Selectable>(());
|
||||
let msg_box = text
|
||||
.background(rect(Color::WHITE.darker(0.5)))
|
||||
.add_strong(rsc);
|
||||
texts(rsc).push(msg_box);
|
||||
})
|
||||
.add(rsc);
|
||||
|
||||
let text_edit_scroll = (
|
||||
msg_area.height(rest(1)),
|
||||
(
|
||||
Rect::new(Color::WHITE.darker(0.9)),
|
||||
(
|
||||
add_text.width(rest(1)),
|
||||
Rect::new(Color::GREEN)
|
||||
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||
})
|
||||
.sized((40, 40)),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.pad(10),
|
||||
)
|
||||
.stack()
|
||||
.size(StackSize::Child(1))
|
||||
.layer_offset(1)
|
||||
.align(Align::BOT),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.add(rsc);
|
||||
|
||||
let main = WidgetPtr::new().add(rsc);
|
||||
|
||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||
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);
|
||||
main(rsc).set(to);
|
||||
} else {
|
||||
vec.push(Some(to));
|
||||
}
|
||||
let vals = vals.clone();
|
||||
let rect = rect(color)
|
||||
.on(CursorSense::click(), move |ctx, rsc| {
|
||||
let (prev, vec) = &mut *vals.borrow_mut();
|
||||
if let Some(h) = vec[i].take() {
|
||||
vec[*prev] = main(rsc).replace(h);
|
||||
*prev = i;
|
||||
}
|
||||
ctx.widget(rsc).color = color.darker(0.3);
|
||||
})
|
||||
.on(
|
||||
CursorSense::HoverStart | CursorSense::unclick(),
|
||||
move |ctx, rsc| {
|
||||
ctx.widget(rsc).color = color.brighter(0.2);
|
||||
},
|
||||
)
|
||||
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
||||
ctx.widget(rsc).color = color;
|
||||
});
|
||||
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
||||
};
|
||||
|
||||
let tabs = (
|
||||
switch_button(Color::RED, pad_test, "pad"),
|
||||
switch_button(Color::GREEN, span_test, "span"),
|
||||
switch_button(Color::BLUE, span_add_test, "image span"),
|
||||
switch_button(Color::MAGENTA, text_test, "text layout"),
|
||||
switch_button(
|
||||
Color::YELLOW.mul_rgb(0.5),
|
||||
text_edit_scroll,
|
||||
"text edit scroll",
|
||||
),
|
||||
)
|
||||
.span(Dir::RIGHT);
|
||||
|
||||
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(rsc, &mut ui_state);
|
||||
|
||||
Self { ui_state, info }
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_: WindowEvent,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
let new = format!(
|
||||
"widgets: {}\nactive: {}\ntextures: {}",
|
||||
rsc.widgets().len(),
|
||||
render.active_widgets(),
|
||||
rsc.ui().textures.count(),
|
||||
);
|
||||
if new != *rsc.widgets()[self.info].content {
|
||||
*rsc.widgets_mut()[self.info].content = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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>) -> 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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! Text sizing: wrapped text reads the width it is offered, fixed text does
|
||||
//! not, and both report a height their container lays out around.
|
||||
|
||||
use iris::prelude::*;
|
||||
|
||||
fn main() {
|
||||
DefaultApp::<State>::run();
|
||||
}
|
||||
|
||||
#[derive(DefaultUiState)]
|
||||
struct State {
|
||||
ui_state: DefaultUiState,
|
||||
}
|
||||
|
||||
const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \
|
||||
leaves room for, so the height of a paragraph is an answer rather than a setting, and \
|
||||
the same words in a narrower box come back taller. Resize the window and watch the \
|
||||
text below reflow into a different number of lines while nothing about it changes.";
|
||||
|
||||
impl DefaultAppState for State {
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||
let panel = || rect(Color::WHITE.darker(0.85));
|
||||
|
||||
let wrapped = wtext(SAMPLE)
|
||||
.size(28)
|
||||
.wrap(true)
|
||||
.text_align(Align::LEFT)
|
||||
.pad(16)
|
||||
.background(panel());
|
||||
|
||||
let aligned = (
|
||||
wtext("left").size(24).text_align(Align::LEFT),
|
||||
wtext("centred").size(24).text_align(Align::CENTER),
|
||||
wtext("right").size(24).text_align(Align::RIGHT),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(8)
|
||||
.pad(16)
|
||||
.background(panel());
|
||||
|
||||
// The same words in half the width, which is a different number of
|
||||
// lines and so a different height. A declared width only holds along
|
||||
// a span's own axis, hence the row.
|
||||
let narrow = (
|
||||
wtext(SAMPLE)
|
||||
.size(20)
|
||||
.wrap(true)
|
||||
.pad(16)
|
||||
.background(panel())
|
||||
.align(Align::TOP)
|
||||
.width(rel(0.5)),
|
||||
rect(Color::WHITE.darker(0.95)),
|
||||
)
|
||||
.span(Dir::RIGHT);
|
||||
|
||||
(wrapped, aligned, narrow)
|
||||
.span(Dir::DOWN)
|
||||
.gap(12)
|
||||
.pad(12)
|
||||
.set_root(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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>) -> Self {
|
||||
let test = Test::new(rsc);
|
||||
|
||||
test.on(CursorSense::click(), move |_, rsc| {
|
||||
test.toggle(rsc);
|
||||
})
|
||||
.set_root(rsc, &mut ui_state);
|
||||
|
||||
Self { ui_state }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "iris-macro"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.103"
|
||||
quote = "1.0.42"
|
||||
syn = { version = "2.0.111", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -0,0 +1,196 @@
|
||||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
|
||||
Token, Type, Visibility,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input, parse_quote,
|
||||
spanned::Spanned,
|
||||
};
|
||||
|
||||
struct Input {
|
||||
attrs: Vec<Attribute>,
|
||||
vis: Visibility,
|
||||
name: Ident,
|
||||
generics: Generics,
|
||||
fns: Vec<InputFn>,
|
||||
}
|
||||
|
||||
struct InputFn {
|
||||
sig: Signature,
|
||||
body: Block,
|
||||
}
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let vis = input.parse()?;
|
||||
input.parse::<Token![trait]>()?;
|
||||
let name = input.parse()?;
|
||||
let generics = input.parse::<Generics>()?;
|
||||
input.parse::<Token![;]>()?;
|
||||
let mut fns = Vec::new();
|
||||
while !input.is_empty() {
|
||||
let sig = input.parse()?;
|
||||
let body = input.parse()?;
|
||||
fns.push(InputFn { sig, body })
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.error("function expected");
|
||||
}
|
||||
Ok(Input {
|
||||
attrs,
|
||||
vis,
|
||||
name,
|
||||
generics,
|
||||
fns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn widget_trait(input: TokenStream) -> TokenStream {
|
||||
let Input {
|
||||
attrs,
|
||||
vis,
|
||||
name,
|
||||
mut generics,
|
||||
fns,
|
||||
} = parse_macro_input!(input as Input);
|
||||
|
||||
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
|
||||
let impls: Vec<_> = fns
|
||||
.iter()
|
||||
.map(|InputFn { sig, body }| quote! { #sig #body })
|
||||
.collect();
|
||||
|
||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||
return Error::new(name.span(), "expected state generic parameter")
|
||||
.into_compile_error()
|
||||
.into();
|
||||
};
|
||||
|
||||
let state = &state.ident;
|
||||
|
||||
generics
|
||||
.params
|
||||
.push(parse_quote!(WL: WidgetLike<#state, Tag>));
|
||||
generics.params.push(parse_quote!(Tag));
|
||||
|
||||
let mut trai: ItemTrait = parse_quote!(
|
||||
#vis trait #name #generics {
|
||||
#(#sigs;)*
|
||||
}
|
||||
);
|
||||
|
||||
trai.attrs = attrs;
|
||||
|
||||
quote! {
|
||||
#trai
|
||||
|
||||
impl #generics #name<Rsc, WL, Tag> for WL {
|
||||
#(#impls)*
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[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 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;
|
||||
};
|
||||
if found_attr {
|
||||
output.extend(
|
||||
Error::new(
|
||||
attr.span(),
|
||||
"cannot have more than one default_ui_state attribute",
|
||||
)
|
||||
.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 DefaultUiState field found").into_compile_error(),
|
||||
);
|
||||
return output.into();
|
||||
};
|
||||
let sname = &state.ident;
|
||||
let fname = field.ident.as_ref().unwrap();
|
||||
output.extend(quote! {
|
||||
impl iris::default::HasDefaultUiState for #sname {
|
||||
fn default_state(&self) -> &iris::default::DefaultUiState {
|
||||
&self.#fname
|
||||
}
|
||||
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState {
|
||||
&mut self.#fname
|
||||
}
|
||||
}
|
||||
});
|
||||
output.into()
|
||||
}
|
||||
|
||||
#[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 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()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# iris
|
||||
|
||||
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.
|
||||
|
||||
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, try to beat iced and xilem)
|
||||
|
||||
## dev details
|
||||
|
||||
not targeting web rn cause wanna use actual nice gpu features & entire point of this is to make desktop apps / not need a web browser
|
||||
|
||||
general ideas trynna use rn / experiment with:
|
||||
- retained mode
|
||||
- specifically designed around wgpu so there's no translation
|
||||
- postfix functions for most things to prevent unreadable indentation (going very well)
|
||||
- events can be done directly where you draw the widgets
|
||||
- almost no macros in user code & actual LSP typechecking (variadic generics if you can hear me please save us)
|
||||
- relative anchor + absolute offset coord system (+ "rest" / leftover during widget layout)
|
||||
- single threaded ui & pass context around to make non async usage straightforward (pretty unsure about this)
|
||||
- widgets store outside of the actual rendering so they can be moved around and swapped easily (unsure about this but seems to work good for now)
|
||||
|
||||
under heavy initial development so not gonna try to explain status, maybe check TODO for that;
|
||||
sizable chance it gets a rewrite once I know everything I need and what seems to work best
|
||||
|
||||
it's called iris because it's the structure around what you actually want to display and colorful
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "rig-input"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
|
||||
# headless sway has no input devices for coordinate-driving tools to move.
|
||||
[[bin]]
|
||||
name = "replay-touch"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Share the harness parser so both ways of replaying read a file the same.
|
||||
iris = { path = ".." }
|
||||
wayland-client = { workspace = true }
|
||||
wayland-protocols-wlr = { workspace = true }
|
||||
@@ -0,0 +1,141 @@
|
||||
use iris::harness::{TouchAction, TouchScript};
|
||||
use std::time::Duration;
|
||||
use wayland_client::protocol::wl_pointer::ButtonState;
|
||||
use wayland_client::protocol::{wl_registry, wl_seat};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
||||
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
||||
};
|
||||
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
||||
const SETTLE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Globals {
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
manager: Option<ZwlrVirtualPointerManagerV1>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match interface.as_str() {
|
||||
"wl_seat" => {
|
||||
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||
}
|
||||
"zwlr_virtual_pointer_manager_v1" => {
|
||||
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(Globals: ignore wl_seat::WlSeat);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerV1);
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [width, height, path] = args.as_slice() else {
|
||||
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
|
||||
let text = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
|
||||
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
|
||||
|
||||
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
|
||||
fail(&format!(
|
||||
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
|
||||
))
|
||||
});
|
||||
let mut queue = conn.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
let display = conn.display();
|
||||
display.get_registry(&qh, ());
|
||||
let mut globals = Globals::default();
|
||||
queue
|
||||
.roundtrip(&mut globals)
|
||||
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
|
||||
|
||||
let manager = globals.manager.as_ref().unwrap_or_else(|| {
|
||||
fail(
|
||||
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
|
||||
be synthesised; sway and every wlroots compositor do",
|
||||
)
|
||||
});
|
||||
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
|
||||
|
||||
// Put the pointer where the gesture starts and let the compositor
|
||||
// settle before anything is pressed. Without this the press is
|
||||
// dropped: sway has just learned about this pointer, and a button
|
||||
// sent in the same breath as the motion that first puts it over a
|
||||
// window arrives before there is a focused surface to send it to --
|
||||
// winit sees `CursorEntered`, the moves and the *release*, never the
|
||||
// press, so the gesture reads as a hover and nothing scrolls. Found
|
||||
// by printing winit's own events; the settle is what fixed it.
|
||||
if let Some(first) = script.samples.first() {
|
||||
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
|
||||
pointer.frame();
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
std::thread::sleep(SETTLE);
|
||||
}
|
||||
|
||||
let mut previous = 0;
|
||||
for sample in &script.samples {
|
||||
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
|
||||
previous = sample.t_ms;
|
||||
let t = sample.t_ms as u32;
|
||||
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
|
||||
pointer.frame();
|
||||
// The button goes in a frame of its own, *after* the motion has
|
||||
// been committed. Sent in the same frame as the motion that
|
||||
// first puts the pointer over the window, sway drops it: the
|
||||
// client sees `CursorEntered` and the moves but never a
|
||||
// `MouseInput { state: Pressed }`, so the whole gesture reads as
|
||||
// a hover and nothing scrolls. Found exactly that way, by
|
||||
// printing winit's events.
|
||||
let state = match sample.action {
|
||||
TouchAction::Down => Some(ButtonState::Pressed),
|
||||
TouchAction::Up => Some(ButtonState::Released),
|
||||
TouchAction::Move => None,
|
||||
};
|
||||
if let Some(state) = state {
|
||||
pointer.button(t, BTN_LEFT, state);
|
||||
pointer.frame();
|
||||
}
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
}
|
||||
pointer.destroy();
|
||||
conn.flush().ok();
|
||||
}
|
||||
|
||||
fn parse(text: &str, what: &str) -> u32 {
|
||||
text.parse()
|
||||
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
|
||||
}
|
||||
|
||||
fn fail(message: &str) -> ! {
|
||||
eprintln!("replay-touch: {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["clippy", "rustfmt"]
|
||||
@@ -0,0 +1,14 @@
|
||||
# The compositor `scripts/run-headless.sh` starts, so that an example has a
|
||||
# surface where there is no display. Nothing here is meant to be looked at
|
||||
# directly; `grim` is.
|
||||
#
|
||||
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
|
||||
# to go wrong for no gain.
|
||||
xwayland disable
|
||||
|
||||
# The default output, overridden per run by `--mode`. Larger than the window
|
||||
# an example opens, so nothing is scaled or clipped.
|
||||
output HEADLESS-1 mode 1920x1200@60Hz
|
||||
|
||||
default_border none
|
||||
focus_follows_mouse no
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/bin/sh
|
||||
# Run an iris example on a machine with no display.
|
||||
#
|
||||
# ./scripts/run-headless.sh tabs
|
||||
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
||||
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
|
||||
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
|
||||
#
|
||||
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
|
||||
# project that depends on iris can be run through the same rig. `--bin` runs a
|
||||
# crate binary rather than an example, and takes its own argv from
|
||||
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
|
||||
#
|
||||
# `--mode` sets the output, for running something at a size other than a
|
||||
# desktop's -- a phone's, say. Set every run rather than only when it changes:
|
||||
# the compositor is reused between runs, so a default-shaped run after a
|
||||
# custom one would otherwise inherit the other's output and quietly screenshot
|
||||
# the wrong size.
|
||||
#
|
||||
# `--replay FILE` drives a `.touch` recording into the window through
|
||||
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
|
||||
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
|
||||
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
|
||||
# since "it moved" is a claim about two pictures.
|
||||
#
|
||||
# What it supplies is a compositor for winit to open a surface on: a headless
|
||||
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
|
||||
# directory rather than joining whatever else is running, because it tiles --
|
||||
# adding a window to someone else's compositor resizes theirs.
|
||||
set -eu
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "run-headless: $1 is not installed ($2)" >&2
|
||||
exit 127
|
||||
}
|
||||
}
|
||||
need sway "the compositor an example opens its window on"
|
||||
need swaymsg "sway's control socket"
|
||||
|
||||
scripts=$(cd "$(dirname "$0")" && pwd)
|
||||
root=$(cd "$scripts/.." && pwd)
|
||||
workdir="$root"
|
||||
cd "$root"
|
||||
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
||||
seconds=3
|
||||
shot=""
|
||||
replay=""
|
||||
example=""
|
||||
kind=example
|
||||
mode=1920x1200@60Hz
|
||||
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--shot) shot=$2; shift 2 ;;
|
||||
--seconds) seconds=$2; shift 2 ;;
|
||||
--bin) kind=bin; shift ;;
|
||||
--mode) mode=$2; shift 2 ;;
|
||||
--replay) replay=$2; shift 2 ;;
|
||||
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
|
||||
--) shift; break ;;
|
||||
*) example=$1; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
||||
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
|
||||
[ -z "$shot" ] || need grim "the screenshot --shot writes"
|
||||
|
||||
mkdir -p "$run"
|
||||
export SWAYSOCK="$run/sway.sock"
|
||||
|
||||
# Named rather than left to sway's pid-based default, so a second run reuses
|
||||
# this compositor instead of starting another beside it.
|
||||
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
||||
rm -f "$SWAYSOCK"
|
||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
||||
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
|
||||
i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
swaymsg -t get_version >/dev/null 2>&1 && break
|
||||
i=$((i + 1)); sleep 0.5
|
||||
done
|
||||
swaymsg -t get_version >/dev/null 2>&1 || {
|
||||
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
rm -f "$run/display"
|
||||
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
||||
i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
[ -s "$run/display" ] && break
|
||||
i=$((i + 1)); sleep 0.5
|
||||
done
|
||||
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
||||
WAYLAND_DISPLAY=$(cat "$run/display")
|
||||
export WAYLAND_DISPLAY
|
||||
|
||||
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
||||
|
||||
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
|
||||
# The extent `replay-touch` positions against, so a script's coordinates
|
||||
# are the output's own pixels.
|
||||
out_w=${mode%x*}
|
||||
out_h=${mode#*x}; out_h=${out_h%@*}
|
||||
|
||||
# Built before the app starts, so a compile error is not reported as a
|
||||
# window that failed to move.
|
||||
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
|
||||
|
||||
cd "$workdir"
|
||||
if [ "$kind" = bin ]; then
|
||||
cargo build --bin "$example" "$@" >&2
|
||||
bin="$workdir/target/debug/$example"
|
||||
else
|
||||
cargo build --example "$example" "$@" >&2
|
||||
bin="$workdir/target/debug/examples/$example"
|
||||
fi
|
||||
|
||||
# Deliberately word-split: this is the binary's own argv, not a single path.
|
||||
# shellcheck disable=SC2086
|
||||
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
||||
pid=$!
|
||||
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
||||
|
||||
# Wait for the window to be mapped rather than for a number of seconds. A
|
||||
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
||||
# had started in the same invocation and had not composited its output yet --
|
||||
# which is indistinguishable from an app that draws nothing.
|
||||
i=0
|
||||
while [ $i -lt 40 ]; do
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
||||
i=$((i + 1)); sleep 0.25
|
||||
done
|
||||
|
||||
i=0
|
||||
while [ $i -lt "$((seconds * 2))" ]; do
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
i=$((i + 1)); sleep 0.5
|
||||
done
|
||||
|
||||
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
if [ -n "$shot" ]; then
|
||||
grim "${shot%.png}-before.png"
|
||||
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
|
||||
fi
|
||||
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
|
||||
# A fling outlives the finger: the gesture's own last sample is not
|
||||
# when the list stops. Long enough for Android's spline to settle
|
||||
# (`FlingCalculator::duration` tops out around a second and a half).
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
||||
kill "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
status=0
|
||||
else
|
||||
wait "$pid" 2>/dev/null || status=$?
|
||||
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
||||
status=${status:-1}
|
||||
fi
|
||||
|
||||
echo "--- $example output ---" >&2
|
||||
cat "$run/$example.log" >&2
|
||||
exit "$status"
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{
|
||||
primitive::{Axis, Painter, RoundedRectData, UIRegion},
|
||||
UIColor, Widget, WidgetArrLike, WidgetFn, WidgetId, WidgetLike,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RoundedRect {
|
||||
pub color: UIColor,
|
||||
pub radius: f32,
|
||||
pub thickness: f32,
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl RoundedRect {
|
||||
pub fn color(mut self, color: UIColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RoundedRect {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
painter.write(RoundedRectData {
|
||||
color: self.color,
|
||||
radius: self.radius,
|
||||
thickness: self.thickness,
|
||||
inner_radius: self.inner_radius,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Span {
|
||||
pub elements: Vec<(Range<f32>, WidgetId)>,
|
||||
pub axis: Axis,
|
||||
}
|
||||
|
||||
impl Widget for Span {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (span, child) in &self.elements {
|
||||
let mut sub_region = UIRegion::full();
|
||||
let view = sub_region.axis_mut(self.axis);
|
||||
*view.top_left.anchor = span.start;
|
||||
*view.bot_right.anchor = span.end;
|
||||
painter.draw_within(child, sub_region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn proportioned<const LEN: usize>(
|
||||
axis: Axis,
|
||||
ratios: [impl UINum; LEN],
|
||||
elements: [WidgetId; LEN],
|
||||
) -> Self {
|
||||
let ratios = ratios.map(|r| r.to_f32());
|
||||
let total: f32 = ratios.iter().sum();
|
||||
let mut start = 0.0;
|
||||
Self {
|
||||
elements: elements
|
||||
.into_iter()
|
||||
.zip(ratios)
|
||||
.map(|(e, r)| {
|
||||
let end = start + r / total;
|
||||
let res = (start..end, e);
|
||||
start = end;
|
||||
res
|
||||
})
|
||||
.collect(),
|
||||
axis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Regioned {
|
||||
region: UIRegion,
|
||||
inner: WidgetId,
|
||||
}
|
||||
|
||||
impl Widget for Regioned {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
painter.region.select(&self.region);
|
||||
painter.draw(&self.inner);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Padding {
|
||||
left: f32,
|
||||
right: f32,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
pub fn uniform(amt: f32) -> Self {
|
||||
Self {
|
||||
left: amt,
|
||||
right: amt,
|
||||
top: amt,
|
||||
bottom: amt,
|
||||
}
|
||||
}
|
||||
pub fn region(&self) -> UIRegion {
|
||||
let mut region = UIRegion::full();
|
||||
region.top_left.offset.x += self.left;
|
||||
region.top_left.offset.y += self.top;
|
||||
region.bot_right.offset.x -= self.right;
|
||||
region.bot_right.offset.y -= self.bottom;
|
||||
region
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: UINum> From<T> for Padding {
|
||||
fn from(amt: T) -> Self {
|
||||
Self::uniform(amt.to_f32())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetUtil<W> {
|
||||
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Regioned>;
|
||||
}
|
||||
|
||||
impl<W: Widget, WL: WidgetLike<W>> WidgetUtil<W> for WL {
|
||||
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Regioned> {
|
||||
WidgetFn(|ui| Regioned {
|
||||
region: padding.into().region(),
|
||||
inner: self.id(ui).erase_type(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetArrUtil<const LEN: usize, Ws> {
|
||||
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Span>;
|
||||
}
|
||||
|
||||
impl<const LEN: usize, Ws, Wa: WidgetArrLike<LEN, Ws>> WidgetArrUtil<LEN, Ws> for Wa {
|
||||
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Span> {
|
||||
WidgetFn(move |ui| Span::proportioned(axis, ratios, self.ui(ui).arr))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UINum {
|
||||
fn to_f32(self) -> f32;
|
||||
}
|
||||
|
||||
impl UINum for f32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl UINum for u32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
impl UINum for i32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
pub trait AppState {
|
||||
type Event: 'static;
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
|
||||
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> {
|
||||
state: Option<State>,
|
||||
proxy: EventLoopProxy<State::Event>,
|
||||
}
|
||||
|
||||
impl<State: AppState> App<State> {
|
||||
pub fn run() {
|
||||
let event_loop = EventLoop::with_user_event().build().unwrap();
|
||||
let proxy = event_loop.create_proxy();
|
||||
event_loop
|
||||
.run_app(&mut App::<State> { state: None, proxy })
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.state.is_none() {
|
||||
let state = State::new(event_loop, self.proxy.clone());
|
||||
self.state = Some(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.window_event(event, event_loop);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.event(event, event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, _: &ActiveEventLoop) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.exit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::prelude::*;
|
||||
use std::time::{Duration, Instant};
|
||||
use winit::dpi::{LogicalPosition, LogicalSize};
|
||||
|
||||
pub struct Selector;
|
||||
|
||||
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector
|
||||
where
|
||||
Rsc::State: HasDefaultUiState,
|
||||
{
|
||||
type Input = WeakWidget<TextEdit>;
|
||||
|
||||
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.data.render.window_region(&container).unwrap().top_left;
|
||||
let pos = ctx.data.pos + container_pos - id_pos;
|
||||
let size = region.size();
|
||||
select(
|
||||
rsc,
|
||||
ctx.data.render,
|
||||
ctx.state,
|
||||
id,
|
||||
pos,
|
||||
size,
|
||||
ctx.data.sense.is_dragging(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Selectable;
|
||||
|
||||
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
|
||||
where
|
||||
Rsc::State: HasDefaultUiState,
|
||||
{
|
||||
type Input = ();
|
||||
|
||||
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,
|
||||
ctx.data.size,
|
||||
ctx.data.sense.is_dragging(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn select(
|
||||
rsc: &mut impl UiRsc,
|
||||
render: &UiRenderState,
|
||||
state: &mut impl HasDefaultUiState,
|
||||
id: WeakWidget<TextEdit>,
|
||||
pos: Vec2,
|
||||
size: Vec2,
|
||||
dragging: bool,
|
||||
) {
|
||||
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(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()),
|
||||
LogicalSize::<f32>::from(region.size().tuple()),
|
||||
);
|
||||
}
|
||||
state.focus = Some(id);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use iris_core::Event;
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
pub struct Submit;
|
||||
impl Event for Submit {}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||
pub struct Edited;
|
||||
impl Event for Edited {}
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::prelude::*;
|
||||
use winit::{
|
||||
event::{MouseButton, MouseScrollDelta, WindowEvent},
|
||||
keyboard::{Key, NamedKey},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
cursor: CursorState,
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn event(&mut self, event: &WindowEvent) -> bool {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.cursor.pos = Vec2::new(position.x as f32, position.y as f32);
|
||||
self.cursor.exists = true;
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
let buttons = &mut self.cursor.buttons;
|
||||
let pressed = state.is_pressed();
|
||||
match button {
|
||||
MouseButton::Left => buttons.left.update(pressed),
|
||||
MouseButton::Right => buttons.right.update(pressed),
|
||||
MouseButton::Middle => buttons.middle.update(pressed),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let mut delta = match *delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
|
||||
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
|
||||
};
|
||||
if delta.x == 0.0 && self.modifiers.shift {
|
||||
delta.x = delta.y;
|
||||
delta.y = 0.0;
|
||||
}
|
||||
self.cursor.scroll_delta = delta;
|
||||
}
|
||||
WindowEvent::CursorLeft { .. } => {
|
||||
self.cursor.exists = false;
|
||||
self.modifiers.clear();
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let Key::Named(named) = event.logical_key {
|
||||
let pressed = event.state.is_pressed();
|
||||
match named {
|
||||
NamedKey::Control => {
|
||||
self.modifiers.control = pressed;
|
||||
}
|
||||
NamedKey::Shift => {
|
||||
self.modifiers.shift = pressed;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.cursor.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultUiState {
|
||||
pub fn window_size(&self) -> Vec2 {
|
||||
let size = self.renderer.window().inner_size();
|
||||
(size.width, size.height).into()
|
||||
}
|
||||
|
||||
pub fn cursor_state(&self) -> &CursorState {
|
||||
&self.input.cursor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
use crate::prelude::*;
|
||||
use arboard::Clipboard;
|
||||
use std::{marker::PhantomData, sync::Arc, time::Instant};
|
||||
use winit::{
|
||||
event::{Ime, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||
window::{Window, WindowAttributes},
|
||||
};
|
||||
|
||||
mod app;
|
||||
mod attr;
|
||||
mod event;
|
||||
mod input;
|
||||
mod render;
|
||||
mod sense;
|
||||
mod state;
|
||||
mod task;
|
||||
|
||||
pub use app::*;
|
||||
pub use attr::*;
|
||||
pub use event::*;
|
||||
pub use input::*;
|
||||
pub use render::*;
|
||||
pub use sense::*;
|
||||
pub use state::*;
|
||||
pub use task::*;
|
||||
|
||||
/// Sends an application's own events to its event loop. It wraps the proxy
|
||||
/// rather than being one because task updates travel the same way: what an
|
||||
/// application sends is its `Event`, not the loop's whole message type.
|
||||
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
|
||||
|
||||
impl<State: DefaultAppState> Clone for Proxy<State> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> Proxy<State> {
|
||||
pub fn send_event(&self, event: State::Event) {
|
||||
let _ = self.0.send_event(DefaultEvent::User(event));
|
||||
}
|
||||
}
|
||||
|
||||
/// What the event loop carries: the application's own events, and the
|
||||
/// updates tasks send back to the ui thread.
|
||||
pub enum DefaultEvent<State: DefaultAppState> {
|
||||
User(State::Event),
|
||||
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
|
||||
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
|
||||
let _ = self.0.send_event(DefaultEvent::Update(update));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultUiState {
|
||||
pub root: Option<StrongWidget>,
|
||||
pub renderer: UiRenderer,
|
||||
pub input: Input,
|
||||
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(),
|
||||
clipboard: Clipboard::new().unwrap(),
|
||||
ime: 0,
|
||||
last_click: Instant::now(),
|
||||
focus: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasDefaultUiState: Sized + 'static {
|
||||
fn default_state(&self) -> &DefaultUiState;
|
||||
fn default_state_mut(&mut self) -> &mut DefaultUiState;
|
||||
}
|
||||
|
||||
pub trait DefaultAppState: HasDefaultUiState {
|
||||
type Event: Send = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {}
|
||||
#[allow(unused_variables)]
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event: WindowEvent,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
}
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
events: Default::default(),
|
||||
tasks: Tasks::init(queue),
|
||||
state: Default::default(),
|
||||
_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
type Event = DefaultEvent<State>;
|
||||
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(State::window_attributes())
|
||||
.unwrap();
|
||||
let default_state = DefaultUiState::new(window);
|
||||
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
|
||||
let state = State::new(default_state, &mut rsc, Proxy(proxy));
|
||||
let render = UiRenderState::new();
|
||||
Self { rsc, state, render }
|
||||
}
|
||||
|
||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||
match event {
|
||||
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
|
||||
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
|
||||
}
|
||||
self.request_redraw_if_needed();
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
let Self { rsc, render, state } = self;
|
||||
|
||||
let ui_state = state.default_state_mut();
|
||||
let input_changed = ui_state.input.event(&event);
|
||||
let cursor_state = ui_state.cursor_state().clone();
|
||||
let old = ui_state.focus;
|
||||
if cursor_state.buttons.left.is_start() {
|
||||
ui_state.focus = None;
|
||||
}
|
||||
if input_changed {
|
||||
render.run_sensors(rsc, state, cursor_state);
|
||||
}
|
||||
let ui_state = state.default_state_mut();
|
||||
if old != ui_state.focus
|
||||
&& let Some(old) = old
|
||||
{
|
||||
old.edit(rsc).deselect();
|
||||
}
|
||||
match &event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => {
|
||||
render.update(&ui_state.root, rsc);
|
||||
ui_state.renderer.update(&mut rsc.ui, render);
|
||||
ui_state.renderer.draw();
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
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(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 => {
|
||||
rsc.run_event::<Submit>(sel, (), state);
|
||||
}
|
||||
TextInputResult::Paste => {
|
||||
if let Ok(t) = ui_state.clipboard.get_text() {
|
||||
text.insert(&t);
|
||||
}
|
||||
rsc.run_event::<Edited>(sel, (), state);
|
||||
}
|
||||
TextInputResult::Copy(text) => {
|
||||
if let Err(err) = ui_state.clipboard.set_text(text) {
|
||||
eprintln!("failed to copy text to clipboard: {err}")
|
||||
}
|
||||
}
|
||||
TextInputResult::Used => {
|
||||
rsc.run_event::<Edited>(sel, (), state);
|
||||
}
|
||||
TextInputResult::Unused => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::Ime(ime) => {
|
||||
if let Some(sel) = ui_state.focus {
|
||||
let mut text = sel.edit(rsc);
|
||||
match ime {
|
||||
Ime::Enabled | Ime::Disabled => (),
|
||||
Ime::Preedit(content, _pos) => {
|
||||
// TODO: highlight once that's real
|
||||
text.replace(ui_state.ime, content);
|
||||
ui_state.ime = content.chars().count();
|
||||
}
|
||||
Ime::Commit(content) => {
|
||||
text.insert(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
state.window_event(event, rsc, render);
|
||||
self.request_redraw_if_needed();
|
||||
self.state.default_state_mut().input.end_frame();
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.state.exit(&mut self.rsc, &mut self.render);
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> DefaultApp<State> {
|
||||
fn request_redraw_if_needed(&mut self) {
|
||||
let ui_state = self.state.default_state_mut();
|
||||
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RscIdx<Rsc> {
|
||||
type Output;
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
|
||||
}
|
||||
|
||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use wgpu::*;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
|
||||
pub struct UiRenderer {
|
||||
window: Arc<Window>,
|
||||
surface: Surface<'static>,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
encoder: CommandEncoder,
|
||||
pub ui: UiRenderNode,
|
||||
}
|
||||
|
||||
impl UiRenderer {
|
||||
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 = 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());
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(CLEAR_COLOR),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
self.ui.draw(render_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
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.width, size.height), &self.queue);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = Instance::new(InstanceDescriptor {
|
||||
backends: Backends::PRIMARY,
|
||||
display: Some(Box::new(window.clone())),
|
||||
..InstanceDescriptor::new_without_display_handle()
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Could not create window surface!");
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
apply_limit_buckets: false,
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_limits: Limits {
|
||||
max_buffer_size: 1 << 30,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get device!");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let ui = UiRenderNode::new(&device, &config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
encoder,
|
||||
ui,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &Window {
|
||||
self.window.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
use crate::prelude::*;
|
||||
use std::{
|
||||
ops::{BitOr, Deref, DerefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CursorButton {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CursorSense {
|
||||
PressStart(CursorButton),
|
||||
Pressing(CursorButton),
|
||||
PressEnd(CursorButton),
|
||||
HoverStart,
|
||||
Hovering,
|
||||
HoverEnd,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CursorSenses(Vec<CursorSense>);
|
||||
|
||||
impl Event for CursorSenses {
|
||||
type Data<'a> = CursorData<'a>;
|
||||
type Global = Hovered;
|
||||
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;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A press or a scroll is used up by whatever answered it, so it stops
|
||||
/// there. Hovering is not: a cursor resting somewhere goes on resting.
|
||||
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||
!data.sense.position_only()
|
||||
}
|
||||
}
|
||||
|
||||
/// Who the cursor was inside, before and after an input. The difference is
|
||||
/// whose hover has ended -- including a widget a higher layer has covered,
|
||||
/// which the walk stops before reaching.
|
||||
///
|
||||
/// Two buffers that swap rather than one rebuilt, so an input allocates
|
||||
/// nothing once they have grown.
|
||||
#[derive(Default)]
|
||||
pub struct Hovered {
|
||||
was: Vec<WidgetId>,
|
||||
now: Vec<WidgetId>,
|
||||
}
|
||||
|
||||
impl CursorSense {
|
||||
pub fn click() -> Self {
|
||||
Self::PressStart(CursorButton::Left)
|
||||
}
|
||||
pub fn click_or_drag() -> CursorSenses {
|
||||
Self::click() | Self::Pressing(CursorButton::Left)
|
||||
}
|
||||
pub fn unclick() -> Self {
|
||||
Self::PressEnd(CursorButton::Left)
|
||||
}
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
||||
}
|
||||
|
||||
/// False if the sense is a button or a scroll, true if it is only about
|
||||
/// where the cursor is.
|
||||
fn position_only(&self) -> bool {
|
||||
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CursorState {
|
||||
pub pos: Vec2,
|
||||
pub exists: bool,
|
||||
pub buttons: CursorButtons,
|
||||
pub scroll_delta: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CursorButtons {
|
||||
pub left: ActivationState,
|
||||
pub middle: ActivationState,
|
||||
pub right: ActivationState,
|
||||
}
|
||||
|
||||
impl CursorButtons {
|
||||
pub fn select(&self, button: &CursorButton) -> &ActivationState {
|
||||
match button {
|
||||
CursorButton::Left => &self.left,
|
||||
CursorButton::Right => &self.right,
|
||||
CursorButton::Middle => &self.middle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.left.end_frame();
|
||||
self.middle.end_frame();
|
||||
self.right.end_frame();
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (CursorButton, &ActivationState)> {
|
||||
[
|
||||
CursorButton::Left,
|
||||
CursorButton::Middle,
|
||||
CursorButton::Right,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|b| (b, self.select(&b)))
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorState {
|
||||
/// True if the cursor is only reporting where it is: no button and no
|
||||
/// scroll this frame.
|
||||
pub fn position_only(&self) -> bool {
|
||||
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.buttons.end_frame();
|
||||
self.scroll_delta = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub enum ActivationState {
|
||||
Start,
|
||||
On,
|
||||
End,
|
||||
#[default]
|
||||
Off,
|
||||
}
|
||||
|
||||
/// this and other similar stuff has a generic
|
||||
/// because I kind of want to make CursorModule generic
|
||||
/// 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: HasEvents, Data> {
|
||||
pub senses: CursorSenses,
|
||||
pub f: Rc<dyn EventFn<Ctx, Data>>,
|
||||
}
|
||||
|
||||
pub type SenseShape = UiRegion;
|
||||
|
||||
#[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: CursorState,
|
||||
/// the first sense that triggered this
|
||||
pub sense: CursorSense,
|
||||
pub render: &'a UiRenderState,
|
||||
}
|
||||
|
||||
pub trait SensorUi {
|
||||
fn run_sensors<Rsc: HasEvents>(
|
||||
&self,
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
);
|
||||
}
|
||||
|
||||
impl SensorUi for UiRenderState {
|
||||
fn run_sensors<Rsc: HasEvents>(
|
||||
&self,
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
) {
|
||||
// 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 active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
|
||||
hovered.now.clear();
|
||||
let position_only = cursor.position_only();
|
||||
let region_of = |id| self.window_region(&id);
|
||||
|
||||
for layer in self.layers.indices().rev() {
|
||||
let mut consumed = false;
|
||||
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
||||
let Some(region) = region_of(id) else {
|
||||
continue;
|
||||
};
|
||||
if !cursor.exists || !region.contains(cursor.pos) {
|
||||
continue;
|
||||
}
|
||||
hovered.now.push(id);
|
||||
let hover = match hovered.was.contains(&id) {
|
||||
true => ActivationState::On,
|
||||
false => ActivationState::Start,
|
||||
};
|
||||
// A press or a scroll stops where something answered it, so a
|
||||
// button over a list does not swallow the list's scrolling.
|
||||
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
|
||||
// A cursor doing neither stops at whatever it is over, so
|
||||
// hovering does not reach through.
|
||||
consumed |= position_only;
|
||||
}
|
||||
// Applied after the layer, never during it: senses on one layer do
|
||||
// not block each other.
|
||||
if consumed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever the cursor was inside and is not now, whether it left or a
|
||||
// layer above took the input before the walk reached it. A widget that
|
||||
// stopped being drawn has no region to report and is simply dropped.
|
||||
for &id in &hovered.was {
|
||||
if !hovered.now.contains(&id)
|
||||
&& let Some(region) = region_of(id)
|
||||
{
|
||||
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
|
||||
}
|
||||
}
|
||||
std::mem::swap(&mut hovered.was, &mut hovered.now);
|
||||
|
||||
let senses = rsc.events_mut().get_type::<CursorSense>();
|
||||
senses.active = active;
|
||||
senses.global = hovered;
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one widget's cursor senses, and says whether they used up the input.
|
||||
fn deliver<Rsc: HasEvents>(
|
||||
render: &UiRenderState,
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
id: WidgetId,
|
||||
hover: ActivationState,
|
||||
cursor: &CursorState,
|
||||
region: PixelRegion,
|
||||
) -> bool {
|
||||
let data = CursorData {
|
||||
pos: cursor.pos - region.top_left,
|
||||
size: region.bot_right - region.top_left,
|
||||
scroll_delta: cursor.scroll_delta,
|
||||
hover,
|
||||
cursor: cursor.clone(),
|
||||
// this does not have any meaning;
|
||||
// might wanna set up Event to have a prepare stage
|
||||
sense: CursorSense::Hovering,
|
||||
render,
|
||||
};
|
||||
rsc.run_event::<CursorSense>(id, data, state)
|
||||
}
|
||||
|
||||
pub fn should_run(
|
||||
senses: &CursorSenses,
|
||||
cursor: &CursorState,
|
||||
hover: ActivationState,
|
||||
) -> Option<CursorSense> {
|
||||
for sense in senses.iter() {
|
||||
// A widget the cursor is no longer inside senses only its position:
|
||||
// the press that ended its hover landed on something else.
|
||||
if !hover.is_on() && !sense.position_only() {
|
||||
continue;
|
||||
}
|
||||
if match sense {
|
||||
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
||||
CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(),
|
||||
CursorSense::HoverStart => hover.is_start(),
|
||||
CursorSense::Hovering => hover.is_on(),
|
||||
CursorSense::HoverEnd => hover.is_end(),
|
||||
CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
||||
} {
|
||||
return Some(*sense);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl ActivationState {
|
||||
pub fn is_start(&self) -> bool {
|
||||
*self == Self::Start
|
||||
}
|
||||
pub fn is_on(&self) -> bool {
|
||||
*self == Self::Start || *self == Self::On
|
||||
}
|
||||
pub fn is_end(&self) -> bool {
|
||||
*self == Self::End
|
||||
}
|
||||
pub fn is_off(&self) -> bool {
|
||||
*self == Self::End || *self == Self::Off
|
||||
}
|
||||
pub fn update(&mut self, on: bool) {
|
||||
*self = match *self {
|
||||
Self::Start => match on {
|
||||
true => Self::On,
|
||||
false => Self::End,
|
||||
},
|
||||
Self::On => match on {
|
||||
true => Self::On,
|
||||
false => Self::End,
|
||||
},
|
||||
Self::End => match on {
|
||||
true => Self::Start,
|
||||
false => Self::Off,
|
||||
},
|
||||
Self::Off => match on {
|
||||
true => Self::Start,
|
||||
false => Self::Off,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
match self {
|
||||
Self::Start => *self = Self::On,
|
||||
Self::End => *self = Self::Off,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLike for CursorSense {
|
||||
type Event = CursorSenses;
|
||||
fn into_event(self) -> Self::Event {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for CursorSenses {
|
||||
type Target = Vec<CursorSense>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for CursorSenses {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CursorSense> for CursorSenses {
|
||||
fn from(val: CursorSense) -> Self {
|
||||
CursorSenses(vec![val])
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr for CursorSense {
|
||||
type Output = CursorSenses;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
CursorSenses(vec![self, rhs])
|
||||
}
|
||||
}
|
||||
|
||||
impl BitOr<CursorSense> for CursorSenses {
|
||||
type Output = Self;
|
||||
|
||||
fn bitor(mut self, rhs: CursorSense) -> Self::Output {
|
||||
self.0.push(rhs);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use iris_core::HasState;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
sync::mpsc::{
|
||||
UnboundedReceiver as AsyncReceiver, UnboundedSender as AsyncSender,
|
||||
unbounded_channel as async_channel,
|
||||
},
|
||||
};
|
||||
|
||||
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 {}
|
||||
|
||||
/// Hands an update from a task to the thread that owns the ui. Delivery and
|
||||
/// waking are one act: a host posts the update as a message its loop already
|
||||
/// carries, so nothing has to wake the loop separately, or claim a redraw to
|
||||
/// be looked at.
|
||||
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
|
||||
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
|
||||
}
|
||||
|
||||
pub struct Tasks<Rsc: HasState> {
|
||||
start: AsyncSender<BoxTask>,
|
||||
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||
}
|
||||
|
||||
pub struct TaskCtx<Rsc: HasState> {
|
||||
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||
}
|
||||
|
||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
||||
self.queue.send(Box::new(f));
|
||||
}
|
||||
}
|
||||
|
||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
|
||||
impl<Rsc: HasState> Tasks<Rsc> {
|
||||
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
|
||||
let (start, start_recv) = async_channel();
|
||||
std::thread::spawn(|| {
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(listen(start_recv))
|
||||
});
|
||||
Self { start, queue }
|
||||
}
|
||||
|
||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||
where
|
||||
F::CallOnceFuture: Send,
|
||||
{
|
||||
let queue = self.queue.clone();
|
||||
let _ = self.start.send(Box::pin(async move {
|
||||
task(TaskCtx { queue }).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen(mut recv: AsyncReceiver<BoxTask>) {
|
||||
while let Some(task) = recv.recv().await {
|
||||
tokio::spawn(task);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use iris_core::*;
|
||||
use iris_macro::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
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,
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
//! A ui with no window: build a tree, run frames, move a pointer, and read
|
||||
//! back where widgets landed.
|
||||
//!
|
||||
//! It does not draw. A claim about pixels still needs a real surface.
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
mpsc::{Receiver, SyncSender, sync_channel},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// There is no loop here to post to, so updates queue until the test asks
|
||||
/// for them.
|
||||
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
|
||||
|
||||
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
|
||||
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
|
||||
let _ = self.0.send(update);
|
||||
}
|
||||
}
|
||||
|
||||
/// `assert_eq!` for where a frame put a widget, written as its two corners.
|
||||
#[macro_export]
|
||||
macro_rules! assert_corners {
|
||||
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
|
||||
assert_eq!(
|
||||
$harness.region(&$id).expect("widget drew nothing"),
|
||||
$crate::core::PixelRegion {
|
||||
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
|
||||
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
pub use crate::assert_corners;
|
||||
|
||||
/// One replayed pointer sample, cut down to what a window delivers: where the
|
||||
/// pointer is, and whether the button changed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TouchAction {
|
||||
Down,
|
||||
Move,
|
||||
Up,
|
||||
}
|
||||
|
||||
impl TouchAction {
|
||||
fn parse(word: &str) -> Option<Self> {
|
||||
match word {
|
||||
"down" => Some(Self::Down),
|
||||
"move" => Some(Self::Move),
|
||||
"up" => Some(Self::Up),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TouchSample {
|
||||
pub t_ms: u64,
|
||||
pub action: TouchAction,
|
||||
pub pos: Vec2,
|
||||
}
|
||||
|
||||
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
|
||||
/// a line, `#` to end of line ignored.
|
||||
///
|
||||
/// One parser for both ways of replaying a recording -- into a harness, and
|
||||
/// into a real window -- so the two cannot read the same file differently.
|
||||
pub struct TouchScript {
|
||||
pub samples: Vec<TouchSample>,
|
||||
}
|
||||
|
||||
impl TouchScript {
|
||||
pub fn parse(text: &str) -> Result<Self, String> {
|
||||
let mut samples: Vec<TouchSample> = Vec::new();
|
||||
for (i, line) in text.lines().enumerate() {
|
||||
let line = line.split('#').next().unwrap_or("").trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
|
||||
let mut words = line.split_whitespace();
|
||||
let (Some(t), Some(action), Some(x), Some(y), None) = (
|
||||
words.next(),
|
||||
words.next(),
|
||||
words.next(),
|
||||
words.next(),
|
||||
words.next(),
|
||||
) else {
|
||||
return Err(at("expected `t_ms action x y`"));
|
||||
};
|
||||
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
|
||||
let action =
|
||||
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
|
||||
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
|
||||
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
|
||||
if let Some(last) = samples.last()
|
||||
&& t_ms < last.t_ms
|
||||
{
|
||||
return Err(at("samples must be in time order"));
|
||||
}
|
||||
samples.push(TouchSample {
|
||||
t_ms,
|
||||
action,
|
||||
pos: Vec2::new(x, y),
|
||||
});
|
||||
}
|
||||
Ok(Self { samples })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HarnessState {
|
||||
pub root: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl HasRoot for HarnessState {
|
||||
fn set_root(&mut self, root: StrongWidget) {
|
||||
self.root = Some(root);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Harness {
|
||||
pub rsc: DefaultRsc<HarnessState>,
|
||||
pub render: UiRenderState,
|
||||
pub state: HarnessState,
|
||||
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
|
||||
cursor: CursorState,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
/// `size` is the output in physical pixels.
|
||||
pub fn new(size: impl Into<Vec2>) -> Self {
|
||||
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
|
||||
// bound that comes with `SyncSender` is far past anything a test
|
||||
// leaves unread.
|
||||
let (send, updates) = sync_channel(1024);
|
||||
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize(size);
|
||||
Self {
|
||||
rsc,
|
||||
render,
|
||||
state: HarnessState::default(),
|
||||
updates,
|
||||
cursor: CursorState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.render.output_size()
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.render.resize(size);
|
||||
}
|
||||
|
||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||
widget.set_root(&mut self.rsc, &mut self.state);
|
||||
self.frame();
|
||||
}
|
||||
|
||||
pub fn needs_redraw(&self) -> bool {
|
||||
self.render
|
||||
.needs_redraw(&self.state.root, self.rsc.widgets())
|
||||
}
|
||||
|
||||
pub fn apply_updates(&mut self) -> usize {
|
||||
let mut applied = 0;
|
||||
while let Ok(update) = self.updates.try_recv() {
|
||||
update(&mut self.state, &mut self.rsc);
|
||||
applied += 1;
|
||||
}
|
||||
applied
|
||||
}
|
||||
|
||||
/// Waits for a task's first update, then applies everything waiting.
|
||||
/// False if none arrived in time.
|
||||
#[must_use]
|
||||
pub fn await_update(&mut self, timeout: Duration) -> bool {
|
||||
let Ok(update) = self.updates.recv_timeout(timeout) else {
|
||||
return false;
|
||||
};
|
||||
update(&mut self.state, &mut self.rsc);
|
||||
self.apply_updates();
|
||||
true
|
||||
}
|
||||
|
||||
/// Lays the tree out and builds its primitives.
|
||||
pub fn frame(&mut self) {
|
||||
self.apply_updates();
|
||||
self.render.update(&self.state.root, &mut self.rsc);
|
||||
}
|
||||
|
||||
/// Where the last frame put a widget, or `None` if it drew nothing.
|
||||
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||
self.render.window_region(id)
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
|
||||
self.cursor.pos = pos.into();
|
||||
self.cursor.exists = true;
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn leave(&mut self) {
|
||||
self.cursor.exists = false;
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn press(&mut self, button: CursorButton) {
|
||||
self.button(button).update(true);
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn release(&mut self, button: CursorButton) {
|
||||
self.button(button).update(false);
|
||||
self.sense();
|
||||
}
|
||||
|
||||
/// A wheel carries no position, so this goes wherever the cursor was last
|
||||
/// moved to -- nowhere, until it has been moved.
|
||||
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
|
||||
self.cursor.scroll_delta = delta.into();
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn click(&mut self, pos: impl Into<Vec2>) {
|
||||
self.move_to(pos);
|
||||
self.press(CursorButton::Left);
|
||||
self.release(CursorButton::Left);
|
||||
}
|
||||
|
||||
/// Drives a recorded gesture through the harness.
|
||||
pub fn replay(&mut self, script: &TouchScript) {
|
||||
for sample in &script.samples {
|
||||
match sample.action {
|
||||
TouchAction::Down => {
|
||||
self.move_to(sample.pos);
|
||||
self.press(CursorButton::Left);
|
||||
}
|
||||
TouchAction::Move => self.move_to(sample.pos),
|
||||
TouchAction::Up => {
|
||||
self.move_to(sample.pos);
|
||||
self.release(CursorButton::Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
||||
let buttons = &mut self.cursor.buttons;
|
||||
match button {
|
||||
CursorButton::Left => &mut buttons.left,
|
||||
CursorButton::Middle => &mut buttons.middle,
|
||||
CursorButton::Right => &mut buttons.right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches against the layout of the last frame, which is what a
|
||||
/// window delivers input against too.
|
||||
fn sense(&mut self) {
|
||||
let cursor = self.cursor.clone();
|
||||
self.render
|
||||
.run_sensors(&mut self.rsc, &mut self.state, cursor);
|
||||
self.cursor.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
|
||||
Ok(TouchScript::parse(text)?
|
||||
.samples
|
||||
.iter()
|
||||
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recording_is_time_action_and_a_point() {
|
||||
assert_eq!(
|
||||
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
|
||||
[
|
||||
(0, TouchAction::Down, 10.0, 20.0),
|
||||
(16, TouchAction::Move, 10.5, 24.0),
|
||||
(32, TouchAction::Up, 10.5, 24.0),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_lines_and_comments_are_not_samples() {
|
||||
assert_eq!(
|
||||
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
|
||||
[(0, TouchAction::Down, 1.0, 2.0)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recording_that_goes_backwards_is_rejected() {
|
||||
// Replay waits out the gap between samples, so time running backwards
|
||||
// would silently become no wait at all.
|
||||
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
|
||||
assert!(err.contains("time order"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_that_is_not_a_sample_says_which_line() {
|
||||
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
|
||||
assert!(err.contains("line 2"), "{err}");
|
||||
assert!(err.contains("down/move/up"), "{err}");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
pub use ui::*;
|
||||
pub use widget::*;
|
||||
|
||||
use crate::primitive::Color;
|
||||
pub type UIColor = Color<u8>;
|
||||
@@ -1,101 +0,0 @@
|
||||
use crate::{
|
||||
primitive::{Painter, Primitives},
|
||||
util::{IDTracker, ID},
|
||||
HashMap, Widget, WidgetId, WidgetLike, WidgetRef,
|
||||
};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
pub struct UI {
|
||||
ids: IDTracker,
|
||||
base: Option<WidgetId>,
|
||||
pub widgets: Widgets,
|
||||
}
|
||||
|
||||
pub struct Widgets(HashMap<ID, Box<dyn Widget>>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UIBuilder {
|
||||
ui: Rc<RefCell<UI>>,
|
||||
}
|
||||
|
||||
impl From<UI> for UIBuilder {
|
||||
fn from(ui: UI) -> Self {
|
||||
UIBuilder {
|
||||
ui: Rc::new(RefCell::new(ui)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UIBuilder {
|
||||
pub fn add<W: Widget>(&mut self, w: W) -> WidgetRef<W> {
|
||||
WidgetRef::new(self.clone(), [self.push(w)])
|
||||
}
|
||||
|
||||
pub fn push<W: Widget>(&mut self, w: W) -> WidgetId {
|
||||
let mut ui = self.ui.borrow_mut();
|
||||
let id = ui.ids.next();
|
||||
ui.widgets.insert(id.duplicate(), w);
|
||||
WidgetId::new(id, TypeId::of::<W>())
|
||||
}
|
||||
|
||||
pub fn finish<WL: WidgetLike<W>, W>(mut self, base: WL) -> UI {
|
||||
let base = base.id(&mut self).erase_type();
|
||||
let mut ui = Rc::into_inner(self.ui).unwrap().into_inner();
|
||||
ui.base = Some(base);
|
||||
ui
|
||||
}
|
||||
}
|
||||
|
||||
impl UI {
|
||||
pub fn build() -> UIBuilder {
|
||||
Self::empty().into()
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
ids: IDTracker::new(),
|
||||
base: None,
|
||||
widgets: Widgets::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_primitives(&self) -> Primitives {
|
||||
let mut painter = Painter::new(&self.widgets);
|
||||
if let Some(base) = &self.base {
|
||||
painter.draw(base);
|
||||
}
|
||||
painter.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &WidgetId) -> &dyn Widget {
|
||||
self.0.get(&id.id).unwrap().as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut<W: Widget>(&mut self, id: &WidgetId<W>) -> Option<&mut W> {
|
||||
self.0.get_mut(&id.id).unwrap().as_any_mut().downcast_mut()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, id: ID, widget: impl Widget) {
|
||||
self.0.insert(id, Box::new(widget));
|
||||
}
|
||||
|
||||
pub fn insert_any(&mut self, id: ID, widget: Box<dyn Widget>) {
|
||||
self.0.insert(id, widget);
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Widget {
|
||||
pub fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use crate::{primitive::Painter, util::ID, UIBuilder};
|
||||
|
||||
pub trait Widget: 'static + Any {
|
||||
fn draw(&self, painter: &mut Painter);
|
||||
}
|
||||
|
||||
impl<W: Widget> Widget for (W,) {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
self.0.draw(painter);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Debug)]
|
||||
pub struct WidgetId<W = ()> {
|
||||
pub(super) ty: TypeId,
|
||||
pub(super) id: ID,
|
||||
_pd: PhantomData<W>,
|
||||
}
|
||||
|
||||
// TODO: temp
|
||||
impl Clone for WidgetId {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
ty: self.ty,
|
||||
id: self.id.duplicate(),
|
||||
_pd: self._pd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> WidgetId<W> {
|
||||
pub(super) fn new(id: ID, ty: TypeId) -> Self {
|
||||
Self {
|
||||
ty,
|
||||
id,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
pub fn erase_type(self) -> WidgetId<()> {
|
||||
self.cast_type()
|
||||
}
|
||||
|
||||
fn cast_type<W2>(self) -> WidgetId<W2> {
|
||||
WidgetId {
|
||||
ty: self.ty,
|
||||
id: self.id,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetLike<W> {
|
||||
fn id(self, ui: &mut UIBuilder) -> WidgetId<W>;
|
||||
}
|
||||
|
||||
/// wouldn't be needed if negative trait bounds & disjoint impls existed
|
||||
pub struct WidgetFn<F: FnOnce(&mut UIBuilder) -> W, W>(pub F);
|
||||
|
||||
impl<W: Widget, F: FnOnce(&mut UIBuilder) -> W> WidgetLike<W> for WidgetFn<F, W> {
|
||||
fn id(self, ui: &mut UIBuilder) -> WidgetId<W> {
|
||||
let w = (self.0)(ui);
|
||||
ui.add(w).to_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Widget> WidgetLike<W> for W {
|
||||
fn id(self, ui: &mut UIBuilder) -> WidgetId<W> {
|
||||
ui.add(self).to_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> WidgetLike<W> for WidgetId<W> {
|
||||
fn id(self, _: &mut UIBuilder) -> WidgetId<W> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> WidgetLike<W> for WidgetArr<1, (W,)> {
|
||||
fn id(self, _: &mut UIBuilder) -> WidgetId<W> {
|
||||
let [id] = self.arr;
|
||||
id.cast_type()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WidgetArr<const LEN: usize, Ws> {
|
||||
pub ui: UIBuilder,
|
||||
pub arr: [WidgetId<()>; LEN],
|
||||
_pd: PhantomData<Ws>,
|
||||
}
|
||||
|
||||
impl<const LEN: usize, Ws> WidgetArr<LEN, Ws> {
|
||||
pub fn new(ui: UIBuilder, arr: [WidgetId<()>; LEN]) -> Self {
|
||||
Self {
|
||||
ui,
|
||||
arr,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type WidgetRef<W> = WidgetArr<1, (W,)>;
|
||||
|
||||
impl<W> WidgetRef<W> {
|
||||
pub fn handle(&self) -> WidgetId<W> {
|
||||
let [id] = &self.arr;
|
||||
id.clone().cast_type()
|
||||
}
|
||||
pub fn to_id(self) -> WidgetId<W> {
|
||||
let [id] = self.arr;
|
||||
id.cast_type()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetArrLike<const LEN: usize, Ws> {
|
||||
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<LEN, Ws>;
|
||||
}
|
||||
|
||||
impl<const LEN: usize, Ws> WidgetArrLike<LEN, Ws> for WidgetArr<LEN, Ws> {
|
||||
fn ui(self, _: &mut UIBuilder) -> WidgetArr<LEN, Ws> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// I hate this language it's so bad why do I even use it
|
||||
macro_rules! impl_node_arr {
|
||||
($n:expr;$($T:tt)*) => {
|
||||
impl<$($T,${concat($T,$T)}: WidgetLike<$T>,)*> WidgetArrLike<$n, ($($T,)*)> for ($(${concat($T,$T)},)*) {
|
||||
#[allow(unused_variables)]
|
||||
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<$n, ($($T,)*)> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($T,)*) = self;
|
||||
WidgetArr::new(
|
||||
ui.clone(),
|
||||
[$($T.id(ui).cast_type(),)*],
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_node_arr!(1;A);
|
||||
impl_node_arr!(2;A B);
|
||||
impl_node_arr!(3;A B C);
|
||||
impl_node_arr!(4;A B C D);
|
||||
impl_node_arr!(5;A B C D E);
|
||||
impl_node_arr!(6;A B C D E F);
|
||||
impl_node_arr!(7;A B C D E F G);
|
||||
impl_node_arr!(8;A B C D E F G H);
|
||||
impl_node_arr!(9;A B C D E F G H I);
|
||||
impl_node_arr!(10;A B C D E F G H I J);
|
||||
impl_node_arr!(11;A B C D E F G H I J K);
|
||||
impl_node_arr!(12;A B C D E F G H I J K L);
|
||||
+23
-13
@@ -1,16 +1,26 @@
|
||||
#![feature(macro_metavar_expr_concat)]
|
||||
#![feature(const_ops)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(const_from)]
|
||||
#![feature(trait_alias)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
#![feature(async_fn_traits)]
|
||||
|
||||
mod layout;
|
||||
mod render;
|
||||
mod util;
|
||||
mod base;
|
||||
pub mod default;
|
||||
pub mod event;
|
||||
pub mod harness;
|
||||
pub mod widget;
|
||||
|
||||
pub use layout::*;
|
||||
pub use render::*;
|
||||
pub use base::*;
|
||||
pub use iris_core as core;
|
||||
pub use iris_macro as macros;
|
||||
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
pub use default::*;
|
||||
pub use event::*;
|
||||
pub use iris_core::*;
|
||||
pub use iris_macro::*;
|
||||
pub use widget::*;
|
||||
|
||||
pub use iris_core::util::Vec2;
|
||||
pub use len_fns::*;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod testing;
|
||||
|
||||
fn main() {
|
||||
testing::main();
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use crate::primitive::UIRegion;
|
||||
use wgpu::VertexAttribute;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UIRegion,
|
||||
pub ptr: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct WindowUniform {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl PrimitiveInstance {
|
||||
const ATTRIBS: [VertexAttribute; 5] = wgpu::vertex_attr_array![
|
||||
0 => Float32x2,
|
||||
1 => Float32x2,
|
||||
2 => Float32x2,
|
||||
3 => Float32x2,
|
||||
4 => Uint32,
|
||||
];
|
||||
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
use crate::{
|
||||
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||
UI,
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
|
||||
mod data;
|
||||
pub mod primitive;
|
||||
mod util;
|
||||
|
||||
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
pub struct UIRenderNode {
|
||||
bind_group_layout: BindGroupLayout,
|
||||
bind_group: BindGroup,
|
||||
pipeline: RenderPipeline,
|
||||
|
||||
window_buffer: Buffer,
|
||||
instance: ArrBuf<PrimitiveInstance>,
|
||||
data: ArrBuf<u32>,
|
||||
}
|
||||
|
||||
impl UIRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
if self.instance.len() != 0 {
|
||||
pass.set_vertex_buffer(0, self.instance.buffer.slice(..));
|
||||
pass.draw(0..4, 0..self.instance.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &UI) {
|
||||
let primitives = ui.to_primitives();
|
||||
self.instance.update(device, queue, &primitives.instances);
|
||||
self.data.update(device, queue, &primitives.data);
|
||||
self.bind_group = Self::bind_group(
|
||||
device,
|
||||
&self.bind_group_layout,
|
||||
&self.window_buffer,
|
||||
&self.data.buffer,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
|
||||
let slice = &[WindowUniform {
|
||||
width: size.width as f32,
|
||||
height: size.height as f32,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
||||
label: Some("UI Shape Shader"),
|
||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
||||
});
|
||||
|
||||
let window_uniform = WindowUniform::default();
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("Camera Buffer"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let instance = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"instance",
|
||||
);
|
||||
let data = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"data",
|
||||
);
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: ShaderStages::VERTEX,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
label: Some("camera_bind_group_layout"),
|
||||
});
|
||||
|
||||
let bind_group = Self::bind_group(device, &bind_group_layout, &window_buffer, &data.buffer);
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[PrimitiveInstance::desc()],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format: config.format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Cw,
|
||||
cull_mode: Some(Face::Back),
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
Self {
|
||||
bind_group_layout,
|
||||
bind_group,
|
||||
pipeline,
|
||||
window_buffer,
|
||||
instance,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
window_buffer: &Buffer,
|
||||
data: &Buffer,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: window_buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: data.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui_bind_group"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#![allow(clippy::multiple_bound_locations)]
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, bytemuck::Zeroable)]
|
||||
pub struct Color<T: ColorNum> {
|
||||
r: T,
|
||||
g: T,
|
||||
b: T,
|
||||
a: T,
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
|
||||
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
|
||||
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
|
||||
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
|
||||
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
|
||||
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
|
||||
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
|
||||
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Color<T> {
|
||||
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
pub const fn rgb(r: T, g: T, b: T) -> Self {
|
||||
Self { r, g, b, a: T::MAX }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ColorNum {
|
||||
const MIN: Self;
|
||||
const MID: Self;
|
||||
const MAX: Self;
|
||||
}
|
||||
|
||||
impl ColorNum for u8 {
|
||||
const MIN: Self = u8::MIN;
|
||||
const MID: Self = u8::MAX / 2;
|
||||
const MAX: Self = u8::MAX;
|
||||
}
|
||||
|
||||
unsafe impl bytemuck::Pod for Color<u8> {}
|
||||
@@ -1,14 +0,0 @@
|
||||
use crate::primitive::{Color, PrimitiveData};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct RoundedRectData {
|
||||
pub color: Color<u8>,
|
||||
pub radius: f32,
|
||||
pub thickness: f32,
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl PrimitiveData for RoundedRectData {
|
||||
const DISCRIM: u32 = 0;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
use crate::primitive::{point::point, Point};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct UIPos {
|
||||
pub anchor: Point,
|
||||
pub offset: Point,
|
||||
}
|
||||
|
||||
impl UIPos {
|
||||
pub const fn anchor_offset(anchor_x: f32, anchor_y: f32, offset_x: f32, offset_y: f32) -> Self {
|
||||
Self {
|
||||
anchor: point(anchor_x, anchor_y),
|
||||
offset: point(offset_x, offset_y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn top_left() -> Self {
|
||||
Self::anchor_offset(0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn bottom_right() -> Self {
|
||||
Self::anchor_offset(1.0, 1.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn within(&self, region: &UIRegion) -> UIPos {
|
||||
let range = region.bot_right.anchor - region.top_left.anchor;
|
||||
let region_offset = region
|
||||
.top_left
|
||||
.offset
|
||||
.lerp(region.bot_right.offset, self.anchor);
|
||||
UIPos {
|
||||
anchor: region.top_left.anchor + self.anchor * range,
|
||||
offset: self.offset + region_offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> UIPosAxisView<'_> {
|
||||
match axis {
|
||||
Axis::X => UIPosAxisView {
|
||||
anchor: &mut self.anchor.x,
|
||||
offset: &mut self.offset.x,
|
||||
},
|
||||
Axis::Y => UIPosAxisView {
|
||||
anchor: &mut self.anchor.y,
|
||||
offset: &mut self.offset.y,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UIPosAxisView<'a> {
|
||||
pub anchor: &'a mut f32,
|
||||
pub offset: &'a mut f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct UIRegion {
|
||||
pub top_left: UIPos,
|
||||
pub bot_right: UIPos,
|
||||
}
|
||||
|
||||
impl UIRegion {
|
||||
pub const fn full() -> Self {
|
||||
Self {
|
||||
top_left: UIPos::top_left(),
|
||||
bot_right: UIPos::bottom_right(),
|
||||
}
|
||||
}
|
||||
pub fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
top_left: self.top_left.within(parent),
|
||||
bot_right: self.bot_right.within(parent),
|
||||
}
|
||||
}
|
||||
pub fn select(&mut self, inner: &Self) {
|
||||
*self = inner.within(self);
|
||||
}
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> UIRegionAxisView<'_> {
|
||||
UIRegionAxisView {
|
||||
top_left: self.top_left.axis_mut(axis),
|
||||
bot_right: self.bot_right.axis_mut(axis),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UIRegionAxisView<'a> {
|
||||
pub top_left: UIPosAxisView<'a>,
|
||||
pub bot_right: UIPosAxisView<'a>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Axis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
mod color;
|
||||
mod def;
|
||||
mod format;
|
||||
mod point;
|
||||
|
||||
pub use color::*;
|
||||
pub use def::*;
|
||||
pub use format::*;
|
||||
pub use point::*;
|
||||
|
||||
use crate::{render::data::PrimitiveInstance, WidgetId, Widgets};
|
||||
use bytemuck::Pod;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Primitives {
|
||||
pub instances: Vec<PrimitiveInstance>,
|
||||
pub data: Vec<u32>,
|
||||
}
|
||||
|
||||
pub struct Painter<'a> {
|
||||
nodes: &'a Widgets,
|
||||
primitives: Primitives,
|
||||
pub region: UIRegion,
|
||||
}
|
||||
|
||||
/// NOTE: Self must have at least u32 alignment
|
||||
pub trait PrimitiveData: Pod {
|
||||
const DISCRIM: u32;
|
||||
}
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
pub fn new(nodes: &'a Widgets) -> Self {
|
||||
Self {
|
||||
nodes,
|
||||
primitives: Primitives::default(),
|
||||
region: UIRegion::full(),
|
||||
}
|
||||
}
|
||||
pub fn write<Data: PrimitiveData>(&mut self, data: Data) {
|
||||
let ptr = self.primitives.data.len() as u32;
|
||||
let region = self.region;
|
||||
self.primitives
|
||||
.instances
|
||||
.push(PrimitiveInstance { region, ptr });
|
||||
self.primitives.data.push(Data::DISCRIM);
|
||||
self.primitives
|
||||
.data
|
||||
.extend_from_slice(bytemuck::cast_slice::<_, u32>(&[data]));
|
||||
}
|
||||
pub fn draw(&mut self, node: &WidgetId) {
|
||||
self.nodes.get(node).draw(self);
|
||||
}
|
||||
pub fn draw_within(&mut self, node: &WidgetId, region: UIRegion) {
|
||||
let old = self.region;
|
||||
self.region.select(®ion);
|
||||
self.draw(node);
|
||||
self.region = old;
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Primitives {
|
||||
self.primitives
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
use std::ops::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Point {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
pub const fn point(x: f32, y: f32) -> Point {
|
||||
Point::new(x, y)
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn lerp(self, to: Self, amt: impl const Into<Self>) -> Self {
|
||||
let amt = amt.into();
|
||||
Self {
|
||||
x: lerp(self.x, to.x, amt.x),
|
||||
y: lerp(self.y, to.y, amt.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn lerp(x: f32, y: f32, amt: f32) -> f32 {
|
||||
(1.0 - amt) * x + y * amt
|
||||
}
|
||||
|
||||
impl const From<f32> for Point {
|
||||
fn from(v: f32) -> Self {
|
||||
Self { x: v, y: v }
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_op_inner {
|
||||
($op:ident $fn:ident $opa:ident $fna:ident) => {
|
||||
impl const $op for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn $fn(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
x: self.x.$fn(rhs.x),
|
||||
y: self.y.$fn(rhs.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl $opa for Point {
|
||||
fn $fna(&mut self, rhs: Self) {
|
||||
self.x.$fna(rhs.x);
|
||||
self.y.$fna(rhs.y);
|
||||
}
|
||||
}
|
||||
impl const $op<f32> for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn $fn(self, rhs: f32) -> Self::Output {
|
||||
Self {
|
||||
x: self.x.$fn(rhs),
|
||||
y: self.y.$fn(rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl $opa<f32> for Point {
|
||||
fn $fna(&mut self, rhs: f32) {
|
||||
self.x.$fna(rhs);
|
||||
self.y.$fna(rhs);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_op {
|
||||
($op:ident $fn:ident) => {
|
||||
impl_op_inner!($op $fn ${concat($op,Assign)} ${concat($fn,_assign)});
|
||||
};
|
||||
}
|
||||
|
||||
impl_op!(Add add);
|
||||
impl_op!(Sub sub);
|
||||
impl_op!(Mul mul);
|
||||
impl_op!(Div div);
|
||||
@@ -1,110 +0,0 @@
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@group(0) @binding(1)
|
||||
var<storage> data: array<u32>;
|
||||
|
||||
struct WindowUniform {
|
||||
dim: vec2<f32>,
|
||||
};
|
||||
|
||||
struct InstanceInput {
|
||||
@location(0) top_left_anchor: vec2<f32>,
|
||||
@location(1) top_left_offset: vec2<f32>,
|
||||
@location(2) bottom_right_anchor: vec2<f32>,
|
||||
@location(3) bottom_right_offset: vec2<f32>,
|
||||
@location(4) pointer: u32,
|
||||
}
|
||||
|
||||
struct RoundedRect {
|
||||
color: u32,
|
||||
radius: f32,
|
||||
thickness: f32,
|
||||
inner_radius: f32,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) pointer: u32,
|
||||
@location(1) top_left: vec2<f32>,
|
||||
@location(2) bot_right: vec2<f32>,
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Region {
|
||||
pos: 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 = in.top_left_anchor * window.dim + in.top_left_offset;
|
||||
let bot_right = in.bottom_right_anchor * window.dim + in.bottom_right_offset;
|
||||
let size = bot_right - top_left;
|
||||
|
||||
var pos = top_left + vec2<f32>(
|
||||
f32(vi % 2u),
|
||||
f32(vi / 2u)
|
||||
) * size;
|
||||
pos = pos / window.dim * 2.0 - 1.0;
|
||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||
out.pointer = in.pointer;
|
||||
out.top_left = top_left;
|
||||
out.bot_right = bot_right;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(
|
||||
in: VertexOutput
|
||||
) -> @location(0) vec4<f32> {
|
||||
let pos = in.clip_position.xy;
|
||||
let ty = data[in.pointer];
|
||||
let dp = in.pointer + 1u;
|
||||
let region = Region(pos, in.top_left, in.bot_right);
|
||||
switch ty {
|
||||
case 0u: {
|
||||
return draw_rounded_rect(region, RoundedRect(
|
||||
data[dp + 0u],
|
||||
bitcast<f32>(data[dp + 1u]),
|
||||
bitcast<f32>(data[dp + 2u]),
|
||||
bitcast<f32>(data[dp + 3u]),
|
||||
));
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
return vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
|
||||
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> 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<f32>(0.0, 0.0))) - radius;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
use super::Client;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct App {
|
||||
client: Option<Client>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run() {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.run_app(&mut App::default()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.client.is_none() {
|
||||
let window = event_loop
|
||||
.create_window(Window::default_attributes())
|
||||
.unwrap();
|
||||
let client = Client::new(window.into());
|
||||
self.client = Some(client);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
self.client.as_mut().unwrap().event(event, event_loop);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use app::App;
|
||||
use gui::{primitive::Axis, RoundedRect, UIColor, WidgetArrLike, WidgetArrUtil, WidgetUtil, UI};
|
||||
use render::Renderer;
|
||||
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
|
||||
|
||||
mod app;
|
||||
mod render;
|
||||
|
||||
pub fn main() {
|
||||
App::run();
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
renderer: Renderer,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let mut renderer = Renderer::new(window);
|
||||
let rect = RoundedRect {
|
||||
color: UIColor::WHITE,
|
||||
radius: 10.0,
|
||||
thickness: 0.0,
|
||||
inner_radius: 0.0,
|
||||
};
|
||||
let mut ui = UI::build();
|
||||
let blue = ui.add(rect.color(UIColor::BLUE));
|
||||
let handle = blue.handle();
|
||||
let mut ui = ui.finish(
|
||||
(
|
||||
(
|
||||
blue,
|
||||
(
|
||||
rect.color(UIColor::RED),
|
||||
(
|
||||
rect.color(UIColor::ORANGE),
|
||||
rect.color(UIColor::LIME).pad(10.0),
|
||||
)
|
||||
.span(Axis::Y, [1, 1]),
|
||||
rect.color(UIColor::YELLOW),
|
||||
)
|
||||
.span(Axis::X, [2, 2, 1])
|
||||
.pad(10),
|
||||
)
|
||||
.span(Axis::X, [1, 3]),
|
||||
rect.color(UIColor::GREEN),
|
||||
)
|
||||
.span(Axis::Y, [3, 1])
|
||||
.pad(10),
|
||||
);
|
||||
ui.widgets.get_mut(&handle).unwrap().color = UIColor::MAGENTA;
|
||||
renderer.update(&ui);
|
||||
Self { renderer }
|
||||
}
|
||||
|
||||
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => self.renderer.draw(),
|
||||
WindowEvent::Resized(size) => self.renderer.resize(&size),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use gui::{UIRenderNode, UI};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use wgpu::util::StagingBelt;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
|
||||
|
||||
pub struct Renderer {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
encoder: wgpu::CommandEncoder,
|
||||
staging_belt: StagingBelt,
|
||||
ui_node: UIRenderNode,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn update(&mut self, ui: &UI) {
|
||||
self.ui_node.update(&self.device, &self.queue, ui);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
let output = self.surface.get_current_texture().unwrap();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
let render_pass = &mut encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
self.ui_node.draw(render_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.staging_belt.finish();
|
||||
output.present();
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
|
||||
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_node.resize(size, &self.queue);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
|
||||
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Could not create window surface!");
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get adapter!");
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get device!");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let staging_belt = StagingBelt::new(4096 * 4);
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
let shape_pipeline = UIRenderNode::new(&device, &config);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
encoder,
|
||||
staging_belt,
|
||||
ui_node: shape_pipeline,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/// intentionally does not implement copy or clone
|
||||
/// which should make it harder to misuse;
|
||||
/// the idea is to generally try to guarantee all IDs
|
||||
/// point to something valid, although duplicate
|
||||
/// gets around this if needed
|
||||
#[derive(Eq, Hash, PartialEq, Debug)]
|
||||
pub struct ID(usize);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct IDTracker {
|
||||
free: Vec<ID>,
|
||||
cur: usize,
|
||||
}
|
||||
|
||||
impl IDTracker {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> ID {
|
||||
if let Some(id) = self.free.pop() {
|
||||
return id;
|
||||
}
|
||||
let id = ID(self.cur);
|
||||
self.cur += 1;
|
||||
id
|
||||
}
|
||||
|
||||
pub fn free(&mut self, id: ID) {
|
||||
self.free.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl ID {
|
||||
/// this must be used carefully to make sure
|
||||
/// all IDs are still valid references;
|
||||
/// named weirdly to indicate this.
|
||||
/// generally should not be used in "user" code
|
||||
pub fn duplicate(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mod id;
|
||||
|
||||
pub use id::*;
|
||||
Whitespace-only changes.
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user