147 Commits

Author SHA1 Message Date
7e257fd042 make shaping a const 2025-11-17 21:38:40 -05:00
c7b255be4f fix full redraw modules not cleaning up 2025-11-17 21:11:35 -05:00
955e6b7588 ptr 2025-11-17 18:33:03 -05:00
f5f4547537 add readme 2025-11-17 17:01:04 -05:00
3425eb7b80 finisth editing todo 2025-11-17 16:39:59 -05:00
681efe1e2b edit todo 2025-11-17 16:39:09 -05:00
ef448ec870 fix awful desired size cache 2025-11-17 16:30:25 -05:00
f74c4dc6e2 perf 2025-11-17 16:08:29 -05:00
b3d0dc3871 more retained size fixes 2025-11-17 14:11:33 -05:00
b6ece4a5ee back to retained... 2025-11-17 13:55:49 -05:00
2914d7968f IP 2025-11-15 02:22:50 -05:00
8896c64445 update imports 😂 2025-11-15 02:10:12 -05:00
bd0805dbac rename repo to iris + z offset 2025-11-15 01:01:18 -05:00
ed87b7c336 arst 2025-11-14 15:42:08 -05:00
448f348356 remove debug prints 2025-11-14 14:44:16 -05:00
182b1d4729 add detail on what the problem is in todo 2025-11-14 14:43:36 -05:00
b4947db850 I give up on retained for now lmao 2025-11-14 14:39:08 -05:00
218b3f14ed app work? 2025-11-14 13:49:01 -05:00
e2690fa611 span builder 2025-11-13 16:59:31 -05:00
125fca4075 rename spacing to gap 2025-11-13 14:29:03 -05:00
73afea8c35 switch to element defined span lens + better size fn 2025-11-13 14:27:31 -05:00
8755c04feb sort of bandaid patch over resizing 2025-11-11 14:45:06 -05:00
afabdc52a2 app work 2025-11-11 14:34:56 -05:00
deaf730901 app work 2025-11-11 13:55:36 -05:00
92db1264a6 beginning actual app 2025-11-11 01:35:59 -05:00
379eec771a stuff 2025-11-10 22:14:27 -05:00
ebff93bec9 new const trait syntax 2025-11-10 22:10:38 -05:00
1c49db1b89 initial mask impl 2025-11-10 14:45:22 -05:00
5c2022396a update todo 2025-09-29 14:54:47 -04:00
db0d11cacb ing prefix gives off bad vibes 2025-09-29 14:01:34 -04:00
337af3e18c positioning dir in core 2025-09-29 14:00:54 -04:00
628840d5cd text update for more code reuse + much better caching 2025-09-29 13:45:48 -04:00
c98a43f94d update px dependent on resize + move painter data into struct in ui 2025-09-28 13:14:51 -04:00
61df088cc7 initial text wrapping impl (resizing will break) 2025-09-28 01:32:10 -04:00
b2950566af add axis to flip so spans w negative sign work correctly 2025-09-27 23:47:42 -04:00
dc9340b26c renaming & comments 2025-09-27 22:16:42 -04:00
8afe2c68e8 add scroll fn to traits 2025-09-27 21:32:27 -04:00
5445008528 add offset / scrolling + clipboard support 2025-09-27 21:13:00 -04:00
95f049acb4 move widgets on draw if region size is same 2025-09-27 16:11:30 -04:00
5f2dffc189 unleash the sizes 2025-09-25 21:30:26 -04:00
06cfeaac6b no branching allowed 2025-09-25 21:19:47 -04:00
6d829dbe81 stack & padding fix sorta, preparing for scroll areas 2025-09-25 19:59:18 -04:00
273a92d1f7 decide it's better to leave them separate 2025-09-25 14:32:20 -04:00
552d66d90f move ctx in event to be on module so run event and stuff can be used easily 2025-09-25 13:59:39 -04:00
fe42092556 jugando 2025-09-25 13:00:06 -04:00
cfd5cda0b2 clean up a bit 2025-09-25 12:43:11 -04:00
21f15fb9c5 event system!!! 2025-09-25 12:37:06 -04:00
4deeabe611 stop doing option transmuting bruh 2025-09-25 00:39:22 -04:00
51f9908103 specify generics for transmute 2025-09-25 00:36:59 -04:00
6e5cce2617 safety comment formatting 2025-09-25 00:35:20 -04:00
055aaf757c HEHEHAW (fixes last commit which panics cause of unsafe UB) 2025-09-25 00:30:00 -04:00
b14aafca30 indices iterator for layers 2025-09-25 00:26:02 -04:00
8829878f2e sanity 2025-09-25 00:07:53 -04:00
57bfd2d348 make layer iter reversible 2025-09-25 00:04:01 -04:00
443e13f094 make run sensors sane and adjust on_edit to just use ui as ctx (so two run calls needed) 2025-09-24 22:46:55 -04:00
3463682d62 delete old run_sensors 2025-09-24 17:42:31 -04:00
719bee4b31 remove context from ui (again) and create weird trait for it 2025-09-24 17:41:25 -04:00
26c248dcba add module system and move sensor into core with it 2025-09-24 16:11:39 -04:00
2adf7a43a1 preload text by default 2025-09-24 12:33:02 -04:00
70d3027bfb move widgets out of ui 2025-09-21 17:51:10 -04:00
c1f0b16f20 switch to fxhash 2025-09-21 16:27:36 -04:00
bc9a273831 name lol 2025-09-20 19:55:29 -04:00
01cec31da0 add darken and brighten color fns 2025-09-20 17:30:53 -04:00
20b044865c we love post fix 2025-09-20 13:42:47 -04:00
3653f24e06 store color in linear 2025-09-20 13:34:04 -04:00
e35e72402f add info back in 2025-09-20 13:09:18 -04:00
949c9df0a0 cache text buf 2025-09-20 12:49:55 -04:00
2d7484a631 prev isn't used atm 2025-09-20 02:00:08 -04:00
fee03fddc8 sensors are now normal 2025-09-20 01:46:55 -04:00
8ecd8bb171 layers initial impl (no sensors) 2025-09-20 00:50:58 -04:00
7651699743 actually use drawing 2025-09-17 12:52:08 -04:00
e880acca66 clear textures in remove so not needed outside 2025-09-17 12:49:41 -04:00
1162ba4c10 Option<Id>.duplicate 2025-09-16 17:34:19 -04:00
f9097807a2 sizing actually working correctly now 2025-09-16 17:31:54 -04:00
b48acccb8d sense specific buttons 2025-09-15 22:22:52 -04:00
21aa2b3501 remove not hovering lol 2025-09-15 21:23:06 -04:00
90cbc2524a sensors now run in correct order 2025-09-15 21:13:23 -04:00
2700c31c13 cursor finally working properly and removed from render_text 2025-09-15 20:30:26 -04:00
9d659b6afd actually use the text library for text editing (fully working I think but code isn't cleanest) 2025-09-15 14:34:57 -04:00
e9853120ce sort of fix text editing (better but still bad) 2025-09-11 17:12:11 -04:00
242c3b992e IDC FINALLY OH MY GOD (I think like ctx + resize propagation + some other stuff) 2025-09-11 00:59:26 -04:00
709a2d0e17 preparation 2025-09-09 21:53:32 -04:00
15cc91d92a update todo 2025-09-07 23:45:24 -04:00
2b5965e2e9 add to todo 2025-09-07 23:44:55 -04:00
09f4de619e better & more fine grained redraw system (should allow movement) 2025-09-07 23:33:36 -04:00
d4690401eb rename widget fn macros 2025-08-29 00:11:41 -04:00
42f5a8d01b btext 2025-08-28 22:58:01 -04:00
4b1ee21e94 text new fn 2025-08-28 22:51:58 -04:00
55bee4b25e rect fn 2025-08-28 22:49:58 -04:00
3df76d926c rename a bit 2025-08-28 22:21:43 -04:00
97f2f67dee small alignment test 2025-08-28 21:57:46 -04:00
1204e3728e alignment!!! 2025-08-28 21:55:34 -04:00
46c7d8ba26 maybe fix relative len for sized span 2025-08-28 18:28:14 -04:00
a0e6623abe sized spans! 2025-08-28 18:25:59 -04:00
d7d67e4ed3 more test stuff 2025-08-28 01:52:00 -04:00
28935e33e9 clean up 2025-08-28 01:36:26 -04:00
834182ffe8 sized widgets! 2025-08-28 01:35:43 -04:00
d4d0b3b580 testing stuff 2025-08-26 01:57:32 -04:00
d0bed07ee0 more testing stuff 2025-08-25 23:13:29 -04:00
e85b503127 testing stuff 2025-08-25 23:11:46 -04:00
94a3ba5837 make name longer 😔 2025-08-25 22:51:33 -04:00
9780724126 senses are now bitflags 2025-08-25 22:36:38 -04:00
e9037cdc14 contextless gaming 2025-08-25 19:21:39 -04:00
e8b255c8f9 remove context generic 2025-08-25 18:53:21 -04:00
d4b1a56467 comments 2025-08-25 16:30:06 -04:00
325e13c01f auto generate label (TODO: should be moved into widgets now that all have one) 2025-08-25 16:23:34 -04:00
7b21b0714d static ids 2025-08-25 16:12:49 -04:00
41103f2732 comments 2025-08-25 14:21:25 -04:00
9e751d4161 clean up 2025-08-24 22:44:21 -04:00
880d7eca50 clean up after text fix 2025-08-24 22:40:33 -04:00
5cb84047b9 text fix? 2025-08-24 22:40:12 -04:00
8f02a358a4 fix view count 2025-08-24 22:31:51 -04:00
44a8b1cbeb fix view leak and add view count 2025-08-24 22:13:02 -04:00
74d01d14d4 fix reactivity 😭 + visual widget counter 2025-08-24 22:02:50 -04:00
6bb6db32a6 REACTIVITY 2025-08-24 20:34:19 -04:00
50ccf7393d snap text on shader 2025-08-23 22:16:00 -04:00
5ce6fca275 initial text impl 2025-08-23 21:15:39 -04:00
abcbc267b5 I forgot why I did it the other way lol (revert) 2025-08-23 15:39:43 -04:00
2ffb09bef0 made painter actually how I wanted it (draw now takes in an owned painter) 2025-08-23 15:20:25 -04:00
6fbdf9fbc8 texture freeing + render updates done a bit nicer 2025-08-23 13:02:00 -04:00
5fe63e311c update todo 2025-08-22 23:10:32 -04:00
7dbdcbba42 added underdeveloped but working image support (no freeing or samplers) 2025-08-22 23:07:31 -04:00
bde929b05a typed primitive buffers + macro for creation 2025-08-21 19:37:50 -04:00
b7f83b58a9 sensor ctx 2025-08-20 13:09:03 -04:00
1482e5d67c comments 2025-08-20 12:18:44 -04:00
368826fe05 refcount ids and delete unused 2025-08-16 19:15:21 -04:00
b2acbcc189 oops 2025-08-16 14:43:36 -04:00
b0fe7310eb please rust analyzer with macros 2025-08-16 02:34:44 -04:00
166394d8d9 remove comment 2025-08-16 01:53:02 -04:00
dd39db847c bruh rust analyzer sucks 2025-08-16 01:52:44 -04:00
11188f2951 actually sane sensor handling 2025-08-16 00:56:37 -04:00
f4aef3a983 idek stuff like stack 2025-08-15 22:59:58 -04:00
a7dfacb83e REAL SENSORS 2025-08-15 21:42:35 -04:00
9f1802f497 clean up buttons 2025-08-15 16:22:28 -04:00
78ea738b8e SENSORS 2025-08-15 15:48:00 -04:00
c5aa0a02e2 clean up 2025-08-14 15:05:55 -04:00
e41970287d TAG TECHNOLOGY 2025-08-14 12:21:26 -04:00
4d68fa476d stuff 2025-08-13 03:15:50 -04:00
9e80a32a4b convert rest of base 2025-08-13 02:10:53 -04:00
f4975df57b widget fn ret macro 2025-08-13 02:07:35 -04:00
c7e3225c5f gaming 2025-08-13 01:35:09 -04:00
23a5ccd05e span direction (sign) now works 2025-08-11 02:41:14 -04:00
fa930180c1 spans now good (other than direction) + refactor 2025-08-11 02:26:28 -04:00
95a07786bb spans now good (other than direction) + refactor 2025-08-11 02:24:27 -04:00
132113f09e center w size 2025-08-10 20:40:14 -04:00
f2cbf90d1d center anchors on 0 0 2025-08-10 20:29:16 -04:00
848347e6b3 clean up 2025-08-10 19:10:26 -04:00
66 changed files with 7434 additions and 1226 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
perf.data*

1842
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,18 @@
[package]
name = "gui"
name = "iris"
version = "0.1.0"
edition = "2021"
edition = "2024"
# 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"
winit = "0.30.12"
wgpu = "27.0.1"
bytemuck = "1.23.1"
image = "0.25.6"
cosmic-text = "0.15.0"
unicode-segmentation = "1.12.0"
fxhash = "0.2.1"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }

36
TODO Normal file
View File

@@ -0,0 +1,36 @@
images
settings (sampler)
text
figure out ways to speed up / what costs the most
resizing (per frame) is really slow (assuming painter isn't griefing)
bug where x offset doesn't shift texture correctly (eg typing a vs j)
bug (?) where if you spam a key it slows down at a certain point, seems to be line based
masks r just made to bare minimum work
scaling
could be just a simple scaling factor that multiplies abs
and need to ensure text uses raw abs and not scaled abs
naming? (pt, px)
want to keep (drawn) regions using px? or should I add another field to UiScalar/Vec
field could be best solution so redrawing stuff isn't needed & you can specify both as user
WidgetRef<W> or smth instead of Id
enum that's either an Id or an actual concrete instance of W
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
really weird limitation:
I don't think you can currently remove an element from a parent and put it in a child of the same parent
because it removes the unused children after the entire parent redraw
but the child gets drawn during that, so it will think the child is still active !!!
or something like that idk, maybe I need a special enum for parent that includes a undecided state where it may or may not get redrawn by the parent
or just do ref counting and ensure all drawn things == 1 afterwards (seems like best way)
tags
vecs for each widget type?

26
readme.md Normal file
View File

@@ -0,0 +1,26 @@
# iris
my take on a rust ui library (also my first ui library)
it's called iris because it's the structure around what you actually want to display and colorful
there's a `main.rs` that runs a testing window, so can just `cargo run` to see it working
goals, in general order:
1. does what I want it to (video, text, animations)
2. very easy to use ignoring ergonomic ref counting
3. reasonably fast / efficient (a lot faster than electron, save battery life)
not targeting web rn cause wanna use actual nice gpu features & entire point of this is to make desktop apps
general ideas trynna use rn / experiment with:
- retained mode
- specifically designed around wgpu
- postfix functions for most things to prevent unreadable indentation (going very well)
- 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, check TODO for that maybe
sizable chance it gets a rewrite once I know everything I need and what seems to work best

View File

@@ -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 {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Widget = Regioned>;
}
impl<W: WidgetLike> WidgetUtil for W {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Widget = Regioned> {
WidgetFn(|ui| Regioned {
region: padding.into().region(),
inner: self.add(ui).erase_type(),
})
}
}
pub trait WidgetArrUtil<const LEN: usize> {
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Widget = Span>;
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN>> WidgetArrUtil<LEN> for Wa {
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Widget = 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
src/core/event.rs Normal file
View File

51
src/core/image.rs Normal file
View File

@@ -0,0 +1,51 @@
use crate::prelude::*;
use image::DynamicImage;
pub struct Image {
handle: TextureHandle,
}
impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) {
painter.texture(&self.handle);
}
fn desired_size(&mut self, _: &mut SizeCtx) -> Size {
Size::abs(self.handle.size())
}
}
pub fn image(image: impl LoadableImage) -> impl WidgetFn<Image> {
let image = image.get_image().expect("Failed to load image");
move |ui| Image {
handle: ui.add_texture(image),
}
}
pub trait LoadableImage {
fn get_image(self) -> Result<DynamicImage, String>;
}
impl LoadableImage for &str {
fn get_image(self) -> Result<DynamicImage, String> {
image::open(self).map_err(|e| format!("{e:?}"))
}
}
impl LoadableImage for String {
fn get_image(self) -> Result<DynamicImage, String> {
image::open(self).map_err(|e| format!("{e:?}"))
}
}
impl<const LEN: usize> LoadableImage for &[u8; LEN] {
fn get_image(self) -> Result<DynamicImage, String> {
image::load_from_memory(self).map_err(|e| format!("{e:?}"))
}
}
impl LoadableImage for DynamicImage {
fn get_image(self) -> Result<DynamicImage, String> {
Ok(self)
}
}

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

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

17
src/core/mod.rs Normal file
View File

@@ -0,0 +1,17 @@
mod image;
mod mask;
mod position;
mod ptr;
mod rect;
mod sense;
mod text;
mod trait_fns;
pub use image::*;
pub use mask::*;
pub use position::*;
pub use ptr::*;
pub use rect::*;
pub use sense::*;
pub use text::*;
pub use trait_fns::*;

View File

@@ -0,0 +1,17 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: WidgetId,
pub align: Align,
}
impl Widget for Aligned {
fn draw(&mut self, painter: &mut Painter) {
let region = UiRegion::from_ui_size_align(painter.region_size(&self.inner), self.align);
painter.widget_within(&self.inner, region);
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
ctx.size(&self.inner)
}
}

13
src/core/position/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
mod align;
mod offset;
mod pad;
mod sized;
mod span;
mod stack;
pub use align::*;
pub use offset::*;
pub use pad::*;
pub use sized::*;
pub use span::*;
pub use stack::*;

View File

@@ -0,0 +1,17 @@
use crate::prelude::*;
pub struct Offset {
pub inner: WidgetId,
pub amt: UiVec2,
}
impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) {
let region = UiRegion::full().offset(self.amt);
painter.widget_within(&self.inner, region);
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
ctx.size(&self.inner)
}
}

74
src/core/position/pad.rs Normal file
View File

@@ -0,0 +1,74 @@
use crate::prelude::*;
pub struct Pad {
pub padding: Padding,
pub inner: WidgetId,
}
impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) {
painter.widget_within(&self.inner, self.padding.region());
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
let mut size = ctx.size(&self.inner);
if size.x.rest == 0.0 {
size.x.abs += self.padding.left + self.padding.right;
}
if size.y.rest == 0.0 {
size.y.abs += self.padding.top + self.padding.bottom;
}
size
}
}
pub struct Padding {
pub left: f32,
pub right: f32,
pub top: f32,
pub bottom: f32,
}
impl Padding {
pub fn uniform(amt: impl UiNum) -> Self {
let amt = amt.to_f32();
Self {
left: amt,
right: amt,
top: amt,
bottom: amt,
}
}
pub fn region(&self) -> UiRegion {
let mut region = UiRegion::full();
region.top_left.abs.x += self.left;
region.top_left.abs.y += self.top;
region.bot_right.abs.x -= self.right;
region.bot_right.abs.y -= self.bottom;
region
}
pub fn x(amt: impl UiNum) -> Self {
let amt = amt.to_f32();
Self {
left: amt,
right: amt,
top: 0.0,
bottom: 0.0,
}
}
pub fn y(amt: impl UiNum) -> Self {
let amt = amt.to_f32();
Self {
left: 0.0,
right: 0.0,
top: amt,
bottom: amt,
}
}
}
impl<T: UiNum> From<T> for Padding {
fn from(amt: T) -> Self {
Self::uniform(amt.to_f32())
}
}

View File

@@ -0,0 +1,27 @@
use crate::prelude::*;
pub struct Sized {
pub inner: WidgetId,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for Sized {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.inner);
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
let rel = ctx.size.rel;
if let Some(x) = self.x {
ctx.size.axis_mut(Axis::X).set(x.apply_rest(rel.x));
}
if let Some(y) = self.y {
ctx.size.axis_mut(Axis::Y).set(y.apply_rest(rel.y));
}
Size {
x: self.x.unwrap_or_else(|| ctx.size(&self.inner).x),
y: self.y.unwrap_or_else(|| ctx.size(&self.inner).y),
}
}
}

116
src/core/position/span.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::marker::PhantomData;
use crate::prelude::*;
pub struct Span {
pub children: Vec<WidgetId>,
pub dir: Dir,
pub gap: f32,
}
impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) {
let total = self.len_sum(&mut painter.size_ctx());
let mut start = UiScalar::rel_min();
for child in &self.children {
let mut child_region = UiRegion::full();
let mut axis = child_region.axis_mut(self.dir.axis);
axis.top_left.set(start);
let len = painter.size(child).axis(self.dir.axis);
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::from_anchor(len.rest / total.rest);
start = rel_end.within(start, (UiScalar::rel_max() + start) - offset);
}
start.abs += len.abs;
start.rel += len.rel;
axis.bot_right.set(start);
if self.dir.sign == Sign::Neg {
child_region.flip(self.dir.axis);
}
painter.widget_within(child, child_region);
start.abs += self.gap;
}
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
let mut sums = self.len_sum(ctx);
let dir_len = if sums.rest == 0.0 && sums.rel == 0.0 {
sums.abs += self.gap * self.children.len().saturating_sub(1) as f32;
sums
} else {
Len::default()
};
let mut max_ortho = Len::ZERO;
for child in &self.children {
let len = ctx.size(child).axis(!self.dir.axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if len.rel > 0.0 || len.rest > 0.0 {
max_ortho.rest = 1.0;
max_ortho.abs = 0.0;
break;
}
max_ortho.abs = max_ortho.abs.max(len.abs);
}
Size::from_axis(self.dir.axis, dir_len, max_ortho)
}
}
impl Span {
pub fn empty(dir: Dir) -> Self {
Self {
children: Vec::new(),
dir,
gap: 0.0,
}
}
pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = gap.to_f32();
self
}
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
self.children.iter_mut().fold(Len::ZERO, |mut s, id| {
s += ctx.size(id).axis(self.dir.axis);
s
})
}
}
pub struct SpanBuilder<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> {
pub children: Wa,
pub dir: Dir,
pub gap: f32,
_pd: PhantomData<Tag>,
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> FnOnce<(&mut Ui,)>
for SpanBuilder<LEN, Wa, Tag>
{
type Output = Span;
extern "rust-call" fn call_once(self, args: (&mut Ui,)) -> Self::Output {
Span {
children: self.children.ui(args.0).arr.to_vec(),
dir: self.dir,
gap: self.gap,
}
}
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> SpanBuilder<LEN, Wa, Tag> {
pub fn new(children: Wa, dir: Dir) -> Self {
Self {
children,
dir,
gap: 0.0,
_pd: PhantomData,
}
}
pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = gap.to_f32();
self
}
}

View File

@@ -0,0 +1,82 @@
use std::marker::PhantomData;
use crate::prelude::*;
pub struct Stack {
pub children: Vec<WidgetId>,
pub size: StackSize,
pub offset: usize,
}
impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset {
painter.next_layer();
}
let mut iter = self.children.iter();
if let Some(child) = iter.next() {
painter.child_layer();
painter.widget(child);
}
for child in iter {
painter.next_layer();
painter.widget(child);
}
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
match self.size {
StackSize::Default => Size::default(),
StackSize::Child(i) => ctx.size(&self.children[i]),
}
}
}
#[derive(Default, Debug)]
pub enum StackSize {
#[default]
Default,
Child(usize),
}
pub struct StackBuilder<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> {
pub children: Wa,
pub size: StackSize,
pub offset: usize,
_pd: PhantomData<Tag>,
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> FnOnce<(&mut Ui,)>
for StackBuilder<LEN, Wa, Tag>
{
type Output = Stack;
extern "rust-call" fn call_once(self, args: (&mut Ui,)) -> Self::Output {
Stack {
children: self.children.ui(args.0).arr.to_vec(),
offset: self.offset,
size: self.size,
}
}
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> StackBuilder<LEN, Wa, Tag> {
pub fn new(children: Wa) -> Self {
Self {
children,
size: StackSize::default(),
offset: 0,
_pd: PhantomData,
}
}
pub fn size(mut self, size: StackSize) -> Self {
self.size = size;
self
}
pub fn offset_layer(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
}

22
src/core/ptr.rs Normal file
View File

@@ -0,0 +1,22 @@
use crate::prelude::*;
#[derive(Default)]
pub struct WidgetPtr {
pub inner: Option<WidgetId>,
}
impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) {
if let Some(id) = &self.inner {
painter.widget(id);
}
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
if let Some(id) = &self.inner {
ctx.size(id)
} else {
Size::ZERO
}
}
}

43
src/core/rect.rs Normal file
View File

@@ -0,0 +1,43 @@
use crate::prelude::*;
#[derive(Clone, Copy)]
pub struct Rect {
pub color: UiColor,
pub radius: f32,
pub thickness: f32,
pub inner_radius: f32,
}
impl Rect {
pub fn new(color: UiColor) -> Self {
Self {
color,
radius: 0.0,
inner_radius: 0.0,
thickness: 0.0,
}
}
pub fn color(mut self, color: UiColor) -> Self {
self.color = color;
self
}
pub fn radius(mut self, radius: impl UiNum) -> Self {
self.radius = radius.to_f32();
self
}
}
impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive {
color: self.color,
radius: self.radius,
thickness: self.thickness,
inner_radius: self.inner_radius,
});
}
}
pub fn rect(color: UiColor) -> Rect {
Rect::new(color)
}

372
src/core/sense.rs Normal file
View File

@@ -0,0 +1,372 @@
use crate::prelude::*;
use std::{
ops::{BitOr, Deref, DerefMut},
rc::Rc,
};
use crate::{
layout::{UiModule, UiRegion, Vec2},
util::{HashMap, Id},
};
#[derive(PartialEq)]
pub enum Button {
Left,
Right,
Middle,
}
#[derive(PartialEq)]
pub enum CursorSense {
PressStart(Button),
Pressing(Button),
PressEnd(Button),
HoverStart,
Hovering,
HoverEnd,
Scroll,
}
pub struct CursorSenses(Vec<CursorSense>);
impl CursorSense {
pub fn click() -> Self {
Self::PressStart(Button::Left)
}
pub fn unclick() -> Self {
Self::PressEnd(Button::Left)
}
}
#[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: &Button) -> &ActivationState {
match button {
Button::Left => &self.left,
Button::Right => &self.right,
Button::Middle => &self.middle,
}
}
pub fn end_frame(&mut self) {
self.left.end_frame();
self.middle.end_frame();
self.right.end_frame();
}
}
impl CursorState {
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, Data> {
pub senses: CursorSenses,
pub f: Rc<dyn EventFn<Ctx, Data>>,
}
pub type SensorMap<Ctx, Data> = HashMap<Id, SensorGroup<Ctx, Data>>;
pub type SenseShape = UiRegion;
pub struct SensorGroup<Ctx, Data> {
pub hover: ActivationState,
pub sensors: Vec<Sensor<Ctx, Data>>,
}
#[derive(Clone)]
pub struct CursorData {
pub cursor: Vec2,
pub size: Vec2,
pub scroll_delta: Vec2,
}
pub struct CursorModule<Ctx> {
map: SensorMap<Ctx, CursorData>,
active: HashMap<usize, HashMap<Id, SenseShape>>,
}
impl<Ctx: 'static> UiModule for CursorModule<Ctx> {
fn on_draw(&mut self, inst: &WidgetInstance) {
if self.map.contains_key(&inst.id) {
self.active
.entry(inst.layer)
.or_default()
.insert(inst.id, inst.region);
}
}
fn on_undraw(&mut self, inst: &WidgetInstance) {
if let Some(layer) = self.active.get_mut(&inst.layer) {
layer.remove(&inst.id);
}
}
fn on_remove(&mut self, id: &Id) {
self.map.remove(id);
for layer in self.active.values_mut() {
layer.remove(id);
}
}
fn on_move(&mut self, inst: &WidgetInstance) {
if let Some(map) = self.active.get_mut(&inst.layer)
&& let Some(region) = map.get_mut(&inst.id)
{
*region = inst.region;
}
}
}
impl<Ctx> CursorModule<Ctx> {
pub fn merge(&mut self, other: Self) {
for (id, group) in other.map {
for sensor in group.sensors {
self.map.entry(id).or_default().sensors.push(sensor);
}
}
}
}
pub trait SensorCtx: UiCtx {
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2);
}
impl<Ctx: UiCtx + 'static> SensorCtx for Ctx {
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) {
CursorModule::<Ctx>::run(self, cursor, window_size);
}
}
impl<Ctx: UiCtx + 'static> CursorModule<Ctx> {
pub fn run(ctx: &mut Ctx, cursor: &CursorState, window_size: Vec2) {
let layers = std::mem::take(&mut ctx.ui().data.layers);
let mut module = std::mem::take(ctx.ui().data.modules.get_mut::<Self>());
for i in layers.indices().rev() {
let Some(list) = module.active.get_mut(&i) else {
continue;
};
let mut sensed = false;
for (id, shape) in list.iter() {
let group = module.map.get_mut(id).unwrap();
let region = shape.to_screen(window_size);
let in_shape = cursor.exists && region.contains(cursor.pos);
group.hover.update(in_shape);
if group.hover == ActivationState::Off {
continue;
}
sensed = true;
for sensor in &mut group.sensors {
if should_run(&sensor.senses, cursor, group.hover) {
let data = CursorData {
cursor: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
};
(sensor.f)(ctx, data);
}
}
}
if sensed {
break;
}
}
let ui_mod = ctx.ui().data.modules.get_mut::<Self>();
std::mem::swap(ui_mod, &mut module);
ui_mod.merge(module);
ctx.ui().data.layers = layers;
}
}
pub fn should_run(senses: &CursorSenses, cursor: &CursorState, hover: ActivationState) -> bool {
for sense in senses.iter() {
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 true;
}
}
false
}
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 Event for CursorSenses {
type Module<Ctx: 'static> = CursorModule<Ctx>;
type Data = CursorData;
}
impl Event for CursorSense {
type Module<Ctx: 'static> = CursorModule<Ctx>;
type Data = CursorData;
}
impl<E: Event<Data = <CursorSenses as Event>::Data> + Into<CursorSenses>, Ctx: 'static> EventModule<E, Ctx>
for CursorModule<Ctx>
{
fn register(&mut self, id: Id, senses: E, f: impl EventFn<Ctx, <E as Event>::Data>) {
// TODO: does not add to active if currently active
self.map.entry(id).or_default().sensors.push(Sensor {
senses: senses.into(),
f: Rc::new(f),
});
}
fn run<'a>(&self, id: &Id, event: E) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, E, Ctx>> {
let senses = event.into();
if let Some(group) = self.map.get(id) {
let fs: Vec<_> = group
.sensors
.iter()
.filter_map(|sensor| {
if sensor.senses.iter().any(|s| senses.contains(s)) {
Some(sensor.f.clone())
} else {
None
}
})
.collect();
Some(move |ctx: &mut Ctx, data: CursorData| {
for f in &fs {
f(ctx, data.clone());
}
})
} else {
None
}
}
}
impl<Ctx, Data> Default for SensorGroup<Ctx, Data> {
fn default() -> Self {
Self {
hover: Default::default(),
sensors: Default::default(),
}
}
}
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
}
}
impl<Ctx> Default for CursorModule<Ctx> {
fn default() -> Self {
Self {
map: Default::default(),
active: Default::default(),
}
}
}

108
src/core/text/build.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::marker::{PhantomData, Sized};
use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics, Shaping};
pub trait TextBuilderOutput: Sized {
type Output;
fn run(ui: &mut Ui, builder: TextBuilder<Self>) -> Self::Output;
}
pub struct TextBuilder<O = TextOutput> {
pub content: String,
pub attrs: TextAttrs,
_pd: PhantomData<O>,
}
impl<O> TextBuilder<O> {
pub fn size(mut self, size: impl UiNum) -> Self {
self.attrs.font_size = size.to_f32();
self.attrs.line_height = self.attrs.font_size * 1.1;
self
}
pub fn color(mut self, color: UiColor) -> Self {
self.attrs.color = color;
self
}
pub fn family(mut self, family: Family<'static>) -> Self {
self.attrs.family = family;
self
}
pub fn line_height(mut self, height: f32) -> Self {
self.attrs.line_height = height;
self
}
pub fn text_align(mut self, align: Align) -> Self {
self.attrs.align = align;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.attrs.wrap = wrap;
self
}
pub fn editable(self) -> TextBuilder<TextEditOutput> {
TextBuilder {
content: self.content,
attrs: self.attrs,
_pd: PhantomData,
}
}
}
pub struct TextOutput;
impl TextBuilderOutput for TextOutput {
type Output = Text;
fn run(ui: &mut Ui, builder: TextBuilder<Self>) -> Self::Output {
let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let font_system = &mut ui.data.text.font_system;
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
let mut text = Text {
content: builder.content.into(),
view: TextView::new(buf, builder.attrs),
};
text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text
}
}
pub struct TextEditOutput;
impl TextBuilderOutput for TextEditOutput {
type Output = TextEdit;
fn run(ui: &mut Ui, builder: TextBuilder<Self>) -> Self::Output {
let buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit {
view: TextView::new(buf, builder.attrs),
cursor: None,
};
let font_system = &mut ui.data.text.font_system;
text.buf
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
builder.attrs.apply(font_system, &mut text.buf, None);
text
}
}
impl<O: TextBuilderOutput> FnOnce<(&mut Ui,)> for TextBuilder<O> {
type Output = O::Output;
extern "rust-call" fn call_once(self, args: (&mut Ui,)) -> Self::Output {
O::run(args.0, self)
}
}
pub fn text(content: impl Into<String>) -> TextBuilder {
TextBuilder {
content: content.into(),
attrs: TextAttrs::default(),
_pd: PhantomData,
}
}

278
src/core/text/edit.rs Normal file
View File

@@ -0,0 +1,278 @@
use std::ops::{Deref, DerefMut};
use crate::prelude::*;
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, Motion, Shaping};
use unicode_segmentation::UnicodeSegmentation;
use winit::{
event::KeyEvent,
keyboard::{Key, NamedKey},
};
pub struct TextEdit {
pub(super) view: TextView,
pub(super) cursor: Option<Cursor>,
}
impl TextEdit {
pub fn region(&self) -> UiRegion {
UiRegion::from_size_align(
self.tex().map(|t| t.size()).unwrap_or(Vec2::ZERO),
self.align,
)
}
pub fn content(&self) -> String {
self.buf
.lines
.iter()
.map(|l| l.text())
// why is this needed?? what??
.collect::<Vec<_>>()
.join("\n")
}
}
impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) {
let tex = self.view.draw(&mut painter.size_ctx());
let region = text_region(&tex, self.align);
painter.texture_within(&tex.handle, region);
if let Some(cursor) = &self.cursor
&& let Some(offset) = cursor_pos(cursor, &self.buf)
{
let size = vec2(1, self.attrs.line_height);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
UiRegion::from_size_align(size, Align::TopLeft)
.offset(offset)
.within(&region),
);
}
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
Size::abs(self.view.draw(ctx).size())
}
}
/// copied & modified from fn found in Editor in cosmic_text
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some((run.line_w, run.line_top));
for glyph in run.glyphs.iter() {
if cursor.index == glyph.start {
return Some((glyph.x, run.line_top));
} else if cursor.index > glyph.start && cursor.index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < cursor.index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some((glyph.x + offset, run.line_top));
}
}
}
prev
}
pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit,
pub font_system: &'a mut FontSystem,
}
impl<'a> TextEditCtx<'a> {
pub fn take(&mut self) -> String {
let text = self
.text
.buf
.lines
.drain(..)
.map(|l| l.into_text())
.collect::<Vec<_>>()
.join("\n");
self.text
.buf
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
if let Some(cursor) = &mut self.text.cursor {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
text
}
pub fn motion(&mut self, motion: Motion) {
if let Some(cursor) = self.text.cursor
&& let Some((cursor, _)) =
self.text
.buf
.cursor_motion(self.font_system, cursor, None, motion)
{
self.text.cursor = Some(cursor);
}
}
pub fn insert(&mut self, text: &str) {
let mut lines = text.split('\n');
let Some(first) = lines.next() else {
return;
};
self.insert_inner(first);
for line in lines {
self.newline();
self.insert_inner(line);
}
}
fn insert_inner(&mut self, text: &str) {
if let Some(cursor) = &mut self.text.cursor {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
line.set_text(line_text, line.ending(), line.attrs_list().clone());
for _ in 0..text.len() {
self.motion(Motion::Right);
}
}
}
pub fn newline(&mut self) {
if let Some(cursor) = &mut self.text.cursor {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
cursor.line += 1;
lines.insert(cursor.line, new);
cursor.index = 0;
}
}
pub fn backspace(&mut self) {
if let Some(cursor) = &mut self.text.cursor
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(Motion::Left);
self.delete();
}
}
pub fn delete(&mut self) {
if let Some(cursor) = &mut self.text.cursor {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
if cursor.line == lines.len() - 1 {
return;
}
let add = lines.remove(cursor.line + 1).into_text();
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
line.set_text(cur, line.ending(), line.attrs_list().clone());
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
line.set_text(text, line.ending(), line.attrs_list().clone());
}
}
}
pub fn select(&mut self, pos: Vec2, size: Vec2) {
let pos = pos - self.text.region().top_left.to_abs(size);
self.text.cursor = self.text.buf.hit(pos.x, pos.y);
}
pub fn deselect(&mut self) {
self.text.cursor = None;
}
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
match &event.logical_key {
Key::Named(named) => match named {
NamedKey::Backspace => self.backspace(),
NamedKey::Delete => self.delete(),
NamedKey::Space => self.insert(" "),
NamedKey::Enter => {
if modifiers.shift {
self.newline();
} else {
return TextInputResult::Submit;
}
}
NamedKey::ArrowRight => self.motion(Motion::Right),
NamedKey::ArrowLeft => self.motion(Motion::Left),
NamedKey::ArrowUp => self.motion(Motion::Up),
NamedKey::ArrowDown => self.motion(Motion::Down),
NamedKey::Escape => {
self.deselect();
return TextInputResult::Unfocus;
}
_ => return TextInputResult::Unused,
},
Key::Character(text) => {
if modifiers.control && text == "v" {
return TextInputResult::Paste;
} else {
self.insert(text)
}
}
_ => return TextInputResult::Unused,
}
TextInputResult::Used
}
}
#[derive(Default)]
pub struct Modifiers {
pub shift: bool,
pub control: bool,
}
impl Modifiers {
pub fn clear(&mut self) {
self.shift = false;
self.control = false;
}
}
pub enum TextInputResult {
Used,
Unused,
Unfocus,
Submit,
Paste,
}
impl TextInputResult {
pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus)
}
}
impl Deref for TextEdit {
type Target = TextView;
fn deref(&self) -> &Self::Target {
&self.view
}
}
impl DerefMut for TextEdit {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.view
}
}

133
src/core/text/mod.rs Normal file
View File

@@ -0,0 +1,133 @@
mod build;
mod edit;
pub use build::*;
pub use edit::*;
use crate::{prelude::*, util::MutDetect};
use cosmic_text::{Attrs, Metrics, Shaping};
use std::ops::{Deref, DerefMut};
pub const SHAPING: Shaping = Shaping::Advanced;
pub struct Text {
pub content: MutDetect<String>,
view: TextView,
}
pub struct TextView {
pub attrs: MutDetect<TextAttrs>,
pub buf: MutDetect<TextBuffer>,
// cache
tex: Option<TextTexture>,
width: Option<f32>,
}
impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs) -> Self {
Self {
attrs: attrs.into(),
buf: buf.into(),
tex: None,
width: None,
}
}
pub fn draw(&mut self, ctx: &mut SizeCtx) -> TextTexture {
let width = if self.attrs.wrap {
Some(ctx.px_size().x)
} else {
None
};
if width == self.width
&& let Some(tex) = &self.tex
&& !self.attrs.changed
&& !self.buf.changed
{
return tex.clone();
}
self.width = width;
let font_system = &mut ctx.text.font_system;
self.attrs.apply(font_system, &mut self.buf, width);
self.buf.shape_until_scroll(font_system, false);
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
self.tex = Some(tex.clone());
self.attrs.changed = false;
self.buf.changed = false;
tex
}
pub fn tex(&self) -> Option<&TextTexture> {
self.tex.as_ref()
}
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
Self {
content: content.into().into(),
view: TextView::new(buf, attrs),
}
}
fn update_buf(&mut self, ctx: &mut SizeCtx) -> TextTexture {
if self.content.changed {
self.content.changed = false;
self.view.buf.set_text(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
}
self.view.draw(ctx)
}
}
impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) {
let tex = self.update_buf(&mut painter.size_ctx());
let region = text_region(&tex, self.align);
painter.texture_within(&tex.handle, region);
}
fn desired_size(&mut self, ctx: &mut SizeCtx) -> Size {
Size::abs(self.update_buf(ctx).size())
}
}
pub fn text_region(tex: &TextTexture, align: Align) -> UiRegion {
let tex_dims = tex.handle.size();
let mut region = UiRegion::from_size_align(tex.size(), align);
region.top_left.abs += tex.top_left;
region.bot_right.abs = region.top_left.abs + tex_dims;
region
}
impl Deref for Text {
type Target = TextAttrs;
fn deref(&self) -> &Self::Target {
&self.view
}
}
impl DerefMut for Text {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.view
}
}
impl Deref for TextView {
type Target = TextAttrs;
fn deref(&self) -> &Self::Target {
&self.attrs
}
}
impl DerefMut for TextView {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.attrs
}
}

122
src/core/trait_fns.rs Normal file
View File

@@ -0,0 +1,122 @@
use super::*;
use crate::prelude::*;
pub trait CoreWidget<W, Tag> {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<Pad>;
fn align(self, align: Align) -> impl WidgetFn<Aligned>;
fn center(self) -> impl WidgetFn<Aligned>;
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<W>;
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Sized>;
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Sized>;
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Sized>;
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset>;
fn scroll(self) -> impl WidgetIdFn<Offset>;
fn masked(self) -> impl WidgetFn<Masked>;
fn background<T>(self, w: impl WidgetLike<T>) -> impl WidgetFn<Stack>;
fn z_offset(self, offset: usize) -> impl WidgetFn<Stack>;
}
impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<Pad> {
|ui| Pad {
padding: padding.into(),
inner: self.add(ui).any(),
}
}
fn align(self, align: Align) -> impl WidgetFn<Aligned> {
move |ui| Aligned {
inner: self.add(ui).any(),
align,
}
}
fn center(self) -> impl WidgetFn<Aligned> {
self.align(Align::Center)
}
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<W::Widget> {
|ui| {
let id = self.add(ui);
ui.set_label(&id, label.into());
id
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Sized> {
let size = size.into();
move |ui| Sized {
inner: self.add(ui).any(),
x: Some(size.x),
y: Some(size.y),
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Sized> {
let len = len.into();
move |ui| Sized {
inner: self.add(ui).any(),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Sized> {
let len = len.into();
move |ui| Sized {
inner: self.add(ui).any(),
x: None,
y: Some(len),
}
}
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset> {
move |ui| Offset {
inner: self.add(ui).any(),
amt: amt.into(),
}
}
fn scroll(self) -> impl WidgetIdFn<Offset> {
self.offset(UiVec2::ZERO)
.edit_on(CursorSense::Scroll, |w, data| {
w.amt += UiVec2::abs(data.scroll_delta * 50.0);
})
}
fn masked(self) -> impl WidgetFn<Masked> {
move |ui| Masked {
inner: self.add(ui).any(),
}
}
fn background<T>(self, w: impl WidgetLike<T>) -> impl WidgetFn<Stack> {
move |ui| Stack {
children: vec![w.add(ui).any(), self.add(ui).any()],
size: StackSize::Child(1),
offset: 0,
}
}
fn z_offset(self, offset: usize) -> impl WidgetFn<Stack> {
move |ui| Stack {
children: vec![self.add(ui).any()],
size: StackSize::Child(0),
offset,
}
}
}
pub trait CoreWidgetArr<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> {
fn span(self, dir: Dir) -> SpanBuilder<LEN, Wa, Tag>;
fn stack(self) -> StackBuilder<LEN, Wa, Tag>;
}
impl<const LEN: usize, Wa: WidgetArrLike<LEN, Tag>, Tag> CoreWidgetArr<LEN, Wa, Tag> for Wa {
fn span(self, dir: Dir) -> SpanBuilder<LEN, Wa, Tag> {
SpanBuilder::new(self, dir)
}
fn stack(self) -> StackBuilder<LEN, Wa, Tag> {
StackBuilder::new(self)
}
}

138
src/layout/color.rs Normal file
View File

@@ -0,0 +1,138 @@
#![allow(clippy::multiple_bound_locations)]
/// stored in linear for sane manipulation
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, Debug)]
pub struct Color<T: ColorNum> {
pub r: T,
pub g: T,
pub b: T,
pub 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 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;
}
impl<T: ColorNum + F32Conversion> Color<T> {
pub fn mul_rgb(self, amt: impl F32Conversion) -> Self {
let amt = amt.to();
self.map_rgb(|x| T::from(x.to() * amt))
}
pub fn add_rgb(self, amt: impl F32Conversion) -> Self {
let amt = amt.to();
self.map_rgb(|x| T::from(x.to() + amt))
}
pub fn darker(self, amt: f32) -> Self {
self.mul_rgb(1.0 - amt)
}
pub fn brighter(self, amt: f32) -> Self {
self.map_rgb(|x| {
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> {}
impl const F32Conversion for f32 {
fn to(self) -> f32 {
self
}
fn from(x: f32) -> Self {
x
}
}
impl const 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
}
}

27
src/layout/data.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::any::{Any, TypeId};
use crate::util::{HashMap, Id};
#[derive(Default)]
pub struct UiData {
map: HashMap<TypeId, Box<dyn Any>>,
}
impl UiData {
pub fn get<T: 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.map(|d| d.downcast_ref().unwrap())
}
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.map
.get_mut(&TypeId::of::<T>())
.map(|d| d.downcast_mut().unwrap())
}
pub fn emit_remove(&mut self, id: &Id) {
for (tid, f) in &mut self.on_remove {
let data = self.map.get_mut(tid).unwrap().downcast_ref().unwrap();
}
}
}

202
src/layout/event.rs Normal file
View File

@@ -0,0 +1,202 @@
use std::{hash::Hash, rc::Rc};
use crate::{
layout::{IdFnTag, Ui, UiModule, Widget, WidgetId, WidgetIdFn, WidgetLike},
util::{HashMap, Id},
};
pub trait UiCtx {
fn ui(&mut self) -> &mut Ui;
}
impl UiCtx for Ui {
fn ui(&mut self) -> &mut Ui {
self
}
}
pub trait Event: Sized {
type Module<Ctx: 'static>: EventModule<Self, Ctx>;
type Data: Clone;
}
pub trait EventFn<Ctx, Data>: Fn(&mut Ctx, Data) + 'static {}
impl<F: Fn(&mut Ctx, Data) + 'static, Ctx, Data> EventFn<Ctx, Data> for F {}
pub trait Eventable<W, Tag> {
fn on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl EventFn<Ctx, E::Data>,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>;
fn id_on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl Fn(&WidgetId<W>, &mut Ctx, E::Data) + 'static,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>
where
W: Widget;
fn edit_on<E: Event>(
self,
event: E,
f: impl Fn(&mut W, E::Data) + 'static,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>
where
W: Widget;
}
impl<W: WidgetLike<Tag>, Tag> Eventable<W::Widget, Tag> for W {
fn on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl EventFn<Ctx, E::Data>,
) -> impl WidgetIdFn<W::Widget> {
move |ui| {
let id = self.add(ui);
ui.data
.modules
.get_mut::<E::Module<Ctx>>()
.register(id.id, event, f);
id
}
}
fn id_on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl Fn(&WidgetId<W::Widget>, &mut Ctx, E::Data) + 'static,
) -> impl WidgetIdFn<W::Widget>
where
W::Widget: Widget,
{
self.with_id(move |ui, id| {
let id2 = id.clone();
id.on(event, move |ctx, pos| f(&id2, ctx, pos)).add(ui)
})
}
fn edit_on<E: Event>(
self,
event: E,
f: impl Fn(&mut W::Widget, E::Data) + 'static,
) -> impl WidgetIdFn<W::Widget>
where
W::Widget: Widget,
{
self.id_on(event, move |id, ui: &mut Ui, pos| f(&mut ui[id], pos))
}
}
pub trait DefaultEvent: Hash + Eq + 'static {
type Data: Clone;
}
impl<E: DefaultEvent> Event for E {
type Module<Ctx: 'static> = DefaultEventModule<E, Ctx>;
type Data = E::Data;
}
pub trait EventModule<E: Event, Ctx>: UiModule + Default {
fn register(&mut self, id: Id, event: E, f: impl EventFn<Ctx, E::Data>);
fn run<'a>(
&self,
id: &Id,
event: E,
) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, Self, E, Ctx>>;
}
type EventFnMap<Ctx, Data> = HashMap<Id, Vec<Rc<dyn EventFn<Ctx, Data>>>>;
pub struct DefaultEventModule<E: Event, Ctx> {
map: HashMap<E, EventFnMap<Ctx, <E as Event>::Data>>,
}
impl<E: Event + 'static, Ctx: 'static> UiModule for DefaultEventModule<E, Ctx> {
fn on_remove(&mut self, id: &Id) {
for map in self.map.values_mut() {
map.remove(id);
}
}
}
pub trait HashableEvent: Event + Hash + Eq + 'static {}
impl<E: Event + Hash + Eq + 'static> HashableEvent for E {}
impl<E: HashableEvent, Ctx: 'static> EventModule<E, Ctx> for DefaultEventModule<E, Ctx> {
fn register(&mut self, id: Id, event: E, f: impl EventFn<Ctx, <E as Event>::Data>) {
self.map
.entry(event)
.or_default()
.entry(id)
.or_default()
.push(Rc::new(f));
}
fn run<'a>(&self, id: &Id, event: E) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, E, Ctx>> {
if let Some(map) = self.map.get(&event)
&& let Some(fs) = map.get(id)
{
let fs = fs.clone();
Some(move |ctx: &mut Ctx, data: E::Data| {
for f in &fs {
f(ctx, data.clone())
}
})
} else {
None
}
}
}
impl<E: HashableEvent, Ctx: 'static> DefaultEventModule<E, Ctx> {
pub fn run_all(&self, ctx: &mut Ctx, event: E, data: E::Data)
where
E::Data: Clone,
{
if let Some(map) = self.map.get(&event) {
for fs in map.values() {
for f in fs {
f(ctx, data.clone())
}
}
}
}
}
impl<E: Event + 'static, Ctx: 'static> Default for DefaultEventModule<E, Ctx> {
fn default() -> Self {
Self {
map: Default::default(),
}
}
}
impl Ui {
pub fn run_event<E: Event, Ctx: UiCtx + 'static, W>(
ctx: &mut Ctx,
id: &WidgetId<W>,
event: E,
data: E::Data,
) {
if let Some(f) = ctx
.ui()
.data
.modules
.get_mut::<E::Module<Ctx>>()
.run(&id.id, event)
{
f(ctx, data);
}
}
}
pub trait EventCtx: UiCtx {
fn run_event<E: Event + Clone, W>(&mut self, id: &WidgetId<W>, event: E, data: E::Data);
}
impl<Ctx: UiCtx + 'static> EventCtx for Ctx {
fn run_event<E: Event + Clone, W>(&mut self, id: &WidgetId<W>, event: E, data: E::Data) {
Ui::run_event(self, id, event.clone(), data.clone());
}
}

250
src/layout/id.rs Normal file
View File

@@ -0,0 +1,250 @@
use std::{
any::TypeId,
marker::PhantomData,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::Sender,
},
};
use crate::{
layout::{FnTag, Ui, Widget, WidgetLike, WidgetTag},
util::{Id, RefCounter},
};
pub struct AnyWidget;
/// An identifier for a widget that can index a UI to get the associated widget.
/// It should always remain valid; it keeps a ref count and removes the widget from the UI if all
/// references are dropped.
///
/// W does not need to implement widget so that AnyWidget is valid;
/// Instead, add generic bounds on methods that take an ID if they need specific data.
///
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
#[repr(C)]
pub struct WidgetId<W = AnyWidget> {
pub(super) ty: TypeId,
pub(super) id: Id,
counter: RefCounter,
send: Sender<Id>,
is_static: Arc<AtomicBool>,
_pd: PhantomData<W>,
}
/// A WidgetId for a static widget that cannot be removed from a Ui.
/// Useful because ergonomic clones don't exist yet so you can easily use these in closures.
/// Do not use this if you want the widget to be freeable.
///
/// This is currently not perfectly efficient and just creates new WidgetIds every time it's used,
/// but they don't send drop messages to Ui.
/// Ideally I'd have an enum or something that lets you use either, but that doesn't seem worth it
/// right now; it's good enough and relatively cheap.
#[repr(C)]
pub struct StaticWidgetId<W = AnyWidget> {
pub(super) ty: TypeId,
pub(super) id: Id,
_pd: PhantomData<W>,
}
impl<W> std::fmt::Debug for WidgetId<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl<W> Clone for WidgetId<W> {
fn clone(&self) -> Self {
Self {
id: self.id,
ty: self.ty,
counter: self.counter.clone(),
send: self.send.clone(),
is_static: self.is_static.clone(),
_pd: PhantomData,
}
}
}
impl<W> WidgetId<W> {
pub(super) fn new(id: Id, ty: TypeId, send: Sender<Id>, is_static: bool) -> Self {
Self {
ty,
id,
counter: RefCounter::new(),
send,
is_static: Arc::new(is_static.into()),
_pd: PhantomData,
}
}
pub fn any(self) -> WidgetId<AnyWidget> {
self.cast_type()
}
pub fn as_any(&self) -> &WidgetId<AnyWidget> {
// SAFETY: self is repr(C) and generic only used for phantom data
unsafe { std::mem::transmute(self) }
}
pub fn key(&self) -> Id {
self.id
}
pub(super) fn cast_type<W2>(self) -> WidgetId<W2> {
// SAFETY: self is repr(C) and generic only used for phantom data
unsafe { std::mem::transmute(self) }
}
pub fn refs(&self) -> u32 {
self.counter.refs()
}
pub fn into_static(self) -> StaticWidgetId<W> {
self.is_static.store(true, Ordering::Release);
StaticWidgetId {
ty: self.ty,
id: self.id,
_pd: PhantomData,
}
}
}
impl WidgetId {
pub fn set_static<W>(&mut self, other: StaticWidgetId<W>) {
let send = self.send.clone();
drop(std::mem::replace(
self,
Self::new(other.id, self.ty, send, true),
));
}
}
impl<W> Drop for WidgetId<W> {
fn drop(&mut self) {
if self.counter.drop() && !self.is_static.load(Ordering::Acquire) {
let _ = self.send.send(self.id);
}
}
}
pub struct IdTag;
pub struct IdFnTag;
pub trait WidgetIdFn<W>: FnOnce(&mut Ui) -> WidgetId<W> {}
impl<W, F: FnOnce(&mut Ui) -> WidgetId<W>> WidgetIdFn<W> for F {}
/// TODO: does this ever make sense to use? it allows for invalid ids
pub trait Idable<Tag> {
type Widget: Widget;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>);
fn id(self, id: &WidgetId<Self::Widget>) -> impl WidgetIdFn<Self::Widget>
where
Self: Sized,
{
let id = id.clone();
move |ui| {
self.set(ui, &id);
id
}
}
fn id_static(self, id: StaticWidgetId<Self::Widget>) -> impl WidgetIdFn<Self::Widget>
where
Self: Sized,
{
move |ui| {
let id = id.id(&ui.send);
self.set(ui, &id);
id
}
}
}
impl<W: Widget> Idable<WidgetTag> for W {
type Widget = W;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>) {
ui.set(id, self);
}
}
impl<F: FnOnce(&mut Ui) -> W, W: Widget> Idable<FnTag> for F {
type Widget = W;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>) {
let w = self(ui);
ui.set(id, w);
}
}
impl<W: 'static> WidgetLike<IdTag> for WidgetId<W> {
type Widget = W;
fn add(self, _: &mut Ui) -> WidgetId<W> {
self
}
}
impl<W: 'static, F: FnOnce(&mut Ui) -> WidgetId<W>> WidgetLike<IdFnTag> for F {
type Widget = W;
fn add(self, ui: &mut Ui) -> WidgetId<W> {
self(ui)
}
}
impl<W> StaticWidgetId<W> {
pub fn to_id(&self, send: &Sender<Id>) -> WidgetId<W> {
WidgetId::new(self.id, self.ty, send.clone(), true)
}
pub fn any(self) -> StaticWidgetId<AnyWidget> {
// SAFETY: self is repr(C)
unsafe { std::mem::transmute(self) }
}
}
impl<W: 'static> WidgetLike<IdTag> for StaticWidgetId<W> {
type Widget = W;
fn add(self, ui: &mut Ui) -> WidgetId<W> {
self.id(&ui.send)
}
}
impl<W> Clone for StaticWidgetId<W> {
fn clone(&self) -> Self {
*self
}
}
impl<W> Copy for StaticWidgetId<W> {}
pub trait WidgetIdLike<W> {
fn id(self, send: &Sender<Id>) -> WidgetId<W>;
}
impl<W> WidgetIdLike<W> for &WidgetId<W> {
fn id(self, _: &Sender<Id>) -> WidgetId<W> {
self.clone()
}
}
impl<W> WidgetIdLike<W> for StaticWidgetId<W> {
fn id(self, send: &Sender<Id>) -> WidgetId<W> {
self.to_id(send)
}
}
pub trait IdLike<W> {
fn id(&self) -> Id;
}
impl<W> IdLike<W> for WidgetId<W> {
fn id(&self) -> Id {
self.id
}
}
impl<W> IdLike<W> for StaticWidgetId<W> {
fn id(&self) -> Id {
self.id
}
}

257
src/layout/layer.rs Normal file
View File

@@ -0,0 +1,257 @@
use std::ops::{Index, IndexMut};
use crate::render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives};
struct LayerNode {
next: Ptr,
prev: Ptr,
child: Option<Child>,
data: Layer,
}
#[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 {
vec: Vec<LayerNode>,
/// index of last layer at top level (start at first = 0)
last: usize,
}
/// TODO: this can be replaced with Primitives itself atm
#[derive(Default)]
pub struct Layer {
pub primitives: Primitives,
}
#[derive(Clone, Copy)]
struct Child {
head: usize,
tail: usize,
}
impl Layers {
pub fn new() -> Layers {
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) -> usize {
let i = self.vec.len();
self.vec.push(node);
i
}
pub fn next(&mut self, i: usize) -> usize {
if let Ptr::Next(i) = self.vec[i].next {
return i;
}
let i_new = self.push(LayerNode::new(
Layer::default(),
self.vec[i].next,
Ptr::Next(i),
));
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: usize) -> usize {
if let Some(c) = self.vec[i].child {
return c.head;
}
let i_child = self.push(LayerNode::new(
Layer::default(),
Ptr::Parent(i),
Ptr::Parent(i),
));
self.vec[i].child = Some(Child {
head: i_child,
tail: i_child,
});
i_child
}
pub fn iter_mut(&mut self) -> LayerIteratorMut<'_> {
LayerIteratorMut::new(&mut self.vec, self.last)
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &Layer)> {
self.indices().map(|i| (i, &self.vec[i].data))
}
pub fn indices(&self) -> LayerIndexIterator<'_> {
LayerIndexIterator::new(&self.vec, self.last)
}
pub fn write<P: Primitive>(&mut self, layer: usize, info: PrimitiveInst<P>) -> PrimitiveHandle {
self[layer].primitives.write(layer, info)
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self[h.layer].primitives.free(h)
}
}
impl Default for Layers {
fn default() -> Self {
Self::new()
}
}
impl Index<usize> for Layers {
type Output = Layer;
fn index(&self, index: usize) -> &Self::Output {
&self.vec[index].data
}
}
impl IndexMut<usize> for Layers {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.vec[index].data
}
}
impl LayerNode {
pub fn new(data: Layer, next: Ptr, prev: Ptr) -> Self {
Self {
next,
prev,
child: None,
data,
}
}
pub fn head() -> Self {
Self::new(Layer::default(), Ptr::None, Ptr::None)
}
}
pub struct LayerIteratorMut<'a> {
inner: LayerIndexIterator<'a>,
}
impl<'a> Iterator for LayerIteratorMut<'a> {
type Item = (usize, &'a mut Layer);
fn next(&mut self) -> Option<Self::Item> {
let i = self.inner.next()?;
// SAFETY: requires index iterator to work properly
#[allow(mutable_transmutes)]
let layer = unsafe { std::mem::transmute::<&Layer, &mut Layer>(&self.inner.vec[i].data) };
Some((i, layer))
}
}
impl<'a> DoubleEndedIterator for LayerIteratorMut<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
let i = self.inner.next_back()?;
// SAFETY: requires index iterator to work properly
#[allow(mutable_transmutes)]
let layer = unsafe { std::mem::transmute::<&Layer, &mut Layer>(&self.inner.vec[i].data) };
Some((i, layer))
}
}
impl<'a> LayerIteratorMut<'a> {
fn new(vec: &'a mut Vec<LayerNode>, last: usize) -> Self {
Self {
inner: LayerIndexIterator::new(vec, last),
}
}
}
pub struct LayerIndexIterator<'a> {
next: Option<usize>,
next_back: Option<usize>,
vec: &'a Vec<LayerNode>,
}
impl<'a> Iterator for LayerIndexIterator<'a> {
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> DoubleEndedIterator for LayerIndexIterator<'a> {
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> LayerIndexIterator<'a> {
fn new(vec: &'a Vec<LayerNode>, 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,
}
}
}

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

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

View File

@@ -1,8 +1,33 @@
mod color;
mod event;
mod id;
mod layer;
mod module;
mod num;
mod orientation;
mod painter;
mod pos;
mod text;
mod texture;
mod ui;
mod vec2;
mod widget;
mod widgets;
pub use color::*;
pub use event::*;
pub use id::*;
pub use layer::*;
pub use module::*;
pub use num::*;
pub use orientation::*;
pub use painter::*;
pub use pos::*;
pub use text::*;
pub use texture::*;
pub use ui::*;
pub use vec2::*;
pub use widget::*;
pub use widgets::*;
use crate::primitive::Color;
pub type UIColor = Color<u8>;
pub type UiColor = Color<u8>;

35
src/layout/module.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::any::{Any, TypeId};
use crate::{
layout::WidgetInstance,
util::{HashMap, Id},
};
#[allow(unused_variables)]
pub trait UiModule: Any {
fn on_draw(&mut self, inst: &WidgetInstance) {}
fn on_undraw(&mut self, inst: &WidgetInstance) {}
fn on_remove(&mut self, id: &Id) {}
fn on_move(&mut self, inst: &WidgetInstance) {}
}
#[derive(Default)]
pub struct Modules {
map: HashMap<TypeId, Box<dyn UiModule>>,
}
impl Modules {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (dyn UiModule + 'static)> {
self.map.values_mut().map(|m| m.as_mut())
}
pub fn get_mut<M: UiModule + Default>(&mut self) -> &mut M {
let rf = self
.map
.entry(TypeId::of::<M>())
.or_insert_with(|| Box::new(M::default()))
.as_mut();
let any: &mut dyn Any = &mut *rf;
any.downcast_mut().unwrap()
}
}

21
src/layout/num.rs Normal file
View File

@@ -0,0 +1,21 @@
pub const trait UiNum {
fn to_f32(self) -> f32;
}
impl const UiNum for f32 {
fn to_f32(self) -> f32 {
self
}
}
impl const UiNum for u32 {
fn to_f32(self) -> f32 {
self as f32
}
}
impl const UiNum for i32 {
fn to_f32(self) -> f32 {
self as f32
}
}

269
src/layout/orientation.rs Normal file
View File

@@ -0,0 +1,269 @@
use std::ops::Not;
use crate::{
layout::{UiNum, UiScalar, UiVec2, Vec2, vec2},
util::impl_op,
};
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum Axis {
X,
Y,
}
impl 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,
},
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Align {
TopLeft,
Top,
TopRight,
Left,
Center,
Right,
BotLeft,
Bot,
BotRight,
}
impl Align {
pub const fn rel(&self) -> Vec2 {
match self {
Self::TopLeft => vec2(0.0, 0.0),
Self::Top => vec2(0.5, 0.0),
Self::TopRight => vec2(1.0, 0.0),
Self::Left => vec2(0.0, 0.5),
Self::Center => vec2(0.5, 0.5),
Self::Right => vec2(1.0, 0.5),
Self::BotLeft => vec2(0.0, 1.0),
Self::Bot => vec2(0.5, 1.0),
Self::BotRight => vec2(1.0, 1.0),
}
}
}
#[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 Size {
pub const ZERO: Self = Self {
x: Len::ZERO,
y: Len::ZERO,
};
pub fn abs(v: Vec2) -> Self {
Self {
x: Len::abs(v.x),
y: Len::abs(v.y),
}
}
pub fn to_uivec2(self, rel_len: Vec2) -> UiVec2 {
UiVec2::from_scalars(self.x.apply_rest(rel_len.x), self.y.apply_rest(rel_len.y))
}
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 fn apply_rest(&self, rel_len: f32) -> UiScalar {
UiScalar {
rel: if self.rest > 0.0 {
self.rel.max(1.0)
} else {
self.rel
} * rel_len,
abs: if self.rest > 0.0 { 0.0 } else { 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.abs)?;
}
if self.rest != 0.0 {
write!(f, "{} leftover;", self.abs)?;
}
Ok(())
}
}

432
src/layout/painter.rs Normal file
View File

@@ -0,0 +1,432 @@
use crate::{
layout::{
Layers, Modules, Size, TextAttrs, TextBuffer, TextData, TextTexture, TextureHandle,
Textures, UiRegion, UiVec2, Vec2, WidgetId, Widgets,
},
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::{HashMap, HashSet, Id, TrackedArena},
};
pub struct Painter<'a, 'c> {
ctx: &'a mut PainterCtx<'c>,
region: UiRegion,
mask: MaskIdx,
textures: Vec<TextureHandle>,
primitives: Vec<PrimitiveHandle>,
children: Vec<Id>,
sized_children: HashMap<Id, Size>,
/// whether this widget depends on region's final pixel size or not
/// TODO: decide if point (pt) should be used here instead of px
pub layer: usize,
id: Id,
}
pub struct PainterCtx<'a> {
pub widgets: &'a Widgets,
pub active: &'a mut HashMap<Id, WidgetInstance>,
pub layers: &'a mut Layers,
pub textures: &'a mut Textures,
pub masks: &'a mut TrackedArena<Mask, u32>,
pub text: &'a mut TextData,
pub screen_size: Vec2,
pub modules: &'a mut Modules,
pub px_dependent: &'a mut HashSet<Id>,
draw_started: HashSet<Id>,
}
pub struct WidgetInstance {
pub id: Id,
pub region: UiRegion,
pub parent: Option<Id>,
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<Id>,
pub resize: Option<Id>,
pub mask: MaskIdx,
pub layer: usize,
pub desired_size: Size,
}
#[derive(Default)]
pub struct PainterData {
pub widgets: Widgets,
pub active: HashMap<Id, WidgetInstance>,
pub layers: Layers,
pub textures: Textures,
pub text: TextData,
pub output_size: Vec2,
pub modules: Modules,
pub px_dependent: HashSet<Id>,
pub masks: TrackedArena<Mask, u32>,
}
impl<'a> PainterCtx<'a> {
pub fn new(data: &'a mut PainterData) -> Self {
Self {
widgets: &data.widgets,
active: &mut data.active,
layers: &mut data.layers,
textures: &mut data.textures,
text: &mut data.text,
screen_size: data.output_size,
modules: &mut data.modules,
px_dependent: &mut data.px_dependent,
masks: &mut data.masks,
draw_started: HashSet::default(),
}
}
pub fn redraw(&mut self, id: Id) {
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.active.get(&id) else {
return;
};
if let Some(rid) = active.resize {
let desired = SizeCtx {
checked: &mut Default::default(),
text: self.text,
textures: self.textures,
widgets: self.widgets,
size: UiVec2::FULL_SIZE,
screen_size: self.screen_size,
px_dependent: &mut Default::default(),
id,
}
.size_inner(id, active.region.size());
if active.desired_size != desired {
self.redraw(rid);
if self.draw_started.contains(&id) {
return;
}
}
}
let Some(active) = self.remove(id) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
);
self.active.get_mut(&id).unwrap().resize = active.resize;
}
pub fn draw(&mut self, id: Id) {
self.draw_started.clear();
self.layers.clear();
self.draw_inner(0, id, UiRegion::full(), None, MaskIdx::NONE, None);
}
fn draw_inner(
&mut self,
layer: usize,
id: Id,
region: UiRegion,
parent: Option<Id>,
mask: MaskIdx,
old_children: Option<Vec<Id>>,
) {
// I have no idea if these checks work lol
// the idea is u can't redraw stuff u already drew,
// and if parent is different then there's another copy with a different parent
// but this has a very weird issue where you can't move widgets unless u remove first
// so swapping is impossible rn I think?
// there's definitely better solutions like a counter (>1 = panic) but don't care rn
if self.draw_started.contains(&id) {
panic!(
"Cannot draw the same widget ({}) twice (1)",
self.widgets.data(&id).unwrap().label
);
}
let mut old_children = old_children.unwrap_or_default();
let mut resize = None;
if let Some(active) = self.active.get_mut(&id) {
if active.parent != parent {
panic!("Cannot draw the same widget twice (2)");
}
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
let active = self.remove(id).unwrap();
old_children = active.children;
resize = active.resize;
}
self.draw_started.insert(id);
let desired_size = SizeCtx {
text: self.text,
textures: self.textures,
widgets: self.widgets,
checked: &mut Default::default(),
screen_size: self.screen_size,
px_dependent: &mut Default::default(),
id,
size: region.size(),
}
.size_raw(id);
let mut painter = Painter {
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
ctx: self,
children: Vec::new(),
sized_children: Default::default(),
};
// draw widgets
painter.ctx.widgets.get_dyn_dynamic(id).draw(&mut painter);
let sized_children = painter.sized_children;
// add to active
let instance = WidgetInstance {
id,
region,
parent,
textures: painter.textures,
primitives: painter.primitives,
children: painter.children,
resize,
mask: painter.mask,
desired_size,
layer,
};
for cid in sized_children.keys() {
if let Some(w) = self.active.get_mut(cid)
&& w.resize.is_none()
{
w.resize = Some(id)
}
}
for c in &old_children {
if !instance.children.contains(c) {
self.remove_rec(*c);
}
}
for m in self.modules.iter_mut() {
m.on_draw(&instance);
}
self.active.insert(id, instance);
}
fn mov(&mut self, id: Id, from: UiRegion, to: UiRegion) {
let active = self.active.get_mut(&id).unwrap();
// children will not be changed, so this technically should not be needed
// probably need unsafe
for h in &active.primitives {
let region = self.layers[h.layer].primitives.region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
for m in self.modules.iter_mut() {
m.on_move(active);
}
let children = active.children.clone();
for child in children {
self.mov(child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: Id) -> Option<WidgetInstance> {
let mut inst = self.active.remove(&id);
if let Some(inst) = &mut inst {
for h in &inst.primitives {
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
self.masks.remove(mask);
}
}
inst.textures.clear();
self.textures.free();
for m in self.modules.iter_mut() {
m.on_undraw(inst);
}
}
self.px_dependent.remove(&id);
inst
}
fn remove_rec(&mut self, id: Id) -> Option<WidgetInstance> {
let inst = self.remove(id);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c);
}
}
inst
}
}
impl<'a, 'c> Painter<'a, 'c> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self.ctx.layers.write(
self.layer,
PrimitiveInst {
id: self.id,
primitive,
region,
mask_idx: self.mask,
},
);
if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.ctx.masks.push_ref(self.mask);
}
self.primitives.push(h);
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
self.primitive_at(primitive, self.region)
}
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.primitive_at(primitive, region.within(&self.region));
}
pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE);
self.mask = self.ctx.masks.push(Mask { region });
}
/// Draws a widget within this widget's region.
pub fn widget<W>(&mut self, id: &WidgetId<W>) {
self.widget_at(id, self.region);
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W>(&mut self, id: &WidgetId<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region));
}
fn widget_at<W>(&mut self, id: &WidgetId<W>, region: UiRegion) {
self.children.push(id.id);
self.ctx
.draw_inner(self.layer, id.id, region, Some(self.id), self.mask, None);
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region.within(&self.region));
}
pub fn texture(&mut self, handle: &TextureHandle) {
self.textures.push(handle.clone());
self.primitive(handle.primitive());
}
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region);
}
/// returns (handle, offset from top left)
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> TextTexture {
self.ctx.text.draw(buffer, attrs, self.ctx.textures)
}
pub fn region(&self) -> UiRegion {
self.region
}
pub fn size<W>(&mut self, id: &WidgetId<W>) -> Size {
self.size_ctx().size(id)
}
pub fn region_size<W>(&mut self, id: &WidgetId<W>) -> UiVec2 {
self.size_ctx().size(id).to_uivec2(self.region.size().rel)
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
SizeCtx {
text: self.ctx.text,
textures: self.ctx.textures,
widgets: self.ctx.widgets,
checked: &mut self.sized_children,
screen_size: self.ctx.screen_size,
px_dependent: self.ctx.px_dependent,
id: self.id,
size: self.region.size(),
}
}
pub fn px_size(&mut self) -> Vec2 {
self.ctx.px_dependent.insert(self.id);
self.region.size().to_abs(self.ctx.screen_size)
}
pub fn text_data(&mut self) -> &mut TextData {
self.ctx.text
}
pub fn child_layer(&mut self) {
self.layer = self.ctx.layers.child(self.layer);
}
pub fn next_layer(&mut self) {
self.layer = self.ctx.layers.next(self.layer);
}
}
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
widgets: &'a Widgets,
px_dependent: &'a mut HashSet<Id>,
checked: &'a mut HashMap<Id, Size>,
/// TODO: should this be pub? rn used for sized
pub size: UiVec2,
screen_size: Vec2,
id: Id,
}
impl SizeCtx<'_> {
fn size_inner(&mut self, id: Id, size: UiVec2) -> Size {
let self_size = self.size;
self.size = size;
let size = self.widgets.get_dyn_dynamic(id).desired_size(self);
self.size = self_size;
self.checked.insert(id, size);
size
}
pub fn size<W>(&mut self, id: &WidgetId<W>) -> Size {
if let Some(&size) = self.checked.get(&id.id) {
return size;
}
self.size_inner(id.id, self.size)
}
fn size_raw(&mut self, id: Id) -> Size {
self.size_inner(id, self.size)
}
pub fn px_size(&mut self) -> Vec2 {
self.px_dependent.insert(self.id);
self.size.to_abs(self.screen_size)
}
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> TextTexture {
self.text.draw(buffer, attrs, self.textures)
}
pub fn label(&self) -> &String {
self.widgets.label(&self.id)
}
}

366
src/layout/pos.rs Normal file
View File

@@ -0,0 +1,366 @@
use std::{fmt::Display, marker::Destruct};
use crate::{
layout::{Align, Axis, UiNum, Vec2},
util::{LerpUtil, impl_op},
};
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct UiVec2 {
pub rel: Vec2,
pub abs: Vec2,
}
impl UiVec2 {
pub const ZERO: Self = Self {
rel: Vec2::ZERO,
abs: Vec2::ZERO,
};
/// expands this position into a sized region centered at self
pub fn expand(&self, size: impl Into<Vec2>) -> UiRegion {
let size = size.into();
UiRegion {
top_left: self.offset(-size / 2.0),
bot_right: self.offset(size / 2.0),
}
}
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
Self {
rel: Vec2::ZERO,
abs: abs.into(),
}
}
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
Self {
rel: rel.into(),
abs: Vec2::ZERO,
}
}
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 {
let rel = self.rel.lerp(region.top_left.rel, region.bot_right.rel);
let abs = self.abs + self.rel.lerp(region.top_left.abs, region.bot_right.abs);
UiVec2 { rel, abs }
}
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
let rel = self.rel.lerp_inv(region.top_left.rel, region.bot_right.rel);
let abs = self.abs - rel.lerp(region.top_left.abs, region.bot_right.abs);
UiVec2 { rel, abs }
}
pub fn axis_mut(&mut self, axis: Axis) -> UiScalarView<'_> {
match axis {
Axis::X => UiScalarView {
rel: &mut self.rel.x,
abs: &mut self.abs.x,
},
Axis::Y => UiScalarView {
rel: &mut self.rel.y,
abs: &mut self.abs.y,
},
}
}
pub fn axis(&self, axis: Axis) -> UiScalar {
match axis {
Axis::X => UiScalar {
rel: self.rel.x,
abs: self.abs.x,
},
Axis::Y => UiScalar {
rel: self.rel.y,
abs: self.abs.y,
},
}
}
/// reflection about an axis
pub fn flip(&mut self, axis: Axis) {
*self.rel.axis_mut(axis) = 1.0 - self.rel.axis(axis);
*self.abs.axis_mut(axis) = -self.abs.axis(axis);
}
pub fn flipped(mut self, axis: Axis) -> Self {
self.flip(axis);
self
}
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
self.rel * rel + self.abs
}
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
Self {
rel: Vec2::from_axis(axis, aligned.rel, ortho.rel),
abs: Vec2::from_axis(axis, aligned.abs, ortho.abs),
}
}
pub const fn from_scalars(x: UiScalar, y: UiScalar) -> Self {
Self {
rel: Vec2 { x: x.rel, y: y.rel },
abs: Vec2 { x: x.abs, y: y.abs },
}
}
}
impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel {} / abs {}", self.rel, self.abs)
}
}
impl_op!(UiVec2 Add add; rel abs);
impl_op!(UiVec2 Sub sub; rel abs);
impl const From<Align> for UiVec2 {
fn from(align: Align) -> Self {
Self::rel(align.rel())
}
}
impl Align {
pub fn pos(self) -> UiVec2 {
UiVec2::from(self)
}
}
impl const From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self {
Self::abs(abs)
}
}
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for UiVec2
where
(T, U): const Destruct,
{
fn from(abs: (T, U)) -> Self {
Self::abs(abs)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct UiScalar {
pub rel: f32,
pub abs: f32,
}
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 fn new(rel: f32, abs: f32) -> Self {
Self { rel, abs }
}
pub fn rel_min() -> Self {
Self::new(0.0, 0.0)
}
pub fn rel_max() -> Self {
Self::new(1.0, 0.0)
}
pub fn max(&self, other: Self) -> Self {
Self {
rel: self.rel.max(other.rel),
abs: self.abs.max(other.abs),
}
}
pub fn min(&self, other: Self) -> Self {
Self {
rel: self.rel.min(other.rel),
abs: self.abs.min(other.abs),
}
}
pub fn from_anchor(anchor: f32) -> Self {
Self::new(anchor, 0.0)
}
pub fn offset(mut self, amt: f32) -> Self {
self.abs += amt;
self
}
pub fn within(&self, start: UiScalar, end: UiScalar) -> Self {
let anchor = self.rel.lerp(start.rel, end.rel);
let offset = self.abs + self.rel.lerp(start.abs, end.abs);
Self {
rel: anchor,
abs: offset,
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct UiRegion {
pub top_left: UiVec2,
pub bot_right: UiVec2,
}
impl UiRegion {
pub const fn full() -> Self {
Self {
top_left: Align::TopLeft.into(),
bot_right: Align::BotRight.into(),
}
}
pub fn rel(anchor: Vec2) -> Self {
Self {
top_left: UiVec2::rel(anchor),
bot_right: UiVec2::rel(anchor),
}
}
pub fn within(&self, parent: &Self) -> Self {
Self {
top_left: self.top_left.within(parent),
bot_right: self.bot_right.within(parent),
}
}
pub fn outside(&self, parent: &Self) -> Self {
Self {
top_left: self.top_left.outside(parent),
bot_right: self.bot_right.outside(parent),
}
}
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 fn flip(&mut self, axis: Axis) {
self.top_left.flip(axis);
self.bot_right.flip(axis);
let tl = self.top_left.axis_mut(axis);
let br = self.bot_right.axis_mut(axis);
std::mem::swap(tl.rel, br.rel);
std::mem::swap(tl.abs, br.abs);
}
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
let offset = offset.into();
self.top_left.shift(offset);
self.bot_right.shift(offset);
}
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
self.shift(offset);
self
}
pub fn to_screen(&self, size: Vec2) -> ScreenRegion {
ScreenRegion {
top_left: self.top_left.rel * size + self.top_left.abs,
bot_right: self.bot_right.rel * size + self.bot_right.abs,
}
}
pub fn center(&self) -> UiVec2 {
Align::Center.pos().within(self)
}
pub fn size(&self) -> UiVec2 {
self.bot_right - self.top_left
}
pub fn select_aligned(&self, size: Vec2, align: Align) -> Self {
Self::from_size_align(size, align).within(self)
}
pub fn from_size_align(size: Vec2, align: Align) -> Self {
let mut top_left = UiVec2::from(align);
top_left.abs -= size * align.rel();
let mut bot_right = UiVec2::from(align);
bot_right.abs += size * (Vec2::ONE - align.rel());
Self {
top_left,
bot_right,
}
}
pub fn from_ui_size_align(size: UiVec2, align: Align) -> Self {
let mut top_left = UiVec2::from(align);
top_left.abs -= size.abs * align.rel();
top_left.rel -= size.rel * align.rel();
let mut bot_right = UiVec2::from(align);
bot_right.abs += size.abs * (Vec2::ONE - align.rel());
bot_right.rel += size.rel * (Vec2::ONE - align.rel());
Self {
top_left,
bot_right,
}
}
}
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)]
pub struct ScreenRegion {
pub top_left: Vec2,
pub bot_right: Vec2,
}
impl ScreenRegion {
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 struct UIRegionAxisView<'a> {
pub top_left: UiScalarView<'a>,
pub bot_right: UiScalarView<'a>,
}
pub struct UiScalarView<'a> {
pub rel: &'a mut f32,
pub abs: &'a mut f32,
}
impl UiScalarView<'_> {
pub fn set(&mut self, scalar: UiScalar) {
*self.rel = scalar.rel;
*self.abs = scalar.abs;
}
pub fn get(self) -> UiScalar {
UiScalar {
rel: *self.rel,
abs: *self.abs,
}
}
}

147
src/layout/text.rs Normal file
View File

@@ -0,0 +1,147 @@
use cosmic_text::{Attrs, AttrsList, Buffer, Family, FontSystem, Metrics, SwashCache};
use image::{Rgba, RgbaImage};
use crate::{
layout::{Align, TextureHandle, Textures, UiColor, Vec2},
util::HashMap,
};
/// TODO: properly wrap this
pub mod text_lib {
pub use cosmic_text::*;
}
pub struct TextData {
pub font_system: FontSystem,
pub swash_cache: SwashCache,
}
impl Default for TextData {
fn default() -> Self {
Self {
font_system: FontSystem::new(),
swash_cache: SwashCache::new(),
}
}
}
#[derive(Clone, Copy)]
pub struct TextAttrs {
pub color: UiColor,
pub font_size: f32,
pub line_height: f32,
pub family: Family<'static>,
pub wrap: bool,
/// inner alignment of text region (within where its drawn)
pub align: Align,
}
impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics_and_size(
font_system,
Metrics::new(self.font_size, self.line_height),
width,
None,
);
let attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs);
for line in &mut buf.lines {
line.set_attrs_list(list.clone());
}
}
}
pub type TextBuffer = Buffer;
impl Default for TextAttrs {
fn default() -> Self {
let size = 14.0;
Self {
color: UiColor::WHITE,
font_size: size,
line_height: size * 1.2,
family: Family::SansSerif,
wrap: false,
align: Align::Center,
}
}
}
impl TextData {
pub fn draw(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
textures: &mut Textures,
) -> TextTexture {
// TODO: either this or the layout stuff (or both) is super slow,
// should probably do texture packing and things if possible.
// very visible if you add just a couple of wrapping texts and resize window
// should also be timed to figure out exactly what points need to be sped up
let mut pixels = HashMap::default();
let mut min_x = 0;
let mut min_y = 0;
let mut max_x = 0;
let mut max_y = 0;
let cosmic_color = {
let c = attrs.color;
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
};
let mut max_width = 0.0f32;
let mut height = 0.0;
for run in buffer.layout_runs() {
for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((0., 0.), 1.0);
let glyph_color = match glyph.color_opt {
Some(some) => some,
None => cosmic_color,
};
self.swash_cache.with_pixels(
&mut self.font_system,
physical_glyph.cache_key,
glyph_color,
|x, y, color| {
let x = physical_glyph.x + x;
let y = run.line_y as i32 + physical_glyph.y + y;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
pixels.insert((x, y), Rgba(color.as_rgba()));
},
);
}
max_width = max_width.max(run.line_w);
height += run.line_height;
}
let img_width = (max_x - min_x + 1) as u32;
let img_height = (max_y - min_y + 1) as u32;
let mut image = RgbaImage::new(img_width, img_height);
for ((x, y), color) in pixels {
let x = (x - min_x) as u32;
let y = (y - min_y) as u32;
image.put_pixel(x, y, color);
}
TextTexture {
handle: textures.add(image),
top_left: Vec2::new(min_x as f32, min_y as f32),
bot_right: Vec2::new(max_width - max_x as f32, height - max_y as f32),
}
}
}
#[derive(Clone)]
pub struct TextTexture {
pub handle: TextureHandle,
pub top_left: Vec2,
pub bot_right: Vec2,
}
impl TextTexture {
pub fn size(&self) -> Vec2 {
self.handle.size() - self.top_left + self.bot_right
}
}

135
src/layout/texture.rs Normal file
View File

@@ -0,0 +1,135 @@
use std::{
ops::Index,
sync::mpsc::{Receiver, Sender, channel},
};
use image::{DynamicImage, GenericImageView};
use crate::{layout::Vec2, render::TexturePrimitive, util::RefCounter};
#[derive(Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
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),
Free(u32),
PushFree,
SetFree,
}
enum Update {
Push(u32),
Set(u32),
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();
let view_idx = self.push(image);
// 0 == default in renderer; TODO: actually create samplers here
let sampler_idx = 0;
TextureHandle {
inner: TexturePrimitive {
view_idx,
sampler_idx,
},
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 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::Free(i) => TextureUpdate::Free(i),
})
}
}
impl TextureHandle {
pub fn primitive(&self) -> TexturePrimitive {
self.inner
}
pub fn size(&self) -> Vec2 {
self.size
}
}
impl Drop for TextureHandle {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.inner.view_idx);
}
}
}
impl Index<&TextureHandle> for Textures {
type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.inner.view_idx as usize].as_ref().unwrap()
}
}
impl Default for Textures {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,101 +1,249 @@
use image::DynamicImage;
use crate::{
primitive::{Painter, Primitives},
util::{IDTracker, ID},
HashMap, Widget, WidgetId, WidgetLike, WidgetRef,
core::{TextEdit, TextEditCtx},
layout::{
IdLike, PainterCtx, PainterData, StaticWidgetId, TextureHandle, Vec2, Widget, WidgetId,
WidgetLike,
},
util::Id,
};
use std::{
any::{Any, TypeId},
cell::RefCell,
rc::Rc,
ops::{Index, IndexMut},
sync::mpsc::{Receiver, Sender, channel},
};
pub struct UI {
ids: IDTracker,
base: Option<WidgetId>,
pub widgets: Widgets,
pub struct Ui {
// TODO: make this at least pub(super)
pub(crate) data: PainterData,
root: Option<WidgetId>,
updates: Vec<Id>,
recv: Receiver<Id>,
pub(super) send: Sender<Id>,
full_redraw: bool,
resized: bool,
}
pub struct Widgets(HashMap<ID, Box<dyn Widget>>);
impl Ui {
pub fn add<W: Widget, Tag>(&mut self, w: impl WidgetLike<Tag, Widget = W>) -> WidgetId<W> {
w.add(self)
}
#[derive(Clone)]
pub struct UIBuilder {
ui: Rc<RefCell<UI>>,
}
pub fn add_static<W: Widget, Tag>(
&mut self,
w: impl WidgetLike<Tag, Widget = W>,
) -> StaticWidgetId<W> {
let id = w.add(self);
id.into_static()
}
impl From<UI> for UIBuilder {
fn from(ui: UI) -> Self {
UIBuilder {
ui: Rc::new(RefCell::new(ui)),
/// useful for debugging
pub fn set_label<W>(&mut self, id: &WidgetId<W>, label: String) {
self.data.widgets.data_mut(&id.id).unwrap().label = label;
}
pub fn label<W>(&self, id: &WidgetId<W>) -> &String {
&self.data.widgets.data(&id.id).unwrap().label
}
pub fn add_widget<W: Widget>(&mut self, w: W) -> WidgetId<W> {
self.push(w)
}
pub fn push<W: Widget>(&mut self, w: W) -> WidgetId<W> {
let id = self.id();
self.data.widgets.insert(id.id, w);
id
}
pub fn set<W: Widget>(&mut self, id: &WidgetId<W>, w: W) {
self.data.widgets.insert(id.id, w);
}
pub fn set_root<Tag>(&mut self, w: impl WidgetLike<Tag>) {
self.root = Some(w.add(self).any());
self.full_redraw = true;
}
pub fn new() -> Self {
Self::default()
}
pub fn get<W: Widget>(&self, id: &impl IdLike<W>) -> Option<&W> {
self.data.widgets.get(id)
}
pub fn get_mut<W: Widget>(&mut self, id: &impl IdLike<W>) -> Option<&mut W> {
self.data.widgets.get_mut(id)
}
pub fn id<W: Widget>(&mut self) -> WidgetId<W> {
WidgetId::new(
self.data.widgets.reserve(),
TypeId::of::<W>(),
self.send.clone(),
false,
)
}
pub fn id_static<W: Widget>(&mut self) -> StaticWidgetId<W> {
let id = self.id();
id.into_static()
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.data.textures.add(image)
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.data.output_size = size.into();
self.resized = true;
}
pub fn redraw_all(&mut self) {
for (_, inst) in self.data.active.drain() {
for m in self.data.modules.iter_mut() {
m.on_undraw(&inst);
}
}
// free before bc nothing should exist
self.free();
let mut ctx = PainterCtx::new(&mut self.data);
if let Some(root) = &self.root {
ctx.draw(root.id);
}
}
pub fn update(&mut self) {
if self.full_redraw {
self.redraw_all();
self.full_redraw = false;
} else if !self.updates.is_empty() {
self.redraw_updates();
}
if self.resized {
self.resized = false;
self.redraw_size();
}
}
fn redraw_size(&mut self) {
// let mut ctx = PainterCtx::new(&mut self.data);
// let dep = ctx.px_dependent.clone();
// for id in dep {
// ctx.redraw(id);
// }
self.redraw_all();
}
fn redraw_updates(&mut self) {
// if self.updates.drain(..).next().is_some() {
// self.redraw_all();
// }
let mut ctx = PainterCtx::new(&mut self.data);
for id in self.updates.drain(..) {
ctx.redraw(id);
}
self.free();
}
/// free any resources that don't have references anymore
fn free(&mut self) {
for id in self.recv.try_iter() {
for m in self.data.modules.iter_mut() {
m.on_remove(&id);
}
self.data.widgets.delete(id);
}
self.data.textures.free();
}
pub fn needs_redraw(&self) -> bool {
self.full_redraw || !self.updates.is_empty()
}
pub fn num_widgets(&self) -> usize {
self.data.widgets.len()
}
pub fn active_widgets(&self) -> usize {
self.data.active.len()
}
pub fn text(&mut self, id: &impl IdLike<TextEdit>) -> TextEditCtx<'_> {
self.updates.push(id.id());
TextEditCtx {
text: self.data.widgets.get_mut(id).unwrap(),
font_system: &mut self.data.text.font_system,
}
}
pub fn debug(&self, label: &str) {
for (id, inst) in &self.data.active {
let l = &self.data.widgets.data(id).unwrap().label;
if l != label {
continue;
}
println!("\"{label}\" {{");
println!(" region: {}", inst.region);
println!(" desired_size: {}", inst.desired_size);
println!("}}");
}
}
}
impl UIBuilder {
pub fn add<W: Widget>(&mut self, w: W) -> WidgetRef<W> {
WidgetRef::new(self.clone(), [self.push(w)])
}
impl<W: Widget> Index<&WidgetId<W>> for Ui {
type Output = 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<W: WidgetLike>(mut self, base: W) -> UI {
let base = base.add(&mut self).erase_type();
let mut ui = Rc::into_inner(self.ui).unwrap().into_inner();
ui.base = Some(base);
ui
fn index(&self, id: &WidgetId<W>) -> &Self::Output {
self.get(id).unwrap()
}
}
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<W: Widget> IndexMut<&WidgetId<W>> for Ui {
fn index_mut(&mut self, id: &WidgetId<W>) -> &mut Self::Output {
self.updates.push(id.id);
self.get_mut(id).unwrap()
}
}
impl Widgets {
fn new() -> Self {
Self(HashMap::new())
}
impl<W: Widget> Index<StaticWidgetId<W>> for Ui {
type Output = W;
pub fn get(&self, id: &WidgetId) -> &dyn Widget {
self.0.get(&id.id).unwrap().as_ref()
fn index(&self, id: StaticWidgetId<W>) -> &Self::Output {
self.data.widgets.get(&id).unwrap()
}
}
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<W: Widget> IndexMut<StaticWidgetId<W>> for Ui {
fn index_mut(&mut self, id: StaticWidgetId<W>) -> &mut Self::Output {
self.updates.push(id.id);
self.data.widgets.get_mut(&id).unwrap()
}
}
impl dyn Widget {
pub fn as_any(&self) -> &dyn Any {
self
}
pub fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Default for Ui {
fn default() -> Self {
let (send, recv) = channel();
Self {
data: PainterData::default(),
root: Default::default(),
updates: Default::default(),
full_redraw: false,
send,
recv,
resized: false,
}
}
}

104
src/layout/vec2.rs Normal file
View File

@@ -0,0 +1,104 @@
use crate::{
layout::UiNum,
util::{DivOr, impl_op},
};
use std::{marker::Destruct, ops::*};
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
}
pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
Vec2::new(x.to_f32(), y.to_f32())
}
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(),
}
}
}
impl<T: const UiNum + Copy> const From<T> for Vec2 {
fn from(v: T) -> Self {
Self {
x: v.to_f32(),
y: v.to_f32(),
}
}
}
// 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 const DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self {
Self {
x: self.x.div_or(rhs.x, other.x),
y: self.y.div_or(rhs.y, other.y),
}
}
}
impl Neg for Vec2 {
type Output = Self;
fn neg(mut self) -> Self::Output {
self.x = -self.x;
self.y = -self.y;
self
}
}
impl<T: const UiNum, U: const UiNum> const 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(),
}
}
}
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)
}
}

View File

@@ -1,150 +1,117 @@
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
use crate::layout::{Painter, Size, SizeCtx, StaticWidgetId, Ui, WidgetId, WidgetIdFn};
use crate::{primitive::Painter, util::ID, UIBuilder};
use std::{any::Any, marker::PhantomData};
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);
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter);
fn desired_size(&mut self, _: &mut SizeCtx) -> Size {
Size::default()
}
}
#[derive(Eq, Hash, PartialEq, Debug)]
pub struct WidgetId<W = ()> {
pub(super) ty: TypeId,
pub(super) id: ID,
_pd: PhantomData<W>,
impl Widget for () {
fn draw(&mut self, _: &mut Painter) {}
}
// TODO: temp
impl Clone for WidgetId {
fn clone(&self) -> Self {
Self {
ty: self.ty,
id: self.id.duplicate(),
_pd: self._pd,
pub struct WidgetTag;
pub struct FnTag;
pub trait WidgetLike<Tag> {
type Widget: 'static;
fn add(self, ui: &mut Ui) -> WidgetId<Self::Widget>;
fn with_id<W2>(
self,
f: impl FnOnce(&mut Ui, WidgetId<Self::Widget>) -> WidgetId<W2>,
) -> impl WidgetIdFn<W2>
where
Self: Sized,
{
move |ui| {
let id = self.add(ui);
f(ui, id)
}
}
}
impl<W> WidgetId<W> {
pub(super) fn new(id: ID, ty: TypeId) -> Self {
Self {
ty,
id,
_pd: PhantomData,
}
fn add_static(self, ui: &mut Ui) -> StaticWidgetId<Self::Widget>
where
Self: Sized,
{
self.add(ui).into_static()
}
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,
}
fn set_root(self, ui: &mut Ui)
where
Self: Sized,
{
ui.set_root(self);
}
}
pub trait WidgetLike {
type Widget;
fn add(self, ui: &mut UIBuilder) -> WidgetId<Self::Widget>;
}
/// 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<W: Widget>: FnOnce(&mut Ui) -> W {}
impl<W: Widget, F: FnOnce(&mut Ui) -> W> WidgetFn<W> for F {}
/// 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 for WidgetFn<F, W> {
impl<W: Widget, F: FnOnce(&mut Ui) -> W> WidgetLike<FnTag> for F {
type Widget = W;
fn add(self, ui: &mut UIBuilder) -> WidgetId<W> {
let w = (self.0)(ui);
ui.add(w).to_id()
fn add(self, ui: &mut Ui) -> WidgetId<W> {
self(ui).add(ui)
}
}
impl<W: Widget> WidgetLike for W {
impl<W: Widget> WidgetLike<WidgetTag> for W {
type Widget = W;
fn add(self, ui: &mut UIBuilder) -> WidgetId<W> {
ui.add(self).to_id()
}
}
impl<W> WidgetLike for WidgetId<W> {
type Widget = W;
fn add(self, _: &mut UIBuilder) -> WidgetId<W> {
self
}
}
impl<W> WidgetLike for WidgetArr<1, (W,)> {
type Widget = W;
fn add(self, _: &mut UIBuilder) -> WidgetId<W> {
let [id] = self.arr;
id.cast_type()
fn add(self, ui: &mut Ui) -> WidgetId<W> {
ui.add_widget(self)
}
}
pub struct WidgetArr<const LEN: usize, Ws> {
pub ui: UIBuilder,
pub arr: [WidgetId<()>; LEN],
pub arr: [WidgetId; LEN],
_pd: PhantomData<Ws>,
}
impl<const LEN: usize, Ws> WidgetArr<LEN, Ws> {
pub fn new(ui: UIBuilder, arr: [WidgetId<()>; LEN]) -> Self {
pub fn new(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> {
pub struct ArrTag;
pub trait WidgetArrLike<const LEN: usize, Tag> {
type Ws;
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<LEN, Self::Ws>;
fn ui(self, ui: &mut Ui) -> WidgetArr<LEN, Self::Ws>;
}
impl<const LEN: usize, Ws> WidgetArrLike<LEN> for WidgetArr<LEN, Ws> {
impl<const LEN: usize, Ws> WidgetArrLike<LEN, ArrTag> for WidgetArr<LEN, Ws> {
type Ws = Ws;
fn ui(self, _: &mut UIBuilder) -> WidgetArr<LEN, Ws> {
fn ui(self, _: &mut Ui) -> WidgetArr<LEN, Ws> {
self
}
}
impl<W: WidgetLike<WidgetTag>> WidgetArrLike<1, WidgetTag> for W {
type Ws = (W::Widget,);
fn ui(self, ui: &mut Ui) -> WidgetArr<1, (W::Widget,)> {
WidgetArr::new([self.add(ui).any()])
}
}
// I hate this language it's so bad why do I even use it
macro_rules! impl_widget_arr {
($n:expr;$($T:tt)*) => {
impl<$($T: WidgetLike,)*> WidgetArrLike<$n> for ($($T,)*) {
type Ws = ($($T::Widget,)*);
#[allow(unused_variables)]
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<$n, ($($T::Widget,)*)> {
($n:expr;$($W:ident)*) => {
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
};
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
impl<$($W: WidgetLike<$Tag>,$Tag,)*> WidgetArrLike<$n, ($($Tag,)*)> for ($($W,)*) {
type Ws = ($($W::Widget,)*);
fn ui(self, ui: &mut Ui) -> WidgetArr<$n, ($($W::Widget,)*)> {
#[allow(non_snake_case)]
let ($($T,)*) = self;
let ($($W,)*) = self;
WidgetArr::new(
ui.clone(),
[$($T.add(ui).cast_type(),)*],
[$($W.add(ui).cast_type(),)*],
)
}
}

103
src/layout/widgets.rs Normal file
View File

@@ -0,0 +1,103 @@
use crate::{
layout::{IdLike, Widget},
util::{DynBorrower, HashMap, Id, IdTracker},
};
#[derive(Default)]
pub struct Widgets {
ids: IdTracker,
map: HashMap<Id, WidgetData>,
}
pub struct WidgetData {
pub widget: Box<dyn Widget>,
pub label: String,
/// dynamic borrow checking
pub borrowed: bool,
}
impl Widgets {
pub fn new() -> Self {
Self {
ids: IdTracker::default(),
map: HashMap::default(),
}
}
pub fn get_dyn(&self, id: Id) -> Option<&dyn Widget> {
Some(self.map.get(&id)?.widget.as_ref())
}
pub fn get_dyn_mut(&mut self, id: Id) -> Option<&mut dyn Widget> {
Some(self.map.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 fn get_dyn_dynamic(&self, id: Id) -> WidgetWrapper<'_> {
// SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable
#[allow(mutable_transmutes)]
let data = unsafe {
std::mem::transmute::<&WidgetData, &mut WidgetData>(self.map.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<W: Widget>(&self, id: &impl IdLike<W>) -> Option<&W> {
self.get_dyn(id.id())?.as_any().downcast_ref()
}
pub fn get_mut<W: Widget>(&mut self, id: &impl IdLike<W>) -> Option<&mut W> {
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
}
pub fn insert<W: Widget>(&mut self, id: Id, widget: W) {
self.insert_any(id, Box::new(widget), std::any::type_name::<W>().to_string());
}
pub fn data(&self, id: &Id) -> Option<&WidgetData> {
self.map.get(id)
}
pub fn label(&self, id: &Id) -> &String {
&self.data(id).unwrap().label
}
pub fn data_mut(&mut self, id: &Id) -> Option<&mut WidgetData> {
self.map.get_mut(id)
}
pub fn insert_any(&mut self, id: Id, widget: Box<dyn Widget>, label: String) {
self.map.insert(
id,
WidgetData {
widget,
label,
borrowed: false,
},
);
}
pub fn delete(&mut self, id: Id) {
self.map.remove(&id);
self.ids.free(id);
}
pub fn reserve(&mut self) -> Id {
self.ids.next()
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;

View File

@@ -1,17 +1,20 @@
#![feature(macro_metavar_expr_concat)]
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_from)]
#![feature(trait_alias)]
#![feature(generic_const_exprs)]
#![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(const_cmp)]
#![feature(const_destruct)]
mod layout;
mod render;
mod util;
mod base;
pub mod core;
pub mod layout;
pub mod render;
pub mod util;
pub use layout::*;
pub use render::*;
pub use base::*;
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
pub mod prelude {
pub use crate::core::*;
pub use crate::layout::*;
pub use crate::render::*;
}

View File

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

View File

@@ -1,6 +1,9 @@
use std::num::NonZero;
use crate::{
render::{data::PrimitiveInstance, util::ArrBuf},
UI,
layout::Ui,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::HashMap,
};
use data::WindowUniform;
use wgpu::{
@@ -10,41 +13,100 @@ use wgpu::{
use winit::dpi::PhysicalSize;
mod data;
pub mod primitive;
mod primitive;
mod texture;
mod util;
pub use data::{Mask, MaskIdx};
pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UIRenderNode {
bind_group_layout: BindGroupLayout,
bind_group: BindGroup,
pub struct UiRenderer {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
rsc_layout: BindGroupLayout,
rsc_group: BindGroup,
pipeline: RenderPipeline,
layers: HashMap<usize, RenderLayer>,
active: Vec<usize>,
window_buffer: Buffer,
instance: ArrBuf<PrimitiveInstance>,
data: ArrBuf<u32>,
textures: GpuTextures,
masks: ArrBuf<Mask>,
}
impl UIRenderNode {
struct RenderLayer {
instance: ArrBuf<PrimitiveInstance>,
primitives: PrimitiveBuffers,
primitive_group: BindGroup,
}
impl UiRenderer {
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);
pass.set_bind_group(0, &self.uniform_group, &[]);
pass.set_bind_group(2, &self.rsc_group, &[]);
for i in &self.active {
let layer = &self.layers[i];
if layer.instance.len() == 0 {
continue;
}
pass.set_bind_group(1, &layer.primitive_group, &[]);
pass.set_vertex_buffer(0, layer.instance.buffer.slice(..));
pass.draw(0..4, 0..layer.instance.len() as u32);
}
}
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 update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) {
self.active.clear();
for (i, ulayer) in ui.data.layers.iter_mut() {
self.active.push(i);
let primitives = &mut ulayer.primitives;
for change in primitives.apply_free() {
if let Some(inst) = ui.data.active.get_mut(&change.id) {
for h in &mut inst.primitives {
if h.inst_idx == change.old {
h.inst_idx = change.new;
break;
}
}
}
}
let rlayer = self.layers.entry(i).or_insert_with(|| {
let primitives = PrimitiveBuffers::new(device);
let primitive_group =
Self::primitive_group(device, &self.primitive_layout, primitives.buffers());
RenderLayer {
instance: ArrBuf::new(
device,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
"instance",
),
primitives,
primitive_group,
}
});
if primitives.updated {
rlayer
.instance
.update(device, queue, primitives.instances());
rlayer.primitives.update(device, queue, primitives.data());
rlayer.primitive_group = Self::primitive_group(
device,
&self.primitive_layout,
rlayer.primitives.buffers(),
)
}
}
if self.textures.update(&mut ui.data.textures) {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks)
}
if ui.data.masks.changed {
ui.data.masks.changed = false;
self.masks.update(device, queue, &ui.data.masks[..]);
}
}
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
@@ -55,7 +117,12 @@ impl UIRenderNode {
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
pub fn new(
device: &Device,
queue: &Queue,
config: &SurfaceConfiguration,
limits: UiLimits,
) -> Self {
let shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("UI Shape Shader"),
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
@@ -63,36 +130,31 @@ impl UIRenderNode {
let window_uniform = WindowUniform::default();
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Camera Buffer"),
label: Some("window"),
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,
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("window"),
});
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| {
BindGroupLayoutEntry {
binding: 1,
binding: i as u32,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
@@ -100,16 +162,24 @@ impl UIRenderNode {
min_binding_size: None,
},
count: None,
},
],
label: Some("camera_bind_group_layout"),
}
}),
label: Some("primitive"),
});
let bind_group = Self::bind_group(device, &bind_group_layout, &window_buffer, &data.buffer);
let tex_manager = GpuTextures::new(device, queue);
let masks = ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks",
);
let rsc_layout = Self::rsc_layout(device, &limits);
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks);
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
@@ -151,34 +221,133 @@ impl UIRenderNode {
});
Self {
bind_group_layout,
bind_group,
uniform_group,
primitive_layout,
rsc_layout,
rsc_group,
pipeline,
window_buffer,
instance,
data,
layers: HashMap::default(),
active: Vec::new(),
textures: tex_manager,
masks,
}
}
pub fn bind_group(
fn bind_group_0(
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(),
}],
label: Some("ui window"),
})
}
fn primitive_group(
device: &Device,
layout: &BindGroupLayout,
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
}),
label: Some("ui primitives"),
})
}
fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: false },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: Some(NonZero::new(limits.max_textures).unwrap()),
},
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
count: Some(NonZero::new(limits.max_samplers).unwrap()),
},
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("ui rsc"),
})
}
fn rsc_group(
device: &Device,
layout: &BindGroupLayout,
tex_manager: &GpuTextures,
masks: &ArrBuf<Mask>,
) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor {
layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: window_buffer.as_entire_binding(),
resource: BindingResource::TextureViewArray(&tex_manager.views()),
},
BindGroupEntry {
binding: 1,
resource: data.as_entire_binding(),
resource: BindingResource::SamplerArray(&tex_manager.samplers()),
},
BindGroupEntry {
binding: 2,
resource: masks.buffer.as_entire_binding(),
},
],
label: Some("ui_bind_group"),
label: Some("ui rsc"),
})
}
pub fn view_count(&self) -> usize {
self.textures.view_count()
}
}
pub struct UiLimits {
max_textures: u32,
max_samplers: u32,
}
impl Default for UiLimits {
fn default() -> Self {
Self {
max_textures: 100000,
max_samplers: 1000,
}
}
}
impl UiLimits {
pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 {
self.max_textures + self.max_samplers
}
pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 {
self.max_samplers
}
}

281
src/render/primitive.rs Normal file
View File

@@ -0,0 +1,281 @@
use std::ops::{Deref, DerefMut};
use crate::{
layout::{Color, UiRegion},
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
},
util::Id,
};
use bytemuck::Pod;
use wgpu::*;
pub struct Primitives {
instances: Vec<PrimitiveInstance>,
assoc: Vec<Id>,
data: PrimitiveData,
free: Vec<usize>,
pub updated: bool,
}
impl Default for Primitives {
fn default() -> Self {
Self {
instances: Default::default(),
assoc: Default::default(),
data: Default::default(),
free: Vec::new(),
updated: true,
}
}
}
pub trait Primitive: Pod {
const BINDING: u32;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
}
macro_rules! primitives {
($($name:ident: $ty:ty => $binding:expr,)*) => {
#[derive(Default)]
pub struct PrimitiveData {
$($name: PrimitiveVec<$ty>,)*
}
pub struct PrimitiveBuffers {
$($name: ArrBuf<$ty>,)*
}
impl PrimitiveBuffers {
pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) {
$(self.$name.update(device, queue, &data.$name);)*
}
}
impl PrimitiveBuffers {
pub const LEN: usize = primitives!(@count $($name)*);
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
[
$((<$ty>::BINDING, &self.$name.buffer),)*
]
}
pub fn new(device: &Device) -> Self {
Self {
$($name: ArrBuf::new(
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
stringify!($name),
),)*
}
}
}
impl PrimitiveData {
pub fn clear(&mut self) {
$(self.$name.clear();)*
}
pub fn free(&mut self, binding: u32, idx: usize) {
match binding {
$(<$ty>::BINDING => self.$name.free(idx),)*
_ => unreachable!()
}
}
}
$(
unsafe impl bytemuck::Pod for $ty {}
unsafe impl bytemuck::Zeroable for $ty {}
impl Primitive for $ty {
const BINDING: u32 = $binding;
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
&mut data.$name
}
}
)*
};
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t:tt) => { 1 };
}
pub struct PrimitiveInst<P> {
pub id: Id,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
}
impl Primitives {
pub fn write<P: Primitive>(
&mut self,
layer: usize,
PrimitiveInst {
id,
primitive,
region,
mask_idx,
}: PrimitiveInst<P>,
) -> PrimitiveHandle {
let vec = P::vec(&mut self.data);
let i = vec.add(primitive);
let inst = PrimitiveInstance {
region,
idx: i as u32,
mask_idx,
binding: P::BINDING,
};
let inst_i = if let Some(i) = self.free.pop() {
self.instances[i] = inst;
self.assoc[i] = id;
i
} else {
let i = self.instances.len();
self.instances.push(inst);
self.assoc.push(id);
i
};
PrimitiveHandle::new::<P>(layer, inst_i, i)
}
/// returns (old index, new index)
pub fn apply_free(&mut self) -> impl Iterator<Item = PrimitiveChange> {
self.free.sort_by(|a, b| b.cmp(a));
self.free.drain(..).filter_map(|i| {
self.instances.swap_remove(i);
self.assoc.swap_remove(i);
if i == self.instances.len() {
return None;
}
let id = self.assoc[i];
let old = self.instances.len();
Some(PrimitiveChange { id, old, new: i })
})
}
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
self.data.free(h.binding, h.data_idx);
self.free.push(h.inst_idx);
self.instances[h.inst_idx].mask_idx
}
pub fn data(&self) -> &PrimitiveData {
&self.data
}
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
&self.instances
}
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
self.updated = true;
&mut self.instances[h.inst_idx].region
}
}
pub struct PrimitiveChange {
pub id: Id,
pub old: usize,
pub new: usize,
}
#[derive(Debug)]
pub struct PrimitiveHandle {
pub layer: usize,
pub inst_idx: usize,
pub data_idx: usize,
pub binding: u32,
}
impl PrimitiveHandle {
fn new<P: Primitive>(layer: usize, inst_idx: usize, data_idx: usize) -> Self {
Self {
layer,
inst_idx,
data_idx,
binding: P::BINDING,
}
}
}
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
);
#[repr(C)]
#[derive(Copy, Clone)]
pub struct RectPrimitive {
pub color: Color<u8>,
pub radius: f32,
pub thickness: f32,
pub inner_radius: f32,
}
impl RectPrimitive {
pub fn color(color: Color<u8>) -> Self {
Self {
color,
radius: 0.0,
thickness: 0.0,
inner_radius: 0.0,
}
}
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct TexturePrimitive {
pub view_idx: u32,
pub sampler_idx: u32,
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
}
impl<T> PrimitiveVec<T> {
pub fn new() -> Self {
Self {
vec: Vec::new(),
free: Vec::new(),
}
}
pub fn add(&mut self, t: T) -> usize {
if let Some(i) = self.free.pop() {
self.vec[i] = t;
i
} else {
let i = self.vec.len();
self.vec.push(t);
i
}
}
pub fn free(&mut self, i: usize) {
self.free.push(i);
}
pub fn clear(&mut self) {
self.free.clear();
self.vec.clear();
}
}
impl<T> Default for PrimitiveVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Deref for PrimitiveVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.vec
}
}
impl<T> DerefMut for PrimitiveVec<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.vec
}
}

View File

@@ -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> {}

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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(&region);
self.draw(node);
self.region = old;
}
pub fn finish(self) -> Primitives {
self.primitives
}
}

View File

@@ -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);

View File

@@ -1,7 +1,41 @@
const RECT: u32 = 0u;
const TEXTURE: u32 = 1u;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@group(0) @binding(1)
var<storage> data: array<u32>;
@group(1) @binding(RECT)
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
struct Rect {
color: u32,
radius: f32,
thickness: f32,
inner_radius: f32,
}
struct TextureInfo {
view_idx: u32,
sampler_idx: u32,
}
struct Mask {
top_left: UiVec2,
bot_right: UiVec2,
}
struct UiVec2 {
rel: vec2<f32>,
abs: vec2<f32>,
}
@group(2) @binding(0)
var views: binding_array<texture_2d<f32>>;
@group(2) @binding(1)
var samplers: binding_array<sampler>;
@group(2) @binding(2)
var<storage> masks: array<Mask>;
struct WindowUniform {
dim: vec2<f32>,
@@ -12,25 +46,24 @@ struct InstanceInput {
@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,
@location(4) binding: u32,
@location(5) idx: u32,
@location(6) mask_idx: u32,
}
struct VertexOutput {
@location(0) pointer: u32,
@location(1) top_left: vec2<f32>,
@location(2) bot_right: vec2<f32>,
@location(0) top_left: vec2<f32>,
@location(1) bot_right: vec2<f32>,
@location(2) uv: vec2<f32>,
@location(3) binding: u32,
@location(4) idx: u32,
@location(5) mask_idx: u32,
@builtin(position) clip_position: vec4<f32>,
};
struct Region {
pos: vec2<f32>,
uv: vec2<f32>,
top_left: vec2<f32>,
bot_right: vec2<f32>,
}
@@ -42,19 +75,22 @@ fn vs_main(
) -> 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 top_left = floor(in.top_left_anchor * window.dim) + floor(in.top_left_offset);
let bot_right = floor(in.bottom_right_anchor * window.dim) + floor(in.bottom_right_offset);
let size = bot_right - top_left;
var pos = top_left + vec2<f32>(
let uv = vec2<f32>(
f32(vi % 2u),
f32(vi / 2u)
) * size;
pos = pos / window.dim * 2.0 - 1.0;
);
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.pointer = in.pointer;
out.uv = uv;
out.binding = in.binding;
out.idx = in.idx;
out.top_left = top_left;
out.bot_right = bot_right;
out.mask_idx = in.mask_idx;
return out;
}
@@ -64,24 +100,37 @@ 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]),
));
let region = Region(pos, in.uv, in.top_left, in.bot_right);
let i = in.idx;
var color: vec4<f32>;
switch in.binding {
case RECT: {
color = draw_rounded_rect(region, rects[i]);
}
case TEXTURE: {
color = draw_texture(region, textures[i]);
}
default: {
color = vec4(1.0, 0.0, 1.0, 1.0);
}
default: {}
}
return vec4(1.0, 0.0, 1.0, 1.0);
if in.mask_idx != 4294967295u {
let mask = masks[in.mask_idx];
let top_left = floor(mask.top_left.rel * window.dim) + floor(mask.top_left.abs);
let bot_right = floor(mask.bot_right.rel * window.dim) + floor(mask.bot_right.abs);
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
color *= 0.0;
}
}
return color;
}
fn draw_rounded_rect(region: Region, rect: RoundedRect) -> vec4<f32> {
// TODO: this seems really inefficient (per frag indexing)?
fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
let edge = 0.5;
@@ -106,5 +155,6 @@ fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner:
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;
return length(max(q, vec2(0.0))) - radius;
}

129
src/render/texture.rs Normal file
View File

@@ -0,0 +1,129 @@
use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *};
use crate::layout::{TextureUpdate, Textures};
pub struct GpuTextures {
device: Device,
queue: Queue,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
null_view: TextureView,
no_views: Vec<TextureView>,
}
impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool {
let mut changed = false;
for update in textures.updates() {
changed = true;
match update {
TextureUpdate::Push(image) => self.push(image),
TextureUpdate::Set(i, image) => self.set(i, image),
TextureUpdate::SetFree => self.view_count += 1,
TextureUpdate::Free(i) => self.free(i),
TextureUpdate::PushFree => self.push_free(),
}
}
changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let view = self.create_view(image);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
self.view_count -= 1;
self.views[i as usize] = self.null_view.clone();
}
fn push(&mut self, image: &DynamicImage) {
self.view_count += 1;
let view = self.create_view(image);
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.views.push(self.null_view.clone());
}
fn create_view(&self, image: &DynamicImage) -> TextureView {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data(
&self.queue,
&TextureDescriptor {
label: None,
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,
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
);
texture.create_view(&TextureViewDescriptor::default())
}
pub fn new(device: &Device, queue: &Queue) -> Self {
let null_view = null_texture_view(device);
Self {
device: device.clone(),
queue: queue.clone(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()],
null_view,
view_count: 0,
}
}
pub fn views(&self) -> Vec<&TextureView> {
if self.views.is_empty() {
&self.no_views
} else {
&self.views
}
.iter()
.by_ref()
.collect()
}
pub fn samplers(&self) -> Vec<&Sampler> {
self.samplers.iter().by_ref().collect()
}
pub fn view_count(&self) -> usize {
self.view_count
}
}
pub fn null_texture_view(device: &Device) -> TextureView {
device
.create_texture(&TextureDescriptor {
label: Some("null"),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
}
pub fn default_sampler(device: &Device) -> Sampler {
device.create_sampler(&SamplerDescriptor::default())
}

View File

@@ -32,7 +32,7 @@ impl<T: Pod> ArrBuf<T> {
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
let mut size = size as u64;
if usage.contains(BufferUsages::STORAGE) {
size = size.max(1);
size = size.max(std::mem::size_of::<T>() as u64);
}
device.create_buffer(&BufferDescriptor {
label: Some(label),

View File

@@ -31,6 +31,7 @@ impl ApplicationHandler for App {
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.client.as_mut().unwrap().event(event, event_loop);
let client = self.client.as_mut().unwrap();
client.event(event, event_loop);
}
}

BIN
src/testing/assets/sungals.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

79
src/testing/input.rs Normal file
View File

@@ -0,0 +1,79 @@
use iris::{
core::{CursorState, Modifiers},
layout::Vec2,
};
use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent},
keyboard::{Key, NamedKey},
};
use crate::testing::Client;
#[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 delta = match *delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
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 Client {
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
}
}

View File

@@ -1,11 +1,17 @@
use std::sync::Arc;
use app::App;
use gui::{primitive::Axis, RoundedRect, UIColor, WidgetArrUtil, WidgetUtil, UI};
use arboard::Clipboard;
use cosmic_text::Family;
use iris::prelude::*;
use render::Renderer;
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
use crate::testing::input::Input;
use len_fns::*;
mod app;
mod input;
mod render;
pub fn main() {
@@ -14,53 +20,276 @@ pub fn main() {
pub struct Client {
renderer: Renderer,
input: Input,
ui: Ui,
info: WidgetId<Text>,
focus: Option<WidgetId<TextEdit>>,
clipboard: Clipboard,
}
#[derive(Eq, PartialEq, Hash, Clone)]
struct Submit;
impl DefaultEvent for Submit {
type Data = ();
}
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(
let renderer = Renderer::new(window);
let mut ui = Ui::new();
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)),
(
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),
rrect.color(Color::ORANGE),
rrect.color(Color::LIME).pad(10.0),
)
.span(Axis::X, [1, 3]),
rect.color(UIColor::GREEN),
.span(Dir::RIGHT)
.width(rest(2)),
rrect.color(Color::YELLOW),
)
.span(Axis::Y, [3, 1])
.pad(10),
);
ui.widgets.get_mut(&handle).unwrap().color = UIColor::MAGENTA;
renderer.update(&ui);
Self { renderer }
.span(Dir::RIGHT)
.pad(10)
.width(rest(3)),
)
.span(Dir::RIGHT)
.add_static(&mut ui);
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_static(&mut ui);
let span_add = Span::empty(Dir::RIGHT).add_static(&mut ui);
let add_button = rect(Color::LIME)
.radius(30)
.on(CursorSense::click(), move |ctx: &mut Client, _| {
let child = ctx
.ui
.add(image(include_bytes!("assets/sungals.png")).center())
.any();
ctx.ui[span_add].children.push(child);
})
.sized((150, 150))
.align(Align::BotRight);
let del_button = rect(Color::RED)
.radius(30)
.on(CursorSense::click(), move |ctx: &mut Client, _| {
ctx.ui[span_add].children.pop();
})
.sized((150, 150))
.align(Align::BotLeft);
let span_add_test = (span_add, add_button, del_button)
.stack()
.add_static(&mut ui);
let main = pad_test.pad(10).add_static(&mut ui);
let btext = |content| text(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(),
text("pretty cool right?").size(50),
)
.span(Dir::DOWN)
.add_static(&mut ui);
let texts = Span::empty(Dir::DOWN).gap(10).add_static(&mut ui);
let msg_area = (Rect::new(Color::SKY), texts.scroll().masked()).stack();
let add_text = text("add")
.editable()
.text_align(Align::Left)
.size(30)
.id_on(CursorSense::click(), |id, client: &mut Client, ctx| {
client.ui.text(id).select(ctx.cursor, ctx.size);
client.focus = Some(id.clone());
})
.id_on(Submit, move |id, client: &mut Client, _| {
let content = client.ui.text(id).take();
let text = text(content)
.editable()
.size(30)
.text_align(Align::Left)
.wrap(true)
.id_on(CursorSense::click(), |id, client: &mut Client, ctx| {
client.ui.text(id).select(ctx.cursor, ctx.size);
client.focus = Some(id.clone());
});
let msg_box = (rect(Color::WHITE.darker(0.5)), text)
.stack()
.size(StackSize::Child(1))
.add(&mut client.ui);
client.ui[texts].children.push(msg_box.any());
})
.add(&mut ui);
let text_edit_scroll = (
msg_area,
(
Rect::new(Color::WHITE.darker(0.9)),
(
add_text.clone().width(rest(1)),
Rect::new(Color::GREEN)
.on(CursorSense::click(), move |client: &mut Client, _| {
client.run_event(&add_text, Submit, ());
})
.sized((40, 40)),
)
.span(Dir::RIGHT)
.pad(10),
)
.stack()
.size(StackSize::Child(1))
.offset_layer(1)
.align(Align::Bot),
)
.span(Dir::DOWN)
.add_static(&mut ui);
let switch_button = |color, to, label| {
let rect = rect(color)
.id_on(CursorSense::click(), move |id, ui: &mut Ui, _| {
ui[main].inner.set_static(to);
ui[id].color = color.darker(0.3);
})
.edit_on(
CursorSense::HoverStart | CursorSense::unclick(),
move |r, _| {
r.color = color.brighter(0.2);
},
)
.edit_on(CursorSense::HoverEnd, move |r, _| {
r.color = color;
});
(rect, text(label).size(30)).stack()
};
let tabs = (
switch_button(Color::RED, pad_test.any(), "pad"),
switch_button(Color::GREEN, span_test.any(), "span"),
switch_button(Color::BLUE, span_add_test.any(), "image span"),
switch_button(Color::MAGENTA, text_test.any(), "text layout"),
switch_button(
Color::YELLOW.mul_rgb(0.5),
text_edit_scroll.any(),
"text edit scroll",
),
)
.span(Dir::RIGHT);
let info = text("").add(&mut ui);
let info_sect = info.clone().pad(10).align(Align::Right);
((tabs.height(40), main).span(Dir::DOWN), info_sect)
.stack()
.set_root(&mut ui);
Self {
renderer,
input: Input::default(),
ui,
info,
focus: None,
clipboard: Clipboard::new().unwrap(),
}
}
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let input_changed = self.input.event(&event);
let cursor_state = self.cursor_state().clone();
if let Some(focus) = &self.focus
&& cursor_state.buttons.left.is_start()
{
self.ui.text(focus).deselect();
self.focus = None;
}
if input_changed {
let window_size = self.window_size();
self.run_sensors(&cursor_state, window_size);
self.ui.run_sensors(&cursor_state, window_size);
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => self.renderer.draw(),
WindowEvent::Resized(size) => self.renderer.resize(&size),
WindowEvent::RedrawRequested => {
self.ui.update();
self.renderer.update(&mut self.ui);
self.renderer.draw()
}
WindowEvent::Resized(size) => {
self.ui.resize((size.width, size.height));
self.renderer.resize(&size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &self.focus
&& event.state.is_pressed()
{
let mut text = self.ui.text(sel);
match text.apply_event(&event, &self.input.modifiers) {
TextInputResult::Unfocus => {
self.focus = None;
}
TextInputResult::Submit => {
self.run_event(&sel.clone(), Submit, ());
}
TextInputResult::Paste => {
if let Ok(t) = self.clipboard.get_text() {
text.insert(&t);
}
}
TextInputResult::Unused | TextInputResult::Used => (),
}
}
}
_ => (),
}
let new = format!(
"widgets: {}\nactive:{}\nviews: {}",
self.ui.num_widgets(),
self.ui.active_widgets(),
self.renderer.ui.view_count()
);
if new != *self.ui[&self.info].content {
*self.ui[&self.info].content = new;
}
if self.ui.needs_redraw() {
self.renderer.window().request_redraw();
}
self.input.end_frame();
}
}
impl UiCtx for Client {
fn ui(&mut self) -> &mut Ui {
&mut self.ui
}
}

View File

@@ -1,47 +1,51 @@
use gui::{UIRenderNode, UI};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::util::StagingBelt;
use iris::{
layout::Ui,
render::{UiLimits, UiRenderer},
};
use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: wgpu::Color = wgpu::Color::BLACK;
pub const CLEAR_COLOR: Color = Color::BLACK;
pub struct Renderer {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
encoder: wgpu::CommandEncoder,
window: Arc<Window>,
surface: Surface<'static>,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
ui_node: UIRenderNode,
pub ui: UiRenderer,
}
impl Renderer {
pub fn update(&mut self, ui: &UI) {
self.ui_node.update(&self.device, &self.queue, ui);
pub fn update(&mut self, updates: &mut Ui) {
self.ui.update(&self.device, &self.queue, updates);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
.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(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(CLEAR_COLOR),
store: wgpu::StoreOp::Store,
ops: Operations {
load: LoadOp::Clear(CLEAR_COLOR),
store: StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
self.ui_node.draw(render_pass);
self.ui.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
@@ -54,11 +58,11 @@ impl Renderer {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui_node.resize(size, &self.queue);
self.ui.resize(size, &self.queue);
}
fn create_encoder(device: &wgpu::Device) -> wgpu::CommandEncoder {
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
fn create_encoder(device: &Device) -> CommandEncoder {
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
@@ -66,8 +70,8 @@ impl Renderer {
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
});
@@ -76,18 +80,28 @@ impl Renderer {
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
..Default::default()
},
..Default::default()
})
.block_on()
@@ -101,12 +115,12 @@ impl Renderer {
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
present_mode: PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
@@ -117,7 +131,7 @@ impl Renderer {
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UIRenderNode::new(&device, &config);
let shape_pipeline = UiRenderer::new(&device, &queue, &config, ui_limits);
Self {
surface,
@@ -126,7 +140,12 @@ impl Renderer {
config,
encoder,
staging_belt,
ui_node: shape_pipeline,
ui: shape_pipeline,
window,
}
}
pub fn window(&self) -> &Window {
self.window.as_ref()
}
}

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

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

35
src/util/borrow.rs Normal file
View File

@@ -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
}
}

30
src/util/change.rs Normal file
View File

@@ -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,
}
}
}

View File

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

87
src/util/math.rs Normal file
View File

@@ -0,0 +1,87 @@
use std::ops::*;
pub const trait LerpUtil {
fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Self;
}
pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self;
}
impl const DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs;
if res.is_nan() { other } else { res }
}
}
impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const
LerpUtil for T
{
/// linear interpolation
/// from * (1.0 - self) + to * self
fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self
}
/// inverse of lerp
fn lerp_inv(self, from: Self, to: Self) -> Self {
(self - from).div_or(to - from, 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::*;
impl const $op for $T {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
Self {
$($field: self.$field.$fn(rhs.$field),)*
}
}
}
impl const $opa for $T {
fn $fna(&mut self, rhs: Self) {
$(self.$field.$fna(rhs.$field);)*
}
}
impl const $op<f32> for $T {
type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output {
Self {
$($field: self.$field.$fn(rhs),)*
}
}
}
impl const $op<$T> for f32 {
type Output = $T;
fn $fn(self, rhs: $T) -> Self::Output {
$T {
$($field: self.$fn(rhs.$field),)*
}
}
}
impl const $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) {
$(self.$field.$fna(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;

View File

@@ -1,3 +1,16 @@
mod arena;
mod borrow;
mod change;
mod id;
mod math;
mod refcount;
pub use id::*;
pub(crate) use arena::*;
pub(crate) use borrow::*;
pub use change::*;
pub(crate) use id::*;
pub(crate) use math::*;
pub(crate) use refcount::*;
pub type HashMap<K, V> = fxhash::FxHashMap<K, V>;
pub type HashSet<K> = fxhash::FxHashSet<K>;

26
src/util/refcount.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::sync::{
Arc,
atomic::{AtomicU32, Ordering},
};
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
}
}
impl Clone for RefCounter {
fn clone(&self) -> Self {
self.0.fetch_add(1, Ordering::Release);
Self(self.0.clone())
}
}