Compare commits

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:26:19 -04:00
iris fae21a1991 Build on current nightly 2026-09-13 00:24:29 -04:00
iris 7b54aaf3c4 readme 2026-01-29 16:39:19 -05:00
iris 17c436d944 stuff 2026-01-22 23:33:46 -05:00
iris a592318a6f impl idlike for widgetview 2026-01-20 18:11:21 -05:00
iris 796bc41752 didn't actually remove state on widget remove (mem leak) 2026-01-19 21:39:13 -05:00
iris 7bafb04a34 widget state 2026-01-19 20:39:58 -05:00
iris 06dd015092 finished moving out render_state 2026-01-19 18:00:24 -05:00
iris 79813db3ba work 2026-01-12 18:40:27 -05:00
iris a9c76e4326 lol bounds 2026-01-05 17:50:18 -05:00
iris 32e45e9238 griefed last commit 2026-01-05 17:20:11 -05:00
iris 2fadfe4b82 app event stuff bruh 2026-01-05 17:16:56 -05:00
iris d11107f965 widget view 2026-01-05 17:01:09 -05:00
iris 07de7c8722 update deps 2026-01-04 14:45:18 -05:00
iris f2ac6f195f remove typed stuff / just specify rsc if needed 2026-01-03 18:06:05 -05:00
iris 59901b6580 tasks initial impl (still working on task_on trait method) 2026-01-03 16:26:23 -05:00
iris 5da1e9e767 separate state from rsc 2026-01-01 22:18:08 -05:00
iris 462c0e6416 move event data out of widgetdata 2025-12-23 15:42:04 -05:00
iris 54534c4c34 bruh vsync griefs my laptop 2025-12-20 18:25:22 -05:00
iris f0fe671bd8 fix illegal instruction during handle ptr creation in release mode 2025-12-20 17:58:10 -05:00
iris 1ccf947220 convenience methods for span 2025-12-20 01:38:56 -05:00
iris c00ded78c0 switch away from handles to refs that must be upgraded once 2025-12-20 01:36:07 -05:00
iris 32ca4ec5a6 warning 2025-12-20 00:27:08 -05:00
iris fabc7d0b90 abuse macros.. 2025-12-20 00:26:08 -05:00
iris bae17235c6 better global state structure? 2025-12-19 21:54:48 -05:00
iris 30bc55c78e remove state generic from a lot of things 2025-12-17 21:37:55 -05:00
iris 7e6369029f trust + fix redraw bug 2025-12-17 01:16:28 -05:00
iris 70ac0fbcb2 typed stuff 2025-12-17 00:55:36 -05:00
iris 1363f31fcd FIX SIZE CACHE 2025-12-17 00:09:20 -05:00
iris ecbb9e56e2 small QOL 2025-12-16 20:24:21 -05:00
iris ac9571b29f small changes 2025-12-16 18:38:06 -05:00
iris 71f3beaf94 refactor painter 2025-12-16 00:48:14 -05:00
iris 2183fbd3cb make painter not stupid (size ctx is kinda tho) 2025-12-16 00:26:25 -05:00
iris 486ed0ffd7 prelude macro visibility 2025-12-15 23:15:52 -05:00
iris 8d1a810483 macro goodness 2025-12-15 23:11:32 -05:00
iris 0b8a93c5ce RE ADD CONTEXT 2025-12-15 21:50:53 -05:00
iris dc2be7f688 refactor out typemap 2025-12-15 16:25:12 -05:00
iris 9d8ca8fa72 refactor events 2025-12-12 02:02:54 -05:00
iris a2a32b4322 ctx inference holy 2025-12-12 01:50:08 -05:00
iris 37b1987aa8 remove modules and have single event manager (atomics feature parity + preparation for local state) 2025-12-12 01:46:24 -05:00
iris a708813ce7 idk work (r + h) 2025-12-11 23:05:27 -05:00
iris a70d09e162 lol 2025-12-11 16:24:07 -05:00
iris 966b6a2ac2 proper widgetid + slot vec instead of map 2025-12-11 16:23:14 -05:00
iris 2dad409300 handles (tuple) 2025-12-11 07:30:59 -05:00
iris 36668c82f4 strong & weak widgets 2025-12-11 07:16:06 -05:00
iris a85e129026 move everything out of layout 2025-12-11 05:48:29 -05:00
iris 174c447706 switch to macro 2025-12-11 05:31:34 -05:00
iris 2dc5b0f62c refactor project structure (start of redoing atomic branch without atomics) 2025-12-11 05:25:58 -05:00
114 changed files with 5762 additions and 4243 deletions

No files matched your search

Generated
+763 -465
View File
File diff suppressed because it is too large. Load diff
+30 -8
View File
@@ -1,19 +1,41 @@
[package]
name = "iris"
version = "0.1.0"
edition = "2024"
default-run = "test"
version.workspace = true
edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
iris-core = { workspace = true }
iris-macro = { workspace = true }
parley = { workspace = true }
winit = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] }
pollster = { workspace = true }
wgpu = { workspace = true }
image = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
[dev-dependencies]
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
[workspace]
members = ["core", "macro"]
[workspace.package]
version = "0.1.0"
edition = "2024"
[workspace.dependencies]
pollster = "0.4.0"
winit = "0.30.12"
wgpu = "27.0.1"
wgpu = "28.0.0"
bytemuck = "1.23.1"
image = "0.25.6"
cosmic-text = "0.15.0"
unicode-segmentation = "1.12.0"
parley = "0.11.1"
swash = "0.2.10"
fxhash = "0.2.1"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
arboard = "3.6.1"
iris-core = { path = "core" }
iris-macro = { path = "macro" }
tokio = "1.49.0"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "iris-core"
version.workspace = true
edition.workspace = true
[dependencies]
wgpu = { workspace = true }
bytemuck ={ workspace = true }
image = { workspace = true }
parley = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true }
+23
View File
@@ -0,0 +1,23 @@
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
pub trait WidgetAttr<Rsc, W: ?Sized> {
type Input;
fn run(rsc: &mut Rsc, id: WeakWidget<W>, input: Self::Input);
}
pub trait Attrable<Rsc, W: ?Sized, Tag> {
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>;
}
impl<Rsc: UiRsc, WL: WidgetLike<Rsc, Tag>, Tag> Attrable<Rsc, WL::Widget, Tag> for WL {
fn attr<A: WidgetAttr<Rsc, WL::Widget>>(
self,
input: A::Input,
) -> impl WidgetIdFn<Rsc, WL::Widget> {
|rsc| {
let id = self.add(rsc);
A::run(rsc, id, input);
id
}
}
}
+18
View File
@@ -0,0 +1,18 @@
use crate::{HasEvents, WeakWidget, Widget};
pub struct EventCtx<'a, Rsc: HasEvents, Data> {
pub state: &'a mut Rsc::State,
pub data: Data,
}
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> {
pub widget: WeakWidget<W>,
pub state: &'a mut Rsc::State,
pub data: Data,
}
impl<Rsc: HasEvents, Data, W: Widget> EventIdCtx<'_, Rsc, Data, W> {
pub fn widget<'a>(&self, rsc: &'a mut Rsc) -> &'a mut W {
&mut rsc.ui_mut().widgets[self.widget]
}
}
+157
View File
@@ -0,0 +1,157 @@
use crate::{
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
WeakWidget, WidgetEventFn, WidgetId,
util::{HashMap, HashSet, TypeMap},
};
use std::{any::TypeId, rc::Rc};
pub struct EventManager<Rsc> {
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
types: TypeMap<dyn EventManagerLike<Rsc>>,
}
impl<Rsc> Default for EventManager<Rsc> {
fn default() -> Self {
Self {
widget_to_types: Default::default(),
types: Default::default(),
}
}
}
impl<Rsc: HasEvents + 'static> EventManager<Rsc> {
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<Rsc, E::Event> {
self.types.type_or_default()
}
pub fn register<I: IdLike + 'static, E: EventLike>(
&mut self,
id: I,
event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, I::Widget>,
) {
let i = id.id();
self.get_type::<E>().register(id, event, f);
self.widget_to_types
.entry(i)
.or_default()
.insert(Self::type_key::<E>());
}
pub fn type_key<E: EventLike>() -> TypeId {
TypeId::of::<TypeEventManager<Rsc, E::Event>>()
}
}
pub trait EventsLike {
fn remove(&mut self, id: WidgetId);
fn draw(&mut self, active: &ActiveData);
fn undraw(&mut self, active: &ActiveData);
}
impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
fn remove(&mut self, id: WidgetId) {
for t in self.widget_to_types.get(&id).into_flat_iter() {
self.types.get_mut(t).unwrap().remove(id);
}
}
fn draw(&mut self, active: &ActiveData) {
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().draw(active);
}
}
fn undraw(&mut self, active: &ActiveData) {
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().undraw(active);
}
}
}
pub trait EventManagerLike<State> {
fn remove(&mut self, id: WidgetId);
fn draw(&mut self, data: &ActiveData);
fn undraw(&mut self, data: &ActiveData);
}
type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>>);
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
}
impl<Rsc: HasEvents, E: Event> EventManagerLike<Rsc> for TypeEventManager<Rsc, E> {
fn remove(&mut self, id: WidgetId) {
self.map.remove(&id);
for layer in self.active.values_mut() {
layer.remove(&id);
}
}
fn draw(&mut self, data: &ActiveData) {
self.active
.entry(data.layer)
.or_default()
.entry(data.id)
.or_default();
}
fn undraw(&mut self, data: &ActiveData) {
if let Some(layer) = self.active.get_mut(&data.layer) {
layer.remove(&data.id);
}
}
}
impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self {
Self {
active: Default::default(),
map: Default::default(),
}
}
}
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
fn register<I: IdLike + 'static>(
&mut self,
widget: I,
event: impl EventLike<Event = E>,
f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, I::Widget>,
) {
let event = event.into_event();
self.map.entry(widget.id()).or_default().push((
event,
Rc::new(move |ctx, rsc| {
f(
EventIdCtx {
widget: WeakWidget::new(widget.id()),
state: ctx.state,
data: ctx.data,
},
rsc,
);
}),
));
}
pub fn run_fn<'a>(
&mut self,
id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| {
for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) {
f(
EventCtx {
state: ctx.state,
data,
},
rsc,
)
}
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
mod ctx;
mod manager;
mod rsc;
pub use ctx::*;
pub use manager::*;
pub use rsc::*;
pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = ();
type State: Default = ();
#[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone())
}
}
pub trait EventLike {
type Event: Event;
fn into_event(self) -> Self::Event;
}
impl<E: Event> EventLike for E {
type Event = Self;
fn into_event(self) -> Self::Event {
self
}
}
pub trait EventFn<Rsc: HasEvents, Data>: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static {}
impl<Rsc: HasEvents, F: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static, Data> EventFn<Rsc, Data>
for F
{
}
pub trait WidgetEventFn<Rsc: HasEvents, Data, W: ?Sized>:
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static
{
}
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized>
WidgetEventFn<Rsc, Data, W> for F
{
}
+34
View File
@@ -0,0 +1,34 @@
use crate::{
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
};
pub trait HasState: 'static {
type State;
}
pub trait HasEvents: Sized + UiRsc + HasState {
fn events(&self) -> &EventManager<Self>;
fn events_mut(&mut self) -> &mut EventManager<Self>;
fn register_event<W: Widget + ?Sized, E: EventLike>(
&mut self,
id: WeakWidget<W>,
event: E,
f: impl for<'a> WidgetEventFn<Self, <E::Event as Event>::Data<'a>, W>,
) {
self.events_mut().register(id, event, f);
}
}
pub trait RunEvents: HasEvents {
fn run_event<E: EventLike>(
&mut self,
id: impl IdLike,
data: <E::Event as Event>::Data<'_>,
state: &mut Self::State,
) {
let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self)
}
}
impl<T: HasEvents> RunEvents for T {}
+33
View File
@@ -0,0 +1,33 @@
#![feature(macro_metavar_expr_concat)]
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_convert)]
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(const_destruct)]
#![feature(associated_type_defaults)]
#![feature(unsize)]
#![feature(coerce_unsized)]
#![feature(option_into_flat_iter)]
mod attr;
mod event;
mod num;
mod orientation;
mod primitive;
mod render;
mod ui;
mod widget;
pub mod util;
pub use attr::*;
pub use event::*;
pub use num::*;
pub use orientation::*;
pub use primitive::*;
pub use render::*;
pub use ui::*;
pub use widget::*;
pub type UiColor = primitive::Color<u8>;
+49
View File
@@ -0,0 +1,49 @@
use crate::util::Vec2;
use std::marker::Destruct;
pub const trait UiNum {
fn to_f32(self) -> f32;
}
const impl UiNum for f32 {
fn to_f32(self) -> f32 {
self
}
}
const impl UiNum for u32 {
fn to_f32(self) -> f32 {
self as f32
}
}
const impl UiNum for i32 {
fn to_f32(self) -> f32 {
self as f32
}
}
pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
Vec2::new(x.to_f32(), y.to_f32())
}
const impl<T: const UiNum + Copy> From<T> for Vec2 {
fn from(v: T) -> Self {
Self {
x: v.to_f32(),
y: v.to_f32(),
}
}
}
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2
where
(T, U): const Destruct,
{
fn from((x, y): (T, U)) -> Self {
Self {
x: x.to_f32(),
y: y.to_f32(),
}
}
}
@@ -1,4 +1,4 @@
use crate::layout::vec2;
use crate::vec2;
use super::*;
@@ -187,7 +187,7 @@ impl From<CardinalAlign> for Align {
}
}
impl const From<RegionAlign> for UiVec2 {
const impl From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self {
Self::rel(align.rel())
}
@@ -68,3 +68,48 @@ impl Vec2 {
}
}
}
pub const trait AxisT {
fn get() -> Axis;
}
pub struct XAxis;
const impl AxisT for XAxis {
fn get() -> Axis {
Axis::X
}
}
pub struct YAxis;
const impl AxisT for YAxis {
fn get() -> Axis {
Axis::Y
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct BothAxis<T> {
pub x: T,
pub y: T,
}
impl<T> BothAxis<T> {
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
match A::get() {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
pub fn take_axis<A: const AxisT>(self) -> T {
match A::get() {
Axis::X => self.x,
Axis::Y => self.y,
}
}
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
match axis {
Axis::X => &mut self.x,
Axis::Y => &mut self.y,
}
}
}
@@ -1,5 +1,5 @@
use super::*;
use crate::{layout::UiNum, util::impl_op};
use crate::{UiNum, util::impl_op};
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Size {
@@ -3,7 +3,7 @@ mod axis;
mod len;
mod pos;
use super::vec2::*;
use crate::util::Vec2;
pub use align::*;
pub use axis::*;
@@ -2,7 +2,7 @@ use std::{fmt::Display, hash::Hash, marker::Destruct};
use super::*;
use crate::{
layout::UiNum,
UiNum,
util::{LerpUtil, impl_op},
};
@@ -124,13 +124,13 @@ impl Display for UiVec2 {
impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y);
impl const From<Vec2> for UiVec2 {
const impl From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self {
Self::abs(abs)
}
}
impl<T: const UiNum, U: const UiNum> const From<(T, U)> for UiVec2
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where
(T, U): const Destruct,
{
@@ -1,15 +1,21 @@
#![allow(clippy::multiple_bound_locations)]
use std::marker::Destruct;
/// stored in linear for sane manipulation
#[repr(C)]
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
pub struct Color<T: ColorNum> {
pub struct Color<T> {
pub r: T,
pub g: T,
pub b: T,
pub a: T,
}
impl<T: ColorNum> Default for Color<T> {
fn default() -> Self {
Self::BLACK
}
}
impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
@@ -56,23 +62,47 @@ pub trait ColorNum {
const MAX: Self;
}
impl<T: ColorNum + F32Conversion> Color<T> {
pub fn mul_rgb(self, amt: impl F32Conversion) -> Self {
macro_rules! map_rgb {
($x:ident,$self:ident, $e:tt) => {
#[allow(unused_braces)]
Self {
r: {
let $x = $self.r;
$e
},
g: {
let $x = $self.g;
$e
},
b: {
let $x = $self.b;
$e
},
a: $self.a,
}
};
}
impl<T: ColorNum + const F32Conversion> Color<T>
where
Self: const Destruct,
{
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
let amt = amt.to();
self.map_rgb(|x| T::from(x.to() * amt))
map_rgb!(x, self, { T::from(x.to() * amt) })
}
pub fn add_rgb(self, amt: impl F32Conversion) -> Self {
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
let amt = amt.to();
self.map_rgb(|x| T::from(x.to() + amt))
map_rgb!(x, self, { T::from(x.to() + amt) })
}
pub fn darker(self, amt: f32) -> Self {
pub const fn darker(self, amt: f32) -> Self {
self.mul_rgb(1.0 - amt)
}
pub fn brighter(self, amt: f32) -> Self {
self.map_rgb(|x| {
pub const fn brighter(self, amt: f32) -> Self {
map_rgb!(x, self, {
let x = x.to();
T::from(x + (1.0 - x) * amt)
})
@@ -120,7 +150,7 @@ impl ColorNum for f32 {
unsafe impl bytemuck::Pod for Color<u8> {}
impl const F32Conversion for f32 {
const impl F32Conversion for f32 {
fn to(self) -> f32 {
self
}
@@ -129,7 +159,7 @@ impl const F32Conversion for f32 {
}
}
impl const F32Conversion for u8 {
const impl F32Conversion for u8 {
fn to(self) -> f32 {
self as f32 / 255.0
}
@@ -1,6 +1,11 @@
use std::ops::{Index, IndexMut};
use crate::render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives};
use crate::{
render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives},
util::to_mut,
};
pub type LayerId = usize;
struct LayerNode<T> {
next: Ptr,
@@ -49,13 +54,13 @@ impl<T: Default> Layers<T> {
self.vec.push(LayerNode::head());
}
fn push(&mut self, node: LayerNode<T>) -> usize {
fn push(&mut self, node: LayerNode<T>) -> LayerId {
let i = self.vec.len();
self.vec.push(node);
i
}
pub fn next(&mut self, i: usize) -> usize {
pub fn next(&mut self, i: LayerId) -> LayerId {
if let Ptr::Next(i) = self.vec[i].next {
return i;
}
@@ -75,7 +80,7 @@ impl<T: Default> Layers<T> {
i_new
}
pub fn child(&mut self, i: usize) -> usize {
pub fn child(&mut self, i: LayerId) -> LayerId {
if let Some(c) = self.vec[i].child {
return c.head;
}
@@ -100,11 +105,11 @@ impl<T: Default> Layers<T> {
self.vec.iter_mut().map(|n| &mut n.data).enumerate()
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &T)> {
pub fn iter(&self) -> impl Iterator<Item = (LayerId, &T)> {
self.indices().map(|i| (i, &self.vec[i].data))
}
pub fn iter_depth(&self) -> impl Iterator<Item = ((usize, usize), &T)> {
pub fn iter_depth(&self) -> impl Iterator<Item = ((LayerId, usize), &T)> {
self.indices()
.map(|i| ((i, self.vec[i].depth), &self.vec[i].data))
}
@@ -115,7 +120,11 @@ impl<T: Default> Layers<T> {
}
impl PrimitiveLayers {
pub fn write<P: Primitive>(&mut self, layer: usize, info: PrimitiveInst<P>) -> PrimitiveHandle {
pub fn write<P: Primitive>(
&mut self,
layer: LayerId,
info: PrimitiveInst<P>,
) -> PrimitiveHandle {
self[layer].write(layer, info)
}
@@ -130,16 +139,16 @@ impl<T: Default> Default for Layers<T> {
}
}
impl<T> Index<usize> for Layers<T> {
impl<T> Index<LayerId> for Layers<T> {
type Output = T;
fn index(&self, index: usize) -> &Self::Output {
fn index(&self, index: LayerId) -> &Self::Output {
&self.vec[index].data
}
}
impl<T> IndexMut<usize> for Layers<T> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
impl<T> IndexMut<LayerId> for Layers<T> {
fn index_mut(&mut self, index: LayerId) -> &mut Self::Output {
&mut self.vec[index].data
}
}
@@ -170,8 +179,7 @@ impl<'a, T> Iterator for LayerIteratorMut<'a, T> {
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::<&T, &mut T>(&self.inner.vec[i].data) };
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
Some((i, layer))
}
}
@@ -180,8 +188,7 @@ impl<'a, T> DoubleEndedIterator for LayerIteratorMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
let i = self.inner.next_back()?;
// SAFETY: requires index iterator to work properly
#[allow(mutable_transmutes)]
let layer = unsafe { std::mem::transmute::<&T, &mut T>(&self.inner.vec[i].data) };
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
Some((i, layer))
}
}
+9
View File
@@ -0,0 +1,9 @@
mod color;
mod layer;
mod text;
mod texture;
pub use color::*;
pub use layer::*;
pub use text::*;
pub use texture::*;
+250
View File
@@ -0,0 +1,250 @@
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
};
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
};
pub struct TextData {
pub font_cx: FontContext,
pub layout_cx: LayoutContext<UiColor>,
scale_cx: ScaleContext,
pub atlas: GlyphAtlas,
}
impl Default for TextData {
fn default() -> Self {
Self {
font_cx: FontContext::new(),
layout_cx: LayoutContext::new(),
scale_cx: ScaleContext::new(),
atlas: GlyphAtlas::default(),
}
}
}
#[derive(Clone, PartialEq)]
pub enum Family {
SansSerif,
Serif,
Monospace,
Named(String),
}
impl Family {
fn family(&self) -> FontFamily<'_> {
let name = match self {
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
};
FontFamily::Single(name)
}
}
#[derive(Clone, PartialEq)]
pub struct TextAttrs {
pub color: UiColor,
pub font_size: f32,
pub line_height: f32,
pub family: Family,
pub wrap: bool,
pub align: RegionAlign,
}
pub const LINE_HEIGHT_MULT: f32 = 1.1;
impl Default for TextAttrs {
fn default() -> Self {
let size = 16.0;
Self {
color: UiColor::WHITE,
font_size: size,
line_height: size * LINE_HEIGHT_MULT,
family: Family::SansSerif,
wrap: false,
align: Align::CENTER_LEFT,
}
}
}
/// Keeps text and its corresponding layout from getting out of sync.
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
shaped: Option<(TextAttrs, Option<f32>)>,
}
impl TextBuffer {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
layout: Layout::new(),
shaped: None,
}
}
pub fn new_empty() -> Self {
Self::new("")
}
pub fn text(&self) -> &str {
&self.text
}
pub fn layout(&self) -> &Layout<UiColor> {
&self.layout
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn set_text(&mut self, text: impl Into<String>) {
let text = text.into();
if text != self.text {
self.text = text;
self.shaped = None;
}
}
/// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String {
self.shaped = None;
&mut self.text
}
pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height())
}
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
return;
}
let mut builder = data
.layout_cx
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
builder.push_default(StyleProperty::FontSize(attrs.font_size));
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
attrs.line_height,
)));
builder.push_default(StyleProperty::Brush(attrs.color));
builder.build_into(&mut self.layout, &self.text);
self.layout.break_all_lines(width);
self.layout
.align(Alignment::Start, AlignmentOptions::default());
self.shaped = Some((attrs.clone(), width));
}
}
impl TextData {
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
let mut placed = Vec::new();
for line in buffer.layout.lines() {
for item in line.items() {
let PositionedLayoutItem::GlyphRun(run) = item else {
continue;
};
let font = run.run().font();
let font_size = run.run().font_size();
let coords = run.run().normalized_coords();
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
else {
continue;
};
let coords_hash = hash_coords(coords);
// `font.data.id()` rather than the pointer, so the same font
// loaded twice is still one set of entries.
let font_id = font.data.id();
for glyph in run.positioned_glyphs() {
let subpixel = ((glyph.x.fract() * 4.0).round() as i32).rem_euclid(4) as u8;
let key = GlyphKey {
font: font_id,
glyph: glyph.id,
size: (font_size * 16.0).round() as u32,
subpixel,
coords: coords_hash,
};
let entry = match self.atlas.get(&key) {
Some(entry) => entry,
None => {
let mut scaler = self
.scale_cx
.builder(font_ref)
.size(font_size)
.hint(true)
.normalized_coords(coords)
.build();
let image = Render::new(&[
Source::ColorOutline(0),
Source::ColorBitmap(StrikeWith::BestFit),
Source::Outline,
])
.format(Format::Alpha)
.offset(Vector::new(subpixel as f32 / 4.0, 0.0))
.render(&mut scaler, glyph.id as u16);
match image {
Some(image) => self.atlas.insert(key, &image, textures),
None => {
self.atlas.insert_empty(key);
None
}
}
}
};
let Some(entry) = entry else { continue };
placed.push(PlacedGlyph {
entry,
offset: Vec2::new(
glyph.x.floor() + entry.left as f32,
glyph.y.floor() - entry.top as f32,
),
});
}
}
}
placed
}
}
fn hash_coords(coords: &[i16]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for c in coords {
h ^= *c as u16 as u64;
h = h.wrapping_mul(0x1000_0000_01b3);
}
h
}
#[derive(Clone)]
pub struct RenderedText {
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
pub size: Vec2,
pub color: UiColor,
}
impl TextData {
pub fn render(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
textures: &mut Textures,
) -> RenderedText {
buffer.shape(self, attrs, width);
let glyphs = self.place(buffer, textures);
RenderedText {
glyphs: std::sync::Arc::new(glyphs),
size: buffer.size(),
color: attrs.color,
}
}
}
@@ -1,12 +1,13 @@
use crate::{
render::TexturePrimitive,
util::{RefCounter, Vec2},
};
use image::{DynamicImage, GenericImageView};
use std::{
ops::Index,
sync::mpsc::{Receiver, Sender, channel},
};
use image::{DynamicImage, GenericImageView};
use crate::{layout::Vec2, render::TexturePrimitive, util::RefCounter};
#[derive(Debug, Clone)]
pub struct TextureHandle {
inner: TexturePrimitive,
@@ -28,14 +29,24 @@ pub struct Textures {
pub enum TextureUpdate<'a> {
Push(&'a DynamicImage),
Set(u32, &'a DynamicImage),
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32),
PushFree,
SetFree,
}
#[derive(Debug, Clone, Copy)]
pub struct PatchRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
enum Update {
Push(u32),
Set(u32),
Patch(u32, PatchRect),
Free(u32),
}
@@ -80,6 +91,18 @@ impl Textures {
}
}
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.inner.view_idx as usize]
.as_mut()
.expect("texture was freed while still held")
}
/// Queue an upload of just `rect`, after writing it with `image_mut`.
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates
.push(Update::Patch(handle.inner.view_idx, rect));
}
pub fn free(&mut self) {
for idx in self.recv.try_iter() {
self.images[idx as usize] = None;
@@ -98,6 +121,10 @@ impl Textures {
.as_ref()
.map(|img| TextureUpdate::Set(i, img))
.unwrap_or(TextureUpdate::SetFree),
Update::Patch(i, rect) => self.images[i as usize]
.as_ref()
.map(|img| TextureUpdate::Patch(i, rect, img))
.unwrap_or(TextureUpdate::SetFree),
Update::Free(i) => TextureUpdate::Free(i),
})
}
+200
View File
@@ -0,0 +1,200 @@
use crate::{
PatchRect, TextureHandle, Textures,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
const PAGE: u32 = 1024;
/// Transparent margin kept around every glyph, so that sampling one cannot
/// pick up its neighbour along a shared edge.
const PAD: u32 = 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font: u64,
pub glyph: u32,
/// Font size in 1/16 px, so sizes that round to the same pixels share a
/// raster instead of filling the atlas with near-duplicates.
pub size: u32,
/// Horizontal subpixel phase, in 1/4 px.
pub subpixel: u8,
/// Hash of the variation coordinates; a variable font at two weights is two
/// different sets of pixels from one glyph id.
pub coords: u64,
}
#[derive(Clone, Copy)]
pub struct GlyphEntry {
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
/// Offset from the glyph's pen position to the top-left of its pixels.
pub left: i32,
pub top: i32,
pub width: u32,
pub height: u32,
pub is_color: bool,
pub view_idx: u32,
pub sampler_idx: u32,
}
struct Page {
handle: TextureHandle,
x: u32,
y: u32,
shelf_height: u32,
}
#[derive(Default)]
pub struct GlyphAtlas {
pages: Vec<Page>,
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
/// too, so it is not re-rasterised on every layout.
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
}
impl GlyphAtlas {
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
self.entries.get(key).copied()
}
pub fn insert(
&mut self,
key: GlyphKey,
image: &Image,
textures: &mut Textures,
) -> Option<GlyphEntry> {
let w = image.placement.width;
let h = image.placement.height;
if w == 0 || h == 0 {
self.entries.insert(key, None);
return None;
}
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
self.entries.insert(key, None);
return None;
}
let (page_idx, x, y) = self.allocate(w, h, textures);
let page = &self.pages[page_idx];
let img = textures.image_mut(&page.handle);
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
write_glyph(rgba, image, x, y);
let handle = page.handle.clone();
let rect = PatchRect {
x,
y,
width: w,
height: h,
};
textures.patch(&handle, rect);
let page = &self.pages[page_idx];
let scale = 1.0 / PAGE as f32;
let entry = GlyphEntry {
uv_min: [x as f32 * scale, y as f32 * scale],
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
left: image.placement.left,
top: image.placement.top,
width: w,
height: h,
is_color: matches!(image.content, Content::Color),
view_idx: page.handle.primitive().view_idx,
sampler_idx: page.handle.primitive().sampler_idx,
};
self.entries.insert(key, Some(entry));
Some(entry)
}
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
let need_w = w + PAD;
let need_h = h + PAD;
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
let page = &mut self.pages[i];
if page.x + need_w > PAGE {
page.y += page.shelf_height;
page.x = PAD;
page.shelf_height = 0;
}
let (x, y) = (page.x, page.y);
page.x += need_w;
page.shelf_height = page.shelf_height.max(need_h);
return (i, x, y);
}
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
self.pages.push(Page {
handle,
x: PAD + w + PAD,
y: PAD,
shelf_height: h + PAD,
});
(self.pages.len() - 1, PAD, PAD)
}
pub fn insert_empty(&mut self, key: GlyphKey) {
self.entries.insert(key, None);
}
pub fn page_count(&self) -> usize {
self.pages.len()
}
pub fn glyph_count(&self) -> usize {
self.entries.len()
}
}
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
}
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
let w = image.placement.width;
let h = image.placement.height;
match image.content {
Content::Mask => {
for row in 0..h {
for col in 0..w {
let a = image.data[(row * w + col) as usize];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
Content::Color => {
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let px = [
image.data[i],
image.data[i + 1],
image.data[i + 2],
image.data[i + 3],
];
page.put_pixel(x + col, y + row, image::Rgba(px));
}
}
}
Content::SubpixelMask => {
// Preserve readable output if the rasterizer returns a subpixel mask.
for row in 0..h {
for col in 0..w {
let i = ((row * w + col) * 4) as usize;
let a = image.data[i + 1];
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
}
}
}
}
}
#[derive(Clone, Copy)]
pub struct PlacedGlyph {
pub entry: GlyphEntry,
pub offset: Vec2,
}
@@ -1,4 +1,4 @@
use crate::{layout::UiRegion, util::Id};
use crate::{UiRegion, util::Id};
use wgpu::*;
#[repr(C)]
+27 -16
View File
@@ -1,22 +1,23 @@
use std::num::NonZero;
use crate::{
layout::Ui,
UiData, UiRenderState,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::HashMap,
util::{HashMap, Vec2},
};
use data::WindowUniform;
use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
*,
};
use winit::dpi::PhysicalSize;
mod atlas;
mod data;
mod primitive;
mod texture;
mod util;
pub use atlas::*;
pub use data::{Mask, MaskIdx};
pub use primitive::*;
@@ -59,12 +60,18 @@ impl UiRenderNode {
}
}
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) {
pub fn update(
&mut self,
device: &Device,
queue: &Queue,
ui: &mut UiData,
ui_render: &mut UiRenderState,
) {
self.active.clear();
for (i, primitives) in ui.data.layers.iter_mut() {
for (i, primitives) in ui_render.layers.iter_mut() {
self.active.push(i);
for change in primitives.apply_free() {
if let Some(inst) = ui.data.active.get_mut(&change.id) {
if let Some(inst) = ui_render.active.get_mut(&change.id) {
for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old {
h.inst_idx = change.new;
@@ -101,10 +108,10 @@ impl UiRenderNode {
}
}
let mut changed = false;
changed |= self.textures.update(&mut ui.data.textures);
if ui.data.masks.changed {
ui.data.masks.changed = false;
self.masks.update(device, queue, &ui.data.masks[..]);
changed |= self.textures.update(&mut ui.textures);
if ui.masks.changed {
ui.masks.changed = false;
self.masks.update(device, queue, &ui.masks[..]);
changed = true;
}
if changed {
@@ -112,10 +119,11 @@ impl UiRenderNode {
}
}
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform {
width: size.width as f32,
height: size.height as f32,
width: size.x,
height: size.y,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
}
@@ -131,7 +139,10 @@ impl UiRenderNode {
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
});
let window_uniform = WindowUniform::default();
let window_uniform = WindowUniform {
width: config.width as f32,
height: config.height as f32,
};
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]),
@@ -183,7 +194,7 @@ impl UiRenderNode {
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
push_constant_ranges: &[],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"),
@@ -219,7 +230,7 @@ impl UiRenderNode {
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
multiview_mask: None,
cache: None,
});
@@ -1,19 +1,18 @@
use std::ops::{Deref, DerefMut};
use crate::{
layout::{Color, UiRegion},
Color, UiRegion, WidgetId,
render::{
ArrBuf,
data::{MaskIdx, PrimitiveInstance},
},
util::Id,
};
use bytemuck::Pod;
use wgpu::*;
pub struct Primitives {
instances: Vec<PrimitiveInstance>,
assoc: Vec<Id>,
assoc: Vec<WidgetId>,
data: PrimitiveData,
free: Vec<usize>,
pub updated: bool,
@@ -94,12 +93,12 @@ macro_rules! primitives {
}
)*
};
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
(@count $t:tt) => { 1 };
}
pub struct PrimitiveInst<P> {
pub id: Id,
pub id: WidgetId,
pub primitive: P,
pub region: UiRegion,
pub mask_idx: MaskIdx,
@@ -175,7 +174,7 @@ impl Primitives {
}
pub struct PrimitiveChange {
pub id: Id,
pub id: WidgetId,
pub old: usize,
pub new: usize,
}
@@ -202,6 +201,7 @@ impl PrimitiveHandle {
primitives!(
rects: RectPrimitive => 0,
textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
);
#[repr(C)]
@@ -231,6 +231,21 @@ pub struct TexturePrimitive {
pub sampler_idx: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive {
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
pub view_idx: u32,
pub sampler_idx: u32,
pub color: Color<u8>,
pub flags: u32,
}
impl GlyphPrimitive {
pub const IS_COLOR: u32 = 1;
}
pub struct PrimitiveVec<T> {
vec: Vec<T>,
free: Vec<usize>,
@@ -1,5 +1,6 @@
const RECT: u32 = 0u;
const TEXTURE: u32 = 1u;
const GLYPH: u32 = 2u;
@group(0) @binding(0)
var<uniform> window: WindowUniform;
@@ -7,6 +8,8 @@ var<uniform> window: WindowUniform;
var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect {
color: u32,
@@ -20,6 +23,15 @@ struct TextureInfo {
sampler_idx: u32,
}
struct GlyphInfo {
uv_min: vec2<f32>,
uv_max: vec2<f32>,
view_idx: u32,
sampler_idx: u32,
color: u32,
flags: u32,
}
struct Mask {
x: UiSpan,
y: UiSpan,
@@ -125,6 +137,9 @@ fn fs_main(
case TEXTURE: {
color = draw_texture(region, textures[i]);
}
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
}
default: {
color = vec4(1.0, 0.0, 1.0, 1.0);
}
@@ -148,6 +163,17 @@ fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv);
}
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
let uv = mix(g.uv_min, g.uv_max, region.uv);
let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
if (g.flags & 1u) != 0u {
return texel;
}
var color = unpack4x8unorm(g.color);
color.a *= texel.a;
return color;
}
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color);
@@ -1,11 +1,13 @@
use image::{DynamicImage, EncodableLayout};
use image::{DynamicImage, EncodableLayout, GenericImageView};
use wgpu::{util::DeviceExt, *};
use crate::layout::{TextureUpdate, Textures};
use crate::{PatchRect, TextureUpdate, Textures};
pub struct GpuTextures {
device: Device,
queue: Queue,
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>,
view_count: usize,
samplers: Vec<Sampler>,
@@ -17,37 +19,95 @@ 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(),
TextureUpdate::Push(image) => {
self.push(image);
changed = true;
}
TextureUpdate::Set(i, image) => {
self.set(i, image);
changed = true;
}
TextureUpdate::Patch(i, rect, image) => {
// Patching contents leaves the binding array unchanged.
self.patch(i, rect, image);
}
TextureUpdate::SetFree => {
self.view_count += 1;
changed = true;
}
TextureUpdate::Free(i) => {
self.free(i);
changed = true;
}
TextureUpdate::PushFree => {
self.push_free();
changed = true;
}
}
}
changed
}
fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1;
let view = self.create_view(image);
let (texture, view) = self.create(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view;
}
fn free(&mut self, i: u32) {
self.view_count -= 1;
self.textures[i as usize] = None;
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);
let (texture, view) = self.create(image);
self.textures.push(Some(texture));
self.views.push(view);
}
fn push_free(&mut self) {
self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone());
}
fn create_view(&self, image: &DynamicImage) -> TextureView {
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
let Some(texture) = &self.textures[i as usize] else {
return;
};
if rect.width == 0 || rect.height == 0 {
return;
}
// `write_texture` requires tightly packed rows, unlike the atlas image.
let sub = image
.view(rect.x, rect.y, rect.width, rect.height)
.to_image();
self.queue.write_texture(
TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
aspect: TextureAspect::All,
},
sub.as_bytes(),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width * 4),
rows_per_image: Some(rect.height),
},
Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
}
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
let image = image.to_rgba8();
let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data(
@@ -63,13 +123,14 @@ impl GpuTextures {
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
},
wgt::TextureDataOrder::MipMajor,
image.as_bytes(),
);
texture.create_view(&TextureViewDescriptor::default())
let view = texture.create_view(&TextureViewDescriptor::default());
(texture, view)
}
pub fn new(device: &Device, queue: &Queue) -> Self {
@@ -77,6 +138,7 @@ impl GpuTextures {
Self {
device: device.clone(),
queue: queue.clone(),
textures: Vec::new(),
views: Vec::new(),
samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()],
File renamed without changes.
+14
View File
@@ -0,0 +1,14 @@
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing
#[derive(Debug)]
pub struct ActiveData {
pub id: WidgetId,
pub region: UiRegion,
pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
pub mask: MaskIdx,
pub layer: LayerId,
}
+18
View File
@@ -0,0 +1,18 @@
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
#[derive(Default)]
pub struct Cache {
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
}
impl Cache {
pub fn remove(&mut self, id: WidgetId) {
self.size.x.remove(&id);
self.size.y.remove(&id);
}
pub fn clear(&mut self) {
self.size.x.clear();
self.size.y.clear();
}
}
+47
View File
@@ -0,0 +1,47 @@
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena};
mod active;
mod cache;
mod painter;
mod render_state;
mod size;
pub use active::*;
pub use painter::Painter;
pub use render_state::*;
pub use size::*;
#[derive(Default)]
pub struct UiData {
pub widgets: Widgets,
pub textures: Textures,
pub text: TextData,
pub masks: TrackedArena<Mask, u32>,
}
pub trait UiRsc {
fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData;
#[allow(unused_variables)]
fn on_add(&mut self, id: WeakWidget) {}
#[allow(unused_variables)]
fn on_remove(&mut self, id: WidgetId) {}
#[allow(unused_variables)]
fn on_draw(&mut self, active: &ActiveData) {}
#[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {}
fn widgets(&self) -> &Widgets {
&self.ui().widgets
}
fn widgets_mut(&mut self) -> &mut Widgets {
&mut self.ui_mut().widgets
}
fn free(&mut self) {
while let Some(id) = self.widgets_mut().free_next() {
self.on_remove(id);
}
self.ui_mut().textures.free();
}
}
+178
View File
@@ -0,0 +1,178 @@
use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::Vec2,
};
/// makes your surfaces look pretty
pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
pub(super) region: UiRegion,
pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>,
pub layer: usize,
pub(super) id: WidgetId,
}
impl<'a> Painter<'a> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self.state.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.rsc.ui_mut().masks.push_ref(self.mask);
}
self.primitives.push(h);
}
/// Writes a primitive to be rendered
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
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.rsc.ui_mut().masks.push(Mask { region });
}
/// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.widget_at(id, self.region);
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region));
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.children.push(id.id());
self.state.draw_inner(
self.layer,
id.id(),
region,
Some(self.id),
self.mask,
None,
self.rsc,
);
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
self.textures.push(handle.clone());
self.primitive_at(handle.primitive(), region.within(&self.region));
}
pub fn 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);
}
pub fn render_text(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width, &mut ui.textures)
}
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
let flags_for = |is_color| {
if is_color {
GlyphPrimitive::IS_COLOR
} else {
0
}
};
for glyph in text.glyphs.iter() {
let mut region = origin;
region.x.end = region.x.start;
region.y.end = region.y.start;
let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.primitive_at(
GlyphPrimitive {
uv_min: glyph.entry.uv_min,
uv_max: glyph.entry.uv_max,
view_idx: glyph.entry.view_idx,
sampler_idx: glyph.entry.sampler_idx,
color: text.color,
flags: flags_for(glyph.entry.is_color),
},
region,
);
}
}
pub fn region(&self) -> UiRegion {
self.region
}
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
self.size_ctx().size(id)
}
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
}
}
pub fn output_size(&self) -> Vec2 {
self.state.output_size
}
pub fn px_size(&mut self) -> Vec2 {
self.region.size().to_abs(self.state.output_size)
}
pub fn text_data(&mut self) -> &mut TextData {
&mut self.rsc.ui_mut().text
}
pub fn child_layer(&mut self) {
self.layer = self.state.layers.child(self.layer);
}
pub fn next_layer(&mut self) {
self.layer = self.state.layers.next(self.layer);
}
pub fn label(&self) -> &str {
&self.rsc.widgets().data(self.id).unwrap().label
}
pub fn id(&self) -> &WidgetId {
&self.id
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
self.state.size_ctx(self.id, self.region.size(), self.rsc)
}
}
+324
View File
@@ -0,0 +1,324 @@
use crate::{
ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ui::cache::Cache,
util::{HashMap, HashSet, Vec2, forget_ref},
};
pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers,
pub(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>,
resized: bool,
draw_started: HashSet<WidgetId>,
}
impl UiRenderState {
pub fn new() -> Self {
Self {
active: Default::default(),
layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO,
old_root: None,
resized: false,
draw_started: Default::default(),
}
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.output_size = size.into();
self.resized = true;
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
let widgets = rsc.widgets();
let len = widgets.waiting.len();
let all: Vec<_> = widgets
.waiting
.iter()
.map(|&w| format!("'{}' ({w:?})", widgets.label(w)))
.collect();
panic!(
"{len} widget(s) were never upgraded\n\
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
weak widgets: {all:#?}"
);
}
let root = root.into();
if self.needs_full_redraw(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
self.resized = false;
} else if rsc.widgets().has_updates() {
self.redraw_updates(rsc);
}
}
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
self.clear(rsc);
// free all resources & cache
if let Some(id) = root {
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
}
}
// TODO: should prolly make a DrawInfo struct or smth for everything other than rsc
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_inner(
&mut self,
layer: usize,
id: WidgetId,
region: UiRegion,
parent: Option<WidgetId>,
mask: MaskIdx,
old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc,
) {
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id)
&& !rsc.widgets().needs_redraw.contains(&id)
{
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap();
old_children = active.children;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
state: self,
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
rsc,
};
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
widget.draw(&mut painter);
drop(widget);
let Painter {
state: _,
rsc: _,
region,
mask,
textures,
primitives,
children,
layer,
id,
} = painter;
// add to active
let active = ActiveData {
id,
region,
parent,
textures,
primitives,
children,
mask,
layer,
};
// remove old children that weren't kept
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c, rsc);
}
}
rsc.on_draw(&active);
self.active.insert(id, active);
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
rsc.ui_mut().masks.remove(mask);
}
}
active.textures.clear();
rsc.ui_mut().textures.free();
if undraw {
rsc.on_undraw(active);
}
}
active
}
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c, rsc);
}
}
inst
}
fn clear(&mut self, rsc: &mut dyn UiRsc) {
for (_, active) in self.active.drain() {
rsc.on_undraw(&active);
}
self.cache.clear();
self.layers.clear();
rsc.widgets_mut().needs_redraw.clear();
rsc.free();
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
self.redraw(id, rsc);
}
rsc.free();
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root
}
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>(
&self,
root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets,
) -> bool {
self.needs_full_redraw(root) || widgets.has_updates()
}
pub fn active_widgets(&self) -> usize {
self.active.len()
}
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
self.active.iter().filter_map(move |(&id, inst)| {
let l = widgets.label(id);
if l == label { Some(inst) } else { None }
})
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
}
}
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let region = self.active.get(&id.id())?.region;
Some(region.to_px(self.output_size))
}
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first
for axis in [Axis::X, Axis::Y] {
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
&& let Some(current) = self.active.get(&id)
&& let Some(pid) = current.parent
{
self.cache.size.axis_dyn(axis).remove(&id);
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
if new != old {
self.redraw(pid, rsc);
}
}
}
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.remove(id, false, rsc) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
rsc,
);
}
pub(super) fn size_ctx<'b>(
&'b mut self,
source: WidgetId,
outer: UiVec2,
rsc: &'b mut dyn UiRsc,
) -> SizeCtx<'b> {
let ui = rsc.ui_mut();
SizeCtx {
source,
cache: &mut self.cache,
text: &mut ui.text,
textures: &mut ui.textures,
widgets: &ui.widgets,
outer,
output_size: self.output_size,
id: source,
}
}
}
impl Default for UiRenderState {
fn default() -> Self {
Self::new()
}
}
+91
View File
@@ -0,0 +1,91 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures,
UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache,
/// TODO: should this be pub? rn used for sized
pub outer: UiVec2,
pub(super) output_size: Vec2,
pub(super) id: WidgetId,
}
impl SizeCtx<'_> {
pub fn id(&self) -> &WidgetId {
&self.id
}
pub fn source(&self) -> &WidgetId {
&self.source
}
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
return *len;
}
let len = self
.widgets
.get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx {
text: self.text,
textures: self.textures,
source: self.source,
widgets: self.widgets,
cache: self.cache,
outer: self.outer,
output_size: self.output_size,
id,
});
self.cache.size.axis::<A>().insert(id, (self.outer, len));
len
}
pub fn width(&mut self, id: impl IdLike) -> Len {
self.len_inner::<XAxis>(id.id())
}
pub fn height(&mut self, id: impl IdLike) -> Len {
self.len_inner::<YAxis>(id.id())
}
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
match axis {
Axis::X => self.width(id),
Axis::Y => self.height(id),
}
}
pub fn size(&mut self, id: impl IdLike) -> Size {
let id = id.id();
Size {
x: self.width(id),
y: self.height(id),
}
}
pub fn px_size(&mut self) -> Vec2 {
self.outer.to_abs(self.output_size)
}
pub fn output_size(&mut self) -> Vec2 {
self.output_size
}
pub fn draw_text(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width, self.textures)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9 -8
View File
@@ -9,15 +9,16 @@ pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self;
}
impl const DivOr for f32 {
const impl DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs;
if res.is_nan() { other } else { res }
}
}
impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const
LerpUtil for T
const impl<
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
> LerpUtil for T
{
/// linear interpolation
/// from * (1.0 - self) + to * self
@@ -37,7 +38,7 @@ macro_rules! impl_op {
use super::*;
#[allow(unused_imports)]
use std::ops::*;
impl const $op for $T {
const impl $op for $T {
type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output {
@@ -46,12 +47,12 @@ macro_rules! impl_op {
}
}
}
impl const $opa for $T {
const impl $opa for $T {
fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs);
}
}
impl const $op<f32> for $T {
const impl $op<f32> for $T {
type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output {
@@ -60,7 +61,7 @@ macro_rules! impl_op {
}
}
}
impl const $op<$T> for f32 {
const impl $op<$T> for f32 {
type Output = $T;
fn $fn(self, rhs: $T) -> Self::Output {
@@ -69,7 +70,7 @@ macro_rules! impl_op {
}
}
}
impl const $opa<f32> for $T {
const impl $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) {
*self = self.$fn(rhs);
}
+24
View File
@@ -0,0 +1,24 @@
mod arena;
mod borrow;
mod change;
mod id;
mod math;
mod refcount;
mod slot;
mod trust;
mod typemap;
mod vec2;
pub use arena::*;
pub use borrow::*;
pub use change::*;
pub use id::*;
pub use math::*;
pub use refcount::*;
pub use slot::*;
pub(crate) use trust::*;
pub use typemap::*;
pub use vec2::*;
pub type HashMap<K, V> = fxhash::FxHashMap<K, V>;
pub type HashSet<K> = fxhash::FxHashSet<K>;
@@ -22,6 +22,12 @@ impl RefCounter {
}
}
impl Default for RefCounter {
fn default() -> Self {
Self::new()
}
}
impl Clone for RefCounter {
fn clone(&self) -> Self {
self.0.fetch_add(1, Ordering::Release);
+69
View File
@@ -0,0 +1,69 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SlotId {
idx: u32,
genr: u32,
}
pub struct SlotVec<T> {
data: Vec<(u32, Option<T>)>,
free: Vec<u32>,
}
impl<T> SlotVec<T> {
pub fn new() -> Self {
Self {
data: Default::default(),
free: Default::default(),
}
}
pub fn add(&mut self, x: T) -> SlotId {
if let Some(idx) = self.free.pop() {
let (genr, data) = &mut self.data[idx as usize];
*data = Some(x);
SlotId { idx, genr: *genr }
} else {
let idx = self.data.len() as u32;
let genr = 0;
self.data.push((genr, Some(x)));
SlotId { idx, genr }
}
}
pub fn free(&mut self, id: SlotId) {
let (genr, data) = &mut self.data[id.idx as usize];
*genr += 1;
*data = None;
self.free.push(id.idx);
}
pub fn get(&self, id: SlotId) -> Option<&T> {
let slot = &self.data[id.idx as usize];
if slot.0 != id.genr {
return None;
}
slot.1.as_ref()
}
pub fn get_mut(&mut self, id: SlotId) -> Option<&mut T> {
let slot = &mut self.data[id.idx as usize];
if slot.0 != id.genr {
return None;
}
slot.1.as_mut()
}
pub fn len(&self) -> usize {
self.data.len() - self.free.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl<T> Default for SlotVec<T> {
fn default() -> Self {
Self::new()
}
}
+17
View File
@@ -0,0 +1,17 @@
#[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
}
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
#[allow(mutable_transmutes)]
unsafe {
std::mem::transmute::<&T, &mut T>(x)
}
}
+56
View File
@@ -0,0 +1,56 @@
use crate::util::HashMap;
use std::{
any::TypeId,
marker::Unsize,
ops::{Deref, DerefMut},
};
pub struct TypeMap<Trait: ?Sized> {
map: HashMap<TypeId, Box<Trait>>,
}
impl<Trait: ?Sized> TypeMap<Trait> {
pub fn set_type<T: Unsize<Trait> + 'static>(&mut self, val: T) {
self.map
.insert(TypeId::of::<T>(), Box::new(val) as Box<Trait>);
}
pub fn type_mut<T: Unsize<Trait> + 'static>(&mut self) -> Option<&mut T> {
Some(Self::convert_mut(self.map.get_mut(&TypeId::of::<T>())?))
}
pub fn type_or_default<T: Default + Unsize<Trait> + 'static>(&mut self) -> &mut T {
Self::convert_mut(
self.map
.entry(TypeId::of::<T>())
.or_insert(Box::new(T::default()) as Box<Trait>),
)
}
fn convert_mut<T: Unsize<Trait>>(entry: &mut Box<Trait>) -> &mut T {
// allegedly this is just what Any does...
unsafe { &mut *(entry.as_mut() as *mut Trait as *mut T) }
}
}
impl<T: ?Sized> Deref for TypeMap<T> {
type Target = HashMap<TypeId, Box<T>>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl<T: ?Sized> DerefMut for TypeMap<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
impl<T: ?Sized> Default for TypeMap<T> {
fn default() -> Self {
Self {
map: Default::default(),
}
}
}
+3 -31
View File
@@ -1,8 +1,5 @@
use crate::{
layout::UiNum,
util::{DivOr, impl_op},
};
use std::{hash::Hash, marker::Destruct, ops::*};
use crate::util::{DivOr, impl_op};
use std::{hash::Hash, ops::*};
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
@@ -20,10 +17,6 @@ impl Hash for Vec2 {
}
}
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);
@@ -68,22 +61,13 @@ impl Vec2 {
}
}
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 {
const impl DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self {
Self {
x: self.x.div_or(rhs.x, other.x),
@@ -102,18 +86,6 @@ impl Neg for Vec2 {
}
}
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)
+22
View File
@@ -0,0 +1,22 @@
use crate::Widget;
pub struct WidgetData {
pub widget: Box<dyn Widget>,
pub label: String,
/// dynamic borrow checking
pub borrowed: bool,
}
impl WidgetData {
pub fn new<W: Widget>(widget: W) -> Self {
let mut label = std::any::type_name::<W>().to_string();
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) {
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1;
}
Self {
widget: Box::new(widget),
label,
borrowed: false,
}
}
}
+164
View File
@@ -0,0 +1,164 @@
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
use crate::{
UiRsc, Widget,
util::{RefCounter, SlotId},
};
pub type WidgetId = SlotId;
/// An identifier for a widget that can index a UI or event ctx to get it.
/// This is a strong handle that does not impl Clone, and when it is dropped,
/// a signal is sent to the owning UI to clean up the resources.
///
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
pub struct StrongWidget<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId,
counter: RefCounter,
send: Sender<WidgetId>,
ty: *const W,
}
/// A weak handle to a widget.
/// Will not keep it alive, but can still be used for indexing like WidgetHandle.
pub struct WeakWidget<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId,
#[allow(unused)]
ty: *const W,
}
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> {
pub fn any(self) -> StrongWidget<dyn Widget> {
self
}
}
impl<W: ?Sized> StrongWidget<W> {
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
Self {
id,
counter: RefCounter::new(),
send,
ty: null_ptr(),
}
}
pub fn id(&self) -> WidgetId {
self.id
}
pub fn refs(&self) -> u32 {
self.counter.refs()
}
pub fn weak(&self) -> WeakWidget<W> {
let Self { ty, id, .. } = *self;
WeakWidget { ty, id }
}
}
impl<W: ?Sized> WeakWidget<W> {
pub(crate) fn new(id: WidgetId) -> Self {
Self { id, ty: null_ptr() }
}
pub fn id(&self) -> WidgetId {
self.id
}
#[track_caller]
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> {
ui.widgets_mut().upgrade(self)
}
}
impl<W: ?Sized> Drop for StrongWidget<W> {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.id);
}
}
}
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
pub trait IdLike {
type Widget: ?Sized;
fn id(&self) -> WidgetId;
}
impl<W: ?Sized> IdLike for &StrongWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
}
}
impl<W: ?Sized> IdLike for StrongWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
}
}
impl<W: ?Sized> IdLike for WeakWidget<W> {
type Widget = W;
fn id(&self) -> WidgetId {
self.id
}
}
impl IdLike for WidgetId {
type Widget = dyn Widget;
fn id(&self) -> WidgetId {
*self
}
}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<StrongWidget<U>> for StrongWidget<T> {}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WeakWidget<U>> for WeakWidget<T> {}
impl<W: ?Sized> Clone for WeakWidget<W> {
fn clone(&self) -> Self {
*self
}
}
impl<W: ?Sized> Copy for WeakWidget<W> {}
impl<W: ?Sized> PartialEq for WeakWidget<W> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<W> PartialEq for StrongWidget<W> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<W> std::fmt::Debug for StrongWidget<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
impl<'a, W: Widget + 'a, State: UiRsc> FnOnce<(&'a mut State,)> for WeakWidget<W> {
type Output = &'a mut W;
extern "rust-call" fn call_once(self, args: (&'a mut State,)) -> Self::Output {
&mut args.0.widgets_mut()[self]
}
}
fn null_ptr<W: ?Sized>() -> *const W {
if size_of::<&W>() == size_of::<*const dyn Widget>() {
let w: *const dyn Widget = &();
unsafe { std::mem::transmute_copy(&w) }
} else {
unsafe { std::mem::transmute_copy(&[0usize; 1]) }
}
}
unsafe impl<W: ?Sized> Send for WeakWidget<W> {}
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
+75
View File
@@ -0,0 +1,75 @@
use crate::UiRsc;
use super::*;
use std::marker::Unsize;
pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
fn add(self, rsc: &mut Rsc) -> WeakWidget<Self::Widget>;
fn add_strong(self, rsc: &mut Rsc) -> StrongWidget<Self::Widget> {
self.add(rsc).upgrade(rsc)
}
fn with_id<W2>(
self,
f: impl FnOnce(&mut Rsc, WeakWidget<Self::Widget>) -> WeakWidget<W2>,
) -> impl WidgetIdFn<Rsc, W2> {
move |state| {
let id = self.add(state);
f(state, id)
}
}
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) {
let id = self.add_strong(rsc);
root.set_root(id);
}
}
pub trait HasRoot {
fn set_root(&mut self, root: StrongWidget);
}
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
#[track_caller]
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>;
}
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> {
self
}
}
// variadic generics please save us
macro_rules! impl_widget_arr {
($n:expr;$($W:ident)*) => {
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
};
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> WidgetArrLike<Rsc, $n, ($($Tag,)*)> for ($($W,)*) {
fn add(self, rsc: &mut Rsc) -> WidgetArr<$n> {
#[allow(non_snake_case)]
let ($($W,)*) = self;
WidgetArr::new(
[$($W.add(rsc).upgrade(rsc),)*],
)
}
}
};
}
impl_widget_arr!(1;A);
impl_widget_arr!(2;A B);
impl_widget_arr!(3;A B C);
impl_widget_arr!(4;A B C D);
impl_widget_arr!(5;A B C D E);
impl_widget_arr!(6;A B C D E F);
impl_widget_arr!(7;A B C D E F G);
impl_widget_arr!(8;A B C D E F G H);
impl_widget_arr!(9;A B C D E F G H I);
impl_widget_arr!(10;A B C D E F G H I J);
impl_widget_arr!(11;A B C D E F G H I J K);
impl_widget_arr!(12;A B C D E F G H I J K L);
+87
View File
@@ -0,0 +1,87 @@
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
use std::any::Any;
mod data;
mod handle;
mod like;
mod tag;
mod view;
mod widgets;
pub use data::*;
pub use handle::*;
pub use like::*;
pub use tag::*;
pub use view::*;
pub use widgets::*;
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter);
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
}
pub trait WidgetAxisFns {
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
}
impl<W: Widget + ?Sized> WidgetAxisFns for W {
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
match A::get() {
Axis::X => self.desired_width(ctx),
Axis::Y => self.desired_height(ctx),
}
}
}
impl Widget for () {
fn draw(&mut self, _: &mut Painter) {}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
}
}
impl dyn Widget {
pub fn as_any(&self) -> &dyn Any {
self
}
pub fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A function that returns a widget given a UI.
/// Useful for defining trait functions on widgets that create a parent widget so that the children
/// don't need to be IDs yet
pub trait WidgetFn<State, W: Widget + ?Sized>: FnOnce(&mut State) -> W {}
impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {}
pub struct WidgetArr<const LEN: usize> {
pub arr: [StrongWidget; LEN],
}
impl<const LEN: usize> WidgetArr<LEN> {
pub fn new(arr: [StrongWidget; LEN]) -> Self {
Self { arr }
}
}
pub trait WidgetOption<State> {
fn get(self, state: &mut State) -> Option<StrongWidget>;
}
impl<State> WidgetOption<State> for () {
fn get(self, _: &mut State) -> Option<StrongWidget> {
None
}
}
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F {
fn get(self, state: &mut State) -> Option<StrongWidget> {
self(state)
}
}
+64
View File
@@ -0,0 +1,64 @@
use super::*;
use crate::UiRsc;
use std::marker::Unsize;
pub struct WidgetTag;
impl<Rsc: UiRsc, W: Widget> WidgetLike<Rsc, WidgetTag> for W {
type Widget = W;
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
let w = rsc.ui_mut().widgets.add_weak(self);
rsc.on_add(w);
w
}
}
pub struct FnTag;
impl<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F {
type Widget = W;
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
self(rsc).add(rsc)
}
}
pub trait WidgetFnTrait<Rsc> {
type Widget: Widget;
fn run(self, rsc: &mut Rsc) -> Self::Widget;
}
pub struct FnTraitTag;
impl<Rsc: UiRsc, T: WidgetFnTrait<Rsc>> WidgetLike<Rsc, FnTraitTag> for T {
type Widget = T::Widget;
#[track_caller]
fn add(self, rsc: &mut Rsc) -> WeakWidget<T::Widget> {
self.run(rsc).add(rsc)
}
}
pub struct RefTag;
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>> WidgetLike<Rsc, RefTag>
for WeakWidget<W>
{
type Widget = W;
fn add(self, _: &mut Rsc) -> WeakWidget<W> {
self
}
}
pub struct RefFnTag;
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>, F: FnOnce(&mut Rsc) -> WeakWidget<W>>
WidgetLike<Rsc, RefFnTag> for F
{
type Widget = W;
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
self(rsc)
}
}
pub struct ViewTag;
impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
type Widget = V::Widget;
fn add(self, _: &mut Rsc) -> WeakWidget<Self::Widget> {
self.root()
}
}
pub struct ArrTag;
+24
View File
@@ -0,0 +1,24 @@
use std::marker::Unsize;
use crate::{IdLike, WeakWidget, Widget};
pub trait WidgetView {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
fn root(&self) -> WeakWidget<Self::Widget>;
}
pub trait HasWidget {
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
}
impl<W: Widget + Unsize<dyn Widget> + ?Sized> HasWidget for WeakWidget<W> {
type Widget = W;
}
impl<WV: WidgetView> IdLike for WV {
type Widget = WV::Widget;
fn id(&self) -> super::WidgetId {
self.root().id
}
}
+145
View File
@@ -0,0 +1,145 @@
use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
};
pub struct Widgets {
pub needs_redraw: HashSet<WidgetId>,
vec: SlotVec<WidgetData>,
send: Sender<WidgetId>,
recv: Receiver<WidgetId>,
pub(crate) waiting: HashSet<WidgetId>,
}
impl Widgets {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
needs_redraw: Default::default(),
vec: Default::default(),
waiting: Default::default(),
send,
recv,
}
}
pub fn has_updates(&self) -> bool {
!self.needs_redraw.is_empty()
}
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
Some(self.vec.get(id)?.widget.as_ref())
}
pub fn get_dyn_mut(&mut self, id: WidgetId) -> Option<&mut dyn Widget> {
self.needs_redraw.insert(id);
Some(self.vec.get_mut(id)?.widget.as_mut())
}
/// get_dyn but dynamic borrow checking of widgets
/// lets you do recursive (tree) operations, like the painter does
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
// SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
if data.borrowed {
panic!("tried to mutably borrow the same widget twice");
}
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
}
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
where
I::Widget: Sized + Widget,
{
self.get_dyn(id.id())?.as_any().downcast_ref()
}
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
where
I::Widget: Sized + Widget,
{
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
}
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
StrongWidget::new(id, self.send.clone())
}
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> {
let id = self.vec.add(WidgetData::new(widget));
self.waiting.insert(id);
WeakWidget::new(id)
}
#[track_caller]
pub fn upgrade<W: ?Sized>(&mut self, rf: WeakWidget<W>) -> StrongWidget<W> {
if !self.waiting.remove(&rf.id()) {
let label = self.label(rf);
let id = rf.id();
panic!(
"widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two"
)
}
StrongWidget::new(rf.id(), self.send.clone())
}
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
self.vec.get(id.id())
}
pub fn label(&self, id: impl IdLike) -> &String {
&self.data(id.id()).unwrap().label
}
/// useful for debugging
pub fn set_label(&mut self, id: impl IdLike, label: String) {
self.data_mut(id.id()).unwrap().label = label;
}
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id())
}
pub fn free_next(&mut self) -> Option<WidgetId> {
let next = self.recv.try_recv().ok()?;
self.vec.free(next);
Some(next)
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.vec.len()
}
}
impl Default for Widgets {
fn default() -> Self {
Self::new()
}
}
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
impl<I: IdLike> std::ops::Index<I> for Widgets
where
I::Widget: Sized + Widget,
{
type Output = I::Widget;
fn index(&self, id: I) -> &Self::Output {
self.get(&id).unwrap()
}
}
impl<I: IdLike> std::ops::IndexMut<I> for Widgets
where
I::Widget: Sized + Widget,
{
fn index_mut(&mut self, id: I) -> &mut Self::Output {
self.get_mut(&id).unwrap()
}
}
+12 -5
View File
@@ -1,14 +1,21 @@
use iris::{prelude::*, winit::*};
use iris::prelude::*;
fn main() {
DefaultApp::<State>::run();
}
struct State;
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(ui: &mut Ui, _state: &UiState, _proxy: Proxy<Self::Event>) -> Self {
rect(Color::RED).set_root(ui);
Self
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
File renamed without changes.
+87 -68
View File
@@ -1,23 +1,25 @@
use cosmic_text::Family;
use iris::{
prelude::*,
winit::{attr::Selectable, event::Submit, *},
};
use len_fns::*;
use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent;
use iris::prelude::*;
type ClientRsc = DefaultRsc<Client>;
fn main() {
DefaultApp::<Client>::run();
}
#[derive(DefaultUiState)]
pub struct Client {
info: WidgetId<Text>,
ui_state: DefaultUiState,
info: WeakWidget<Text>,
}
event_ctx!(Client);
impl DefaultAppState for Client {
fn new(ui: &mut Ui, _state: &UiState, _proxy: Proxy<Self::Event>) -> Self {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
@@ -40,7 +42,7 @@ impl DefaultAppState for Client {
.width(rest(3)),
)
.span(Dir::RIGHT)
.add(ui);
.add(rsc);
let span_test = (
rrect.color(Color::GREEN).width(100),
@@ -51,33 +53,30 @@ impl DefaultAppState for Client {
rrect.color(Color::RED).width(100),
)
.span(Dir::LEFT)
.add(ui);
.add(rsc);
let span_add = Span::empty(Dir::RIGHT).add(ui);
let span_add_ = span_add.clone();
let span_add = Span::empty(Dir::RIGHT).add(rsc);
let add_button = rect(Color::LIME)
.radius(30)
.on(CursorSense::click(), move |ctx| {
let child = ctx
.ui
.add(image(include_bytes!("assets/sungals.png")).center())
.any();
ctx.ui[&span_add_].children.push(child);
.on(CursorSense::click(), move |_, rsc| {
let child = image(include_bytes!("assets/sungals.png"))
.center()
.add_strong(rsc);
span_add(rsc).push(child);
})
.sized((150, 150))
.align(Align::BOT_RIGHT);
let span_add_ = span_add.clone();
let del_button = rect(Color::RED)
.radius(30)
.on(CursorSense::click(), move |ctx| {
ctx.ui[&span_add_].children.pop();
.on(CursorSense::click(), move |_, rsc| {
span_add(rsc).pop();
})
.sized((150, 150))
.align(Align::BOT_LEFT);
let span_add_test = (span_add, add_button, del_button).stack().add(ui);
let span_add_test = (span_add, add_button, del_button).stack().add(rsc);
let btext = |content| wtext(content).size(30);
@@ -100,36 +99,40 @@ impl DefaultAppState for Client {
wtext("pretty cool right?").size(50),
)
.span(Dir::DOWN)
.add(ui);
.add(rsc);
let texts = Span::empty(Dir::DOWN).gap(10).add(ui);
let msg_area = texts.clone().scroll().masked().background(rect(Color::SKY));
let texts = Span::empty(Dir::DOWN).gap(10).add(rsc);
let msg_area = texts.scrollable().masked().background(rect(Color::SKY));
let add_text = wtext("add")
.editable(false)
.editable(EditMode::MultiLine)
.text_align(Align::LEFT)
.size(30)
.attr::<Selectable>(())
.on(Submit, move |ctx| {
let content = ctx.ui.text(ctx.id).take();
.on(Submit, move |ctx, rsc| {
let w = ctx.widget;
let content = w.edit(rsc).take();
let text = wtext(content)
.editable(false)
.editable(EditMode::MultiLine)
.size(30)
.text_align(Align::LEFT)
.wrap(true)
.attr::<Selectable>(());
let msg_box = text.background(rect(Color::WHITE.darker(0.5))).add(ctx.ui);
ctx.ui[&texts].children.push(msg_box.any());
let msg_box = text
.background(rect(Color::WHITE.darker(0.5)))
.add_strong(rsc);
texts(rsc).push(msg_box);
})
.add(ui);
.add(rsc);
let text_edit_scroll = (
msg_area.height(rest(1)),
(
Rect::new(Color::WHITE.darker(0.9)),
(
add_text.clone().width(rest(1)),
add_text.width(rest(1)),
Rect::new(Color::GREEN)
.on(CursorSense::click(), move |ctx| {
ctx.ui.run_event(ctx.state, &add_text, Submit, ());
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
rsc.run_event::<Submit>(add_text, (), ctx.state);
})
.sized((40, 40)),
)
@@ -142,64 +145,80 @@ impl DefaultAppState for Client {
.align(Align::BOT),
)
.span(Dir::DOWN)
.add(ui);
.add(rsc);
let main = pad_test.clone().pad(10).add(ui);
let main = WidgetPtr::new().add(rsc);
let switch_button = |color, to: WidgetId, label| {
let main_ = main.clone();
let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| {
let to = to.upgrade(rsc);
let vec = &mut vals.borrow_mut().1;
let i = vec.len();
if vec.is_empty() {
vec.push(None);
main(rsc).set(to);
} else {
vec.push(Some(to));
}
let vals = vals.clone();
let rect = rect(color)
.on(CursorSense::click(), move |ctx| {
ctx.ui[&main_.clone()].inner = to.clone();
ctx.ui[ctx.id].color = color.darker(0.3);
.on(CursorSense::click(), move |ctx, rsc| {
let (prev, vec) = &mut *vals.borrow_mut();
if let Some(h) = vec[i].take() {
vec[*prev] = main(rsc).replace(h);
*prev = i;
}
ctx.widget(rsc).color = color.darker(0.3);
})
.on(
CursorSense::HoverStart | CursorSense::unclick(),
move |ctx| {
ctx.ui[ctx.id].color = color.brighter(0.2);
move |ctx, rsc| {
ctx.widget(rsc).color = color.brighter(0.2);
},
)
.on(CursorSense::HoverEnd, move |ctx| {
ctx.ui[ctx.id].color = color;
.on(CursorSense::HoverEnd, move |ctx, rsc| {
ctx.widget(rsc).color = color;
});
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
};
let tabs = (
switch_button(Color::RED, pad_test.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::RED, pad_test, "pad"),
switch_button(Color::GREEN, span_test, "span"),
switch_button(Color::BLUE, span_add_test, "image span"),
switch_button(Color::MAGENTA, text_test, "text layout"),
switch_button(
Color::YELLOW.mul_rgb(0.5),
text_edit_scroll.any(),
text_edit_scroll,
"text edit scroll",
),
)
.span(Dir::RIGHT);
let info = wtext("").add(ui);
let info_sect = info.clone().pad(10).align(Align::RIGHT);
let info = wtext("").add(rsc);
let info_sect = info.pad(10).align(Align::RIGHT);
((tabs.height(40), main).span(Dir::DOWN), info_sect)
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
.stack()
.set_root(ui);
.set_root(rsc, &mut ui_state);
Self { info }
Self { ui_state, info }
}
fn window_event(&mut self, _: WindowEvent, ui: &mut Ui, state: &UiState) {
fn window_event(
&mut self,
_: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
let new = format!(
"widgets: {}\nactive:{}\nviews: {}",
ui.num_widgets(),
ui.active_widgets(),
state.renderer.ui.view_count()
"widgets: {}\nactive: {}\nviews: {}",
rsc.widgets().len(),
render.active_widgets(),
self.ui_state.renderer.ui.view_count(),
);
if new != *ui[&self.info].content {
*ui[&self.info].content = new;
}
if ui.needs_redraw() {
state.window.request_redraw();
if new != *rsc.widgets()[self.info].content {
*rsc.widgets_mut()[self.info].content = new;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
use iris::prelude::*;
use std::time::Duration;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await;
ctx.update(move |_, rsc| {
let rect = rect(rsc);
if rect.color == Color::RED {
rect.color = Color::BLUE;
} else {
rect.color = Color::RED;
}
});
})
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+53
View File
@@ -0,0 +1,53 @@
use iris::prelude::*;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
type Rsc = DefaultRsc<State>;
#[derive(Clone, Copy, WidgetView)]
struct Test {
#[root]
root: WeakWidget<Rect>,
cur: WeakState<bool>,
}
impl Test {
pub fn new(rsc: &mut Rsc) -> Self {
let root = rect(Color::RED).add(rsc);
let cur = rsc.create_state(root, false);
Self { root, cur }
}
pub fn toggle(&self, rsc: &mut Rsc) {
let cur = &mut rsc[self.cur];
*cur = !*cur;
if *cur {
rsc[self.root].color = Color::BLUE;
} else {
rsc[self.root].color = Color::RED;
}
}
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
test.toggle(rsc);
})
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "iris-macro"
version.workspace = true
edition.workspace = true
[dependencies]
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = { version = "2.0.111", features = ["full"] }
[lib]
proc-macro = true
+196
View File
@@ -0,0 +1,196 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
Token, Type, Visibility,
parse::{Parse, ParseStream, Result},
parse_macro_input, parse_quote,
spanned::Spanned,
};
struct Input {
attrs: Vec<Attribute>,
vis: Visibility,
name: Ident,
generics: Generics,
fns: Vec<InputFn>,
}
struct InputFn {
sig: Signature,
body: Block,
}
impl Parse for Input {
fn parse(input: ParseStream) -> Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis = input.parse()?;
input.parse::<Token![trait]>()?;
let name = input.parse()?;
let generics = input.parse::<Generics>()?;
input.parse::<Token![;]>()?;
let mut fns = Vec::new();
while !input.is_empty() {
let sig = input.parse()?;
let body = input.parse()?;
fns.push(InputFn { sig, body })
}
if !input.is_empty() {
input.error("function expected");
}
Ok(Input {
attrs,
vis,
name,
generics,
fns,
})
}
}
#[proc_macro]
pub fn widget_trait(input: TokenStream) -> TokenStream {
let Input {
attrs,
vis,
name,
mut generics,
fns,
} = parse_macro_input!(input as Input);
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
let impls: Vec<_> = fns
.iter()
.map(|InputFn { sig, body }| quote! { #sig #body })
.collect();
let Some(GenericParam::Type(state)) = generics.params.first() else {
return Error::new(name.span(), "expected state generic parameter")
.into_compile_error()
.into();
};
let state = &state.ident;
generics
.params
.push(parse_quote!(WL: WidgetLike<#state, Tag>));
generics.params.push(parse_quote!(Tag));
let mut trai: ItemTrait = parse_quote!(
#vis trait #name #generics {
#(#sigs;)*
}
);
trai.attrs = attrs;
quote! {
#trai
impl #generics #name<Rsc, WL, Tag> for WL {
#(#impls)*
}
}
.into()
}
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))]
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input);
let mut found_attr = false;
let mut state_field = None;
for field in &state.fields {
if !found_attr
&& let Type::Path(path) = &field.ty
&& path.path.is_ident("DefaultUiState")
{
state_field = Some(field);
}
let Some(attr) = field
.attrs
.iter()
.find(|a| a.path().is_ident("default_ui_state"))
else {
continue;
};
if found_attr {
output.extend(
Error::new(
attr.span(),
"cannot have more than one default_ui_state attribute",
)
.into_compile_error(),
);
continue;
}
found_attr = true;
state_field = Some(field);
}
let Some(field) = state_field else {
output.extend(
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(),
);
return output.into();
};
let sname = &state.ident;
let fname = field.ident.as_ref().unwrap();
output.extend(quote! {
impl iris::default::HasDefaultUiState for #sname {
fn default_state(&self) -> &iris::default::DefaultUiState {
&self.#fname
}
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState {
&mut self.#fname
}
}
});
output.into()
}
#[proc_macro_derive(WidgetView, attributes(root))]
pub fn derive_widget_view(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input);
let mut found_attr = false;
let mut state_field = None;
for field in &state.fields {
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("root")) else {
continue;
};
if found_attr {
output.extend(
Error::new(attr.span(), "cannot have more than one root widget")
.into_compile_error(),
);
continue;
}
found_attr = true;
state_field = Some(field);
}
let Some(field) = state_field else {
output.extend(
Error::new(state.ident.span(), "no root widget field found (#[root])")
.into_compile_error(),
);
return output.into();
};
let sname = &state.ident;
let fname = field.ident.as_ref().unwrap();
let fty = &field.ty;
output.extend(quote! {
impl iris::core::WidgetView for #sname {
type Widget = <#fty as iris::core::HasWidget>::Widget;
fn root(&self) -> #fty {
self.#fname
}
}
});
output.into()
}
+12 -7
View File
@@ -1,22 +1,25 @@
# iris
my take on a rust ui library (also my first ui library)
My experimental attempt at 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
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
there's a `main.rs` that runs a testing window, so can just `cargo run` to see it working
Examples are in `examples`, eg. `cargo run --example tabs`.
goals, in general order:
Goals, in general order:
1. does what I want it to (text, images, video, animations)
2. very easy to use ignoring ergonomic ref counting
3. reasonably fast / efficient (a lot faster than electron, save battery life)
3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem)
not targeting web rn cause wanna use actual nice gpu features & entire point of this is to make desktop apps
## dev details
not targeting web rn cause wanna use actual nice gpu features & entire point of this is to make desktop apps / not need a web browser
general ideas trynna use rn / experiment with:
- retained mode
- specifically designed around wgpu
- specifically designed around wgpu so there's no translation
- postfix functions for most things to prevent unreadable indentation (going very well)
- events can be done directly where you draw the widgets
- almost no macros in user code & actual LSP typechecking (variadic generics if you can hear me please save us)
- relative anchor + absolute offset coord system (+ "rest" / leftover during widget layout)
- single threaded ui & pass context around to make non async usage straightforward (pretty unsure about this)
@@ -24,3 +27,5 @@ general ideas trynna use rn / experiment with:
under heavy initial development so not gonna try to explain status, maybe check TODO for that;
sizable chance it gets a rewrite once I know everything I need and what seems to work best
it's called iris because it's the structure around what you actually want to display and colorful
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["clippy", "rustfmt"]
-30
View File
@@ -1,30 +0,0 @@
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_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.width(id)
} else {
Len::ZERO
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.height(id)
} else {
Len::ZERO
}
}
}
-395
View File
@@ -1,395 +0,0 @@
use crate::prelude::*;
use std::{
ops::{BitOr, Deref, DerefMut},
rc::Rc,
};
use crate::{
layout::{UiModule, UiRegion, Vec2},
util::{HashMap, Id},
};
#[derive(Clone, Copy, PartialEq)]
pub enum Button {
Left,
Right,
Middle,
}
#[derive(Clone, Copy, 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 click_or_drag() -> CursorSenses {
Self::click() | Self::Pressing(Button::Left)
}
pub fn unclick() -> Self {
Self::PressEnd(Button::Left)
}
pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(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,
/// the (first) sense that triggered this event
/// the senses are checked in order
pub sense: CursorSense,
}
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);
}
}
}
}
impl Ui {
pub fn run_sensors<Ctx: 'static>(
&mut self,
ctx: &mut Ctx,
cursor: &CursorState,
window_size: Vec2,
) {
CursorModule::<Ctx>::run(self, ctx, cursor, window_size);
}
}
impl<Ctx: 'static> CursorModule<Ctx> {
pub fn run(ui: &mut Ui, ctx: &mut Ctx, cursor: &CursorState, window_size: Vec2) {
let layers = std::mem::take(&mut ui.data.layers);
let mut module = std::mem::take(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_px(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 let Some(sense) = 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,
sense,
};
(sensor.f)(EventCtx { ui, state: ctx, data });
}
}
}
if sensed {
break;
}
}
let ui_mod = ui.data.modules.get_mut::<Self>();
std::mem::swap(ui_mod, &mut module);
ui_mod.merge(module);
ui.data.layers = layers;
}
}
pub fn should_run(
senses: &CursorSenses,
cursor: &CursorState,
hover: ActivationState,
) -> Option<CursorSense> {
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 Some(*sense);
}
}
None
}
impl ActivationState {
pub fn is_start(&self) -> bool {
*self == Self::Start
}
pub fn is_on(&self) -> bool {
*self == Self::Start || *self == Self::On
}
pub fn is_end(&self) -> bool {
*self == Self::End
}
pub fn is_off(&self) -> bool {
*self == Self::End || *self == Self::Off
}
pub fn update(&mut self, on: bool) {
*self = match *self {
Self::Start => match on {
true => Self::On,
false => Self::End,
},
Self::On => match on {
true => Self::On,
false => Self::End,
},
Self::End => match on {
true => Self::Start,
false => Self::Off,
},
Self::Off => match on {
true => Self::Start,
false => Self::Off,
},
}
}
pub fn end_frame(&mut self) {
match self {
Self::Start => *self = Self::On,
Self::End => *self = Self::Off,
_ => (),
}
}
}
impl 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(EventCtx<Ctx, <E as Event>::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: EventCtx<Ctx, CursorData>| {
for f in &fs {
f(EventCtx {
state: ctx.state,
ui: ctx.ui,
data: 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(),
}
}
}
-128
View File
@@ -1,128 +0,0 @@
use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics};
use std::marker::Sized;
pub struct TextBuilder<O = TextOutput, H: WidgetOption = ()> {
pub content: String,
pub attrs: TextAttrs,
pub hint: H,
pub output: O,
}
impl<O, H: WidgetOption> TextBuilder<O, H> {
pub fn size(mut self, size: impl UiNum) -> Self {
self.attrs.font_size = size.to_f32();
self.attrs.line_height = self.attrs.font_size * LINE_HEIGHT_MULT;
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: impl Into<RegionAlign>) -> Self {
self.attrs.align = align.into();
self
}
pub fn center_text(mut self) -> Self {
self.attrs.align = Align::CENTER;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.attrs.wrap = wrap;
self
}
pub fn editable(self, single_line: bool) -> TextBuilder<TextEditOutput, H> {
TextBuilder {
content: self.content,
attrs: self.attrs,
hint: self.hint,
output: TextEditOutput { single_line },
}
}
}
impl<O> TextBuilder<O> {
pub fn hint<W: WidgetLike<Tag>, Tag>(self, hint: W) -> TextBuilder<O, impl WidgetOption> {
TextBuilder {
content: self.content,
attrs: self.attrs,
hint: move |ui: &mut Ui| Some(hint.add(ui).any()),
output: self.output,
}
}
}
pub trait TextBuilderOutput: Sized {
type Output;
fn run<H: WidgetOption>(ui: &mut Ui, builder: TextBuilder<Self, H>) -> Self::Output;
}
pub struct TextOutput;
impl TextBuilderOutput for TextOutput {
type Output = Text;
fn run<H: WidgetOption>(ui: &mut Ui, builder: TextBuilder<Self, H>) -> Self::Output {
let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let hint = builder.hint.get(ui);
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, hint),
};
text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text
}
}
pub struct TextEditOutput {
single_line: bool,
}
impl TextBuilderOutput for TextEditOutput {
type Output = TextEdit;
fn run<H: WidgetOption>(ui: &mut Ui, builder: TextBuilder<Self, H>) -> Self::Output {
let buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(ui)),
builder.output.single_line,
);
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, H: WidgetOption> FnOnce<(&mut Ui,)> for TextBuilder<O, H> {
type Output = O::Output;
extern "rust-call" fn call_once(self, args: (&mut Ui,)) -> Self::Output {
O::run(args.0, self)
}
}
pub fn wtext(content: impl Into<String>) -> TextBuilder {
TextBuilder {
content: content.into(),
attrs: TextAttrs::default(),
hint: (),
output: TextOutput,
}
}
-612
View File
@@ -1,612 +0,0 @@
use std::ops::{Deref, DerefMut};
use crate::prelude::*;
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use unicode_segmentation::UnicodeSegmentation;
use winit::{
event::KeyEvent,
keyboard::{Key, NamedKey},
};
pub struct TextEdit {
view: TextView,
selection: TextSelection,
history: Vec<(String, TextSelection)>,
double_hit: Option<Cursor>,
pub single_line: bool,
}
impl TextEdit {
pub fn new(view: TextView, single_line: bool) -> Self {
Self {
view,
selection: Default::default(),
history: Default::default(),
double_hit: None,
single_line,
}
}
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
let (start, end) = sort_cursors(start, end);
let mut iter = self.buf.lines.iter().skip(start.line);
let first = iter.next().unwrap();
if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
}
}
}
impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer;
painter.child_layer();
self.view.draw(painter);
painter.layer = base;
let region = self.region();
let size = vec2(1, self.attrs.line_height);
match self.selection {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(offset).within(&region),
);
}
}
TextSelection::Span { start, end } => {
let (start, end) = sort_cursors(start, end);
for (l, x, width) in iter_layout_lines(start, end, &self.buf) {
let top_left = vec2(x, self.attrs.line_height * l as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.with_x(width)
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
);
}
if let Some(end_offset) = cursor_pos(end, &self.buf) {
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT)
.offset(end_offset)
.within(&region),
);
}
}
}
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_height(ctx)
}
}
/// provides top left + width
fn iter_layout_lines(
start: Cursor,
end: Cursor,
buf: &TextBuffer,
) -> impl Iterator<Item = (usize, f32, f32)> {
gen move {
let mut iter = buf.layout_runs().enumerate();
for (i, line) in iter.by_ref() {
if line.line_i == start.line
&& let Some(start_x) = index_x(&line, start.index)
{
if start.line == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, start_x, end_x - start_x);
return;
}
yield (i, start_x, line.line_w - start_x);
break;
}
}
for (i, line) in iter {
if line.line_i > end.line {
return;
}
if line.line_i == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, 0.0, end_x);
return;
}
yield (i, 0.0, line.line_w);
}
}
}
/// copied & modified from fn found in Editor in cosmic_text
/// returns x pos of a (non layout) index within an layout run
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
for glyph in run.glyphs.iter() {
if index == glyph.start {
return Some(glyph.x);
} else if index > glyph.start && index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset);
}
}
None
}
/// returns top of line segment where cursor should visually select
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some(vec2(run.line_w, run.line_top));
if let Some(pos) = index_x(&run, cursor.index) {
return Some(vec2(pos, run.line_top));
}
}
prev
}
pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit,
pub 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);
self.text.selection.clear();
text
}
pub fn set(&mut self, text: &str) {
let text = self.string(text);
self.text
.buf
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
}
pub fn motion(&mut self, motion: Motion, select: bool) {
if let TextSelection::Pos(cursor) = self.text.selection
&& let Some(new) = self.buf_motion(cursor, motion)
{
if select {
self.text.selection = TextSelection::Span {
start: cursor,
end: new,
};
} else {
self.text.selection = TextSelection::Pos(new);
}
} else if let TextSelection::Span { start, end } = self.text.selection {
if select {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Span { start, end: cursor };
}
} else {
let (start, end) = sort_cursors(start, end);
let sel = &mut self.text.selection;
match motion {
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
_ => {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Pos(cursor);
}
}
}
}
}
}
pub fn replace(&mut self, len: usize, text: &str) {
let text = self.string(text);
for _ in 0..len {
self.delete(false);
}
self.insert_inner(&text, false);
}
fn string(&self, text: &str) -> String {
if self.text.single_line {
text.replace('\n', "")
} else {
text.to_string()
}
}
pub fn insert(&mut self, text: &str) {
let text = self.string(text);
let mut lines = text.split('\n');
let Some(first) = lines.next() else {
return;
};
self.insert_inner(first, true);
for line in lines {
self.newline();
self.insert_inner(line, true);
}
}
pub fn clear_span(&mut self) -> bool {
if let TextSelection::Span { start, end } = self.text.selection {
self.delete_between(start, end);
let (start, _) = sort_cursors(start, end);
self.text.selection = TextSelection::Pos(start);
true
} else {
false
}
}
pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
let lines = &mut self.text.view.buf.lines;
let (start, end) = sort_cursors(start, end);
if start.line == end.line {
let line = &mut lines[start.line];
let text = line.text();
let text = text[..start.index].to_string() + &text[end.index..];
edit_line(line, text);
} else {
// start
let start_text = lines[start.line].text()[..start.index].to_string();
let end_text = &lines[end.line].text()[end.index..];
let text = start_text + end_text;
edit_line(&mut lines[start.line], text);
}
// between
let range = (start.line + 1)..=end.line;
if !range.is_empty() {
lines.splice(range, None);
}
}
fn insert_inner(&mut self, text: &str, mov: bool) {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
edit_line(line, line_text);
if mov {
for _ in 0..text.chars().count() {
self.motion(Motion::Right, false);
}
}
}
}
pub fn newline(&mut self) {
if self.text.single_line {
return;
}
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
cursor.line += 1;
lines.insert(cursor.line, new);
cursor.index = 0;
}
}
pub fn backspace(&mut self, word: bool) {
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
self.delete(word);
}
}
pub fn delete(&mut self, word: bool) {
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
{
if word {
let start = *cursor;
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
self.delete_between(start, end);
}
} else {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
if cursor.line == lines.len() - 1 {
return;
}
let add = lines.remove(cursor.line + 1).into_text();
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
edit_line(line, cur);
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
edit_line(line, text);
}
}
}
}
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
self.text
.buf
.cursor_motion(self.font_system, cursor, None, motion)
.map(|r| r.0)
}
pub fn select_word_at(&mut self, cursor: Cursor) {
if let (Some(start), Some(end)) = (
self.buf_motion(cursor, Motion::LeftWord),
self.buf_motion(cursor, Motion::RightWord),
) {
self.text.selection = TextSelection::Span { start, end };
}
}
pub fn select_line_at(&mut self, cursor: Cursor) {
let end = self.text.buf.lines[cursor.line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(cursor.line, 0),
end: Cursor::new(cursor.line, end),
}
}
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_abs(size);
let hit = self.text.buf.hit(pos.x, pos.y);
let sel = &mut self.text.selection;
match sel {
TextSelection::None => {
if !drag && let Some(hit) = hit {
*sel = TextSelection::Pos(hit)
}
}
TextSelection::Pos(pos) => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => (),
(Some(hit), false) => {
if recent && hit == *pos {
self.text.double_hit = Some(hit);
return self.select_word_at(hit);
} else {
*pos = hit
}
}
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
},
TextSelection::Span { start, end } => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => *sel = TextSelection::Pos(*start),
(Some(hit), false) => {
if recent
&& let Some(double) = self.text.double_hit
&& double == hit
{
return self.select_line_at(hit);
} else {
*sel = TextSelection::Pos(hit)
}
}
(Some(hit), true) => *end = hit,
},
}
if let TextSelection::Span { start, end } = sel
&& start == end
{
*sel = TextSelection::Pos(*start);
}
}
pub fn deselect(&mut self) {
self.text.selection = TextSelection::None;
}
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.content(), self.text.selection);
let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo && let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
} else if self.text.content() != old.0 {
self.text.history.push(old);
}
res
}
fn apply_event_inner(
&mut self,
event: &KeyEvent,
modifiers: &Modifiers,
undo: &mut bool,
) -> TextInputResult {
match &event.logical_key {
Key::Named(named) => match named {
NamedKey::Backspace => self.backspace(modifiers.control),
NamedKey::Delete => self.delete(modifiers.control),
NamedKey::Space => self.insert(" "),
NamedKey::Enter => {
if modifiers.shift {
self.newline();
} else {
return TextInputResult::Submit;
}
}
NamedKey::ArrowRight => {
if modifiers.control {
self.motion(Motion::RightWord, modifiers.shift)
} else {
self.motion(Motion::Right, modifiers.shift)
}
}
NamedKey::ArrowLeft => {
if modifiers.control {
self.motion(Motion::LeftWord, modifiers.shift)
} else {
self.motion(Motion::Left, modifiers.shift)
}
}
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
NamedKey::Escape => {
self.deselect();
return TextInputResult::Unfocus;
}
_ => return TextInputResult::Unused,
},
Key::Character(text) => {
if modifiers.control {
match text.as_str() {
"v" => return TextInputResult::Paste,
"c" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
return TextInputResult::Copy(content);
}
}
"x" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
self.clear_span();
return TextInputResult::Copy(content);
}
}
"a" => {
if !self.text.buf.lines[0].text().is_empty()
|| self.text.buf.lines.len() > 1
{
let lines = &self.text.buf.lines;
let last_line = lines.len() - 1;
let last_idx = lines[last_line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(0, 0),
end: Cursor::new(last_line, last_idx),
};
}
}
"z" => {
*undo = true;
}
_ => self.insert(text),
}
} else {
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,
Copy(String),
Paste,
}
#[derive(Debug, Default, Clone, Copy)]
pub enum TextSelection {
#[default]
None,
Pos(Cursor),
Span {
start: Cursor,
end: Cursor,
},
}
impl TextSelection {
pub fn clear(&mut self) {
match self {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
TextSelection::Span { start: _, end: _ } => {
*self = TextSelection::None;
}
}
}
}
impl TextInputResult {
pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus)
}
}
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
}
}
-160
View File
@@ -1,160 +0,0 @@
use super::*;
use crate::prelude::*;
// these methods should "not require any context" (require unit) because they're in core
event_ctx!(());
pub trait CoreWidget<W, Tag> {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<Pad>;
fn align(self, align: impl Into<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 max_width(self, width: impl Into<Len>) -> impl WidgetFn<MaxSize>;
fn max_height(self, height: impl Into<Len>) -> impl WidgetFn<MaxSize>;
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Offset>;
fn scroll(self) -> impl WidgetIdFn<Scroll>;
fn masked(self) -> impl WidgetFn<Masked>;
fn background<T>(self, w: impl WidgetLike<T>) -> impl WidgetFn<Stack>;
fn foreground<T>(self, w: impl WidgetLike<T>) -> impl WidgetFn<Stack>;
fn layer_offset(self, offset: usize) -> impl WidgetFn<LayerOffset>;
fn to_any(self) -> impl WidgetRet;
}
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: impl Into<Align>) -> impl WidgetFn<Aligned> {
move |ui| Aligned {
inner: self.add(ui).any(),
align: align.into(),
}
}
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 max_width(self, len: impl Into<Len>) -> impl WidgetFn<MaxSize> {
let len = len.into();
move |ui| MaxSize {
inner: self.add(ui).any(),
x: Some(len),
y: None,
}
}
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<MaxSize> {
let len = len.into();
move |ui| MaxSize {
inner: self.add(ui).any(),
x: None,
y: Some(len),
}
}
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<Scroll> {
move |ui| {
Scroll::new(self.add(ui).any(), Axis::Y)
.on(CursorSense::Scroll, |ctx| {
let s = &mut ctx.ui[ctx.id];
s.scroll(ctx.data.scroll_delta.y * 50.0);
})
.add(ui)
}
}
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),
}
}
fn foreground<T>(self, w: impl WidgetLike<T>) -> impl WidgetFn<Stack> {
move |ui| Stack {
children: vec![self.add(ui).any(), w.add(ui).any()],
size: StackSize::Child(0),
}
}
fn layer_offset(self, offset: usize) -> impl WidgetFn<LayerOffset> {
move |ui| LayerOffset {
inner: self.add(ui).any(),
offset,
}
}
fn to_any(self) -> impl WidgetRet {
|ui| self.add(ui).any()
}
}
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)
}
}
+7
View File
@@ -11,6 +11,13 @@ pub trait AppState {
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
fn exit(&mut self);
fn run()
where
Self: Sized,
{
App::<Self>::run();
}
}
pub struct App<State: AppState> {
+78
View File
@@ -0,0 +1,78 @@
use crate::prelude::*;
use std::time::{Duration, Instant};
use winit::dpi::{LogicalPosition, LogicalSize};
pub struct Selector;
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector
where
Rsc::State: HasDefaultUiState,
{
type Input = WeakWidget<TextEdit>;
fn run(rsc: &mut Rsc, container: WeakWidget<W>, id: Self::Input) {
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| {
let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
let pos = ctx.data.pos + container_pos - id_pos;
let size = region.size();
select(
rsc,
ctx.data.render,
ctx.state,
id,
pos,
size,
ctx.data.sense.is_dragging(),
);
});
}
}
pub struct Selectable;
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
where
Rsc::State: HasDefaultUiState,
{
type Input = ();
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) {
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| {
select(
rsc,
ctx.data.render,
ctx.state,
id,
ctx.data.pos,
ctx.data.size,
ctx.data.sense.is_dragging(),
);
});
}
}
fn select(
rsc: &mut impl UiRsc,
render: &UiRenderState,
state: &mut impl HasDefaultUiState,
id: WeakWidget<TextEdit>,
pos: Vec2,
size: Vec2,
dragging: bool,
) {
let state = state.default_state_mut();
let now = Instant::now();
let recent = (now - state.last_click) < Duration::from_millis(300);
state.last_click = now;
id.edit(rsc).select(pos, size, dragging, recent);
if let Some(region) = render.window_region(&id) {
state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().tuple()),
);
}
state.focus = Some(id);
}
+9
View File
@@ -0,0 +1,9 @@
use iris_core::Event;
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Submit;
impl Event for Submit {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited;
impl Event for Edited {}
+2 -6
View File
@@ -1,8 +1,4 @@
use crate::{
core::{CursorState, Modifiers},
layout::Vec2,
winit::UiState,
};
use crate::prelude::*;
use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent},
keyboard::{Key, NamedKey},
@@ -70,7 +66,7 @@ impl Input {
}
}
impl UiState {
impl DefaultUiState {
pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size();
(size.width, size.height).into()
+354
View File
@@ -0,0 +1,354 @@
use crate::prelude::*;
use arboard::Clipboard;
use std::{
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::{Window, WindowAttributes},
};
mod app;
mod attr;
mod event;
mod input;
mod render;
mod sense;
mod state;
mod task;
pub use app::*;
pub use attr::*;
pub use event::*;
pub use input::*;
pub use render::*;
pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
pub renderer: UiRenderer,
pub input: Input,
pub focus: Option<WeakWidget<TextEdit>>,
pub clipboard: Clipboard,
pub window: Arc<Window>,
pub ime: usize,
pub last_click: Instant,
}
impl HasRoot for DefaultUiState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
impl DefaultUiState {
pub fn new(window: impl Into<Arc<Window>>) -> Self {
let window = window.into();
Self {
root: None,
renderer: UiRenderer::new(window.clone()),
window,
input: Input::default(),
clipboard: Clipboard::new().unwrap(),
ime: 0,
last_click: Instant::now(),
focus: None,
}
}
}
pub trait HasDefaultUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState;
}
pub trait DefaultAppState: HasDefaultUiState {
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
#[allow(unused_variables)]
fn event(
&mut self,
event: Self::Event,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
#[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {}
#[allow(unused_variables)]
fn window_event(
&mut self,
event: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
fn window_attributes() -> WindowAttributes {
Default::default()
}
}
pub struct DefaultRsc<State: 'static> {
pub ui: UiData,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> DefaultRsc<State> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(window);
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: Default::default(),
},
recv,
)
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
}
impl<State> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl<State: 'static> HasState for DefaultRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State: 'static> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
render: UiRenderState,
state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
}
impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop
.create_window(State::window_attributes())
.unwrap();
let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, proxy);
let render = UiRenderState::new();
Self {
rsc,
state,
render,
task_recv,
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event);
let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus;
if cursor_state.buttons.left.is_start() {
ui_state.focus = None;
}
if input_changed {
let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
}
let ui_state = state.default_state_mut();
if old != ui_state.focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
render.update(&ui_state.root, rsc);
ui_state.renderer.update(&mut rsc.ui, render);
ui_state.renderer.draw();
}
WindowEvent::Resized(size) => {
render.resize((size.width, size.height));
ui_state.renderer.resize(size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = ui_state.focus
&& event.state.is_pressed()
{
let mut text = sel.edit(rsc);
match text.apply_event(event, &ui_state.input.modifiers) {
TextInputResult::Unfocus => {
ui_state.focus = None;
ui_state.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
rsc.run_event::<Submit>(sel, (), state);
}
TextInputResult::Paste => {
if let Ok(t) = ui_state.clipboard.get_text() {
text.insert(&t);
}
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Copy(text) => {
if let Err(err) = ui_state.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}")
}
}
TextInputResult::Used => {
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Unused => {}
}
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = ui_state.focus {
let mut text = sel.edit(rsc);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(ui_state.ime, content);
ui_state.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(content);
}
}
}
}
_ => (),
}
state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
}
fn exit(&mut self) {
self.state.exit(&mut self.rsc, &mut self.render);
}
}
pub trait RscIdx<Rsc> {
type Output;
fn get(self, rsc: &Rsc) -> &Self::Output;
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
type Output = W;
fn get(self, rsc: &Rsc) -> &Self::Output {
&rsc.ui().widgets[self]
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
&mut rsc.ui_mut().widgets[self]
}
}
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
type Output = T;
fn get(self, rsc: &Rsc) -> &Self::Output {
rsc.widget_state().get(self)
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
rsc.widget_state_mut().get_mut(self)
}
}
+8 -16
View File
@@ -1,10 +1,7 @@
use crate::{
layout::Ui,
render::{UiLimits, UiRenderNode},
};
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::{util::StagingBelt, *};
use wgpu::*;
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK;
@@ -16,13 +13,12 @@ pub struct UiRenderer {
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderNode,
}
impl UiRenderer {
pub fn update(&mut self, updates: &mut Ui) {
self.ui.update(&self.device, &self.queue, updates);
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) {
self.ui.update(&self.device, &self.queue, ui, render);
}
pub fn draw(&mut self) {
@@ -49,16 +45,15 @@ impl UiRenderer {
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
self.window.pre_present_notify();
output.present();
self.staging_belt.recall();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui.resize(size, &self.queue);
self.ui.resize((size.width, size.height), &self.queue);
}
fn create_encoder(device: &Device) -> CommandEncoder {
@@ -72,7 +67,6 @@ impl UiRenderer {
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
flags: InstanceFlags::empty(),
..Default::default()
});
@@ -130,10 +124,9 @@ impl UiRenderer {
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UiRenderNode::new(&device, &queue, &config, ui_limits);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits);
Self {
surface,
@@ -141,8 +134,7 @@ impl UiRenderer {
queue,
config,
encoder,
staging_belt,
ui: shape_pipeline,
ui,
window,
}
}
+308
View File
@@ -0,0 +1,308 @@
use crate::prelude::*;
use std::{
ops::{BitOr, Deref, DerefMut},
rc::Rc,
};
#[derive(Clone, Copy, PartialEq)]
pub enum CursorButton {
Left,
Right,
Middle,
}
#[derive(Clone, Copy, PartialEq)]
pub enum CursorSense {
PressStart(CursorButton),
Pressing(CursorButton),
PressEnd(CursorButton),
HoverStart,
Hovering,
HoverEnd,
Scroll,
}
#[derive(Clone)]
pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses {
type Data<'a> = CursorData<'a>;
type State = SensorState;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone();
data.sense = sense;
Some(data)
} else {
None
}
}
}
impl CursorSense {
pub fn click() -> Self {
Self::PressStart(CursorButton::Left)
}
pub fn click_or_drag() -> CursorSenses {
Self::click() | Self::Pressing(CursorButton::Left)
}
pub fn unclick() -> Self {
Self::PressEnd(CursorButton::Left)
}
pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left))
}
}
#[derive(Default, Clone)]
pub struct CursorState {
pub pos: Vec2,
pub exists: bool,
pub buttons: CursorButtons,
pub scroll_delta: Vec2,
}
#[derive(Default, Clone)]
pub struct CursorButtons {
pub left: ActivationState,
pub middle: ActivationState,
pub right: ActivationState,
}
impl CursorButtons {
pub fn select(&self, button: &CursorButton) -> &ActivationState {
match button {
CursorButton::Left => &self.left,
CursorButton::Right => &self.right,
CursorButton::Middle => &self.middle,
}
}
pub fn end_frame(&mut self) {
self.left.end_frame();
self.middle.end_frame();
self.right.end_frame();
}
pub fn iter(&self) -> impl Iterator<Item = (CursorButton, &ActivationState)> {
[
CursorButton::Left,
CursorButton::Middle,
CursorButton::Right,
]
.into_iter()
.map(|b| (b, self.select(&b)))
}
}
impl CursorState {
pub fn end_frame(&mut self) {
self.buttons.end_frame();
self.scroll_delta = Vec2::ZERO;
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum ActivationState {
Start,
On,
End,
#[default]
Off,
}
/// this and other similar stuff has a generic
/// because I kind of want to make CursorModule generic
/// or basically have some way to have custom senses
/// that depend on active widget positions
/// but I'm not sure how or if worth it
pub struct Sensor<Ctx: HasEvents, Data> {
pub senses: CursorSenses,
pub f: Rc<dyn EventFn<Ctx, Data>>,
}
pub type SenseShape = UiRegion;
#[derive(Default, Debug)]
pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)]
pub struct CursorData<'a> {
/// where this widget was hit
pub pos: Vec2,
pub size: Vec2,
pub scroll_delta: Vec2,
pub hover: ActivationState,
pub cursor: CursorState,
/// the first sense that triggered this
pub sense: CursorSense,
pub render: &'a UiRenderState,
}
pub trait SensorUi {
fn run_sensors<Rsc: HasEvents>(
&self,
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
);
}
impl SensorUi for UiRenderState {
fn run_sensors<Rsc: HasEvents>(
&self,
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
) {
// in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there?
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
for layer in self.layers.indices().rev() {
let mut sensed = false;
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let shape = self.active.get(id).unwrap().region;
let region = shape.to_px(window_size);
let in_shape = cursor.exists && region.contains(cursor.pos);
sensor.hover.update(in_shape);
if sensor.hover == ActivationState::Off {
continue;
}
sensed = true;
let cursor = cursor.clone();
let data = CursorData {
pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta,
hover: sensor.hover,
cursor,
// this does not have any meaning;
// might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering,
render: self,
};
rsc.run_event::<CursorSense>(*id, data, state);
}
if sensed {
break;
}
}
rsc.events_mut().get_type::<CursorSense>().active = active;
}
}
pub fn should_run(
senses: &CursorSenses,
cursor: &CursorState,
hover: ActivationState,
) -> Option<CursorSense> {
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 Some(*sense);
}
}
None
}
impl ActivationState {
pub fn is_start(&self) -> bool {
*self == Self::Start
}
pub fn is_on(&self) -> bool {
*self == Self::Start || *self == Self::On
}
pub fn is_end(&self) -> bool {
*self == Self::End
}
pub fn is_off(&self) -> bool {
*self == Self::End || *self == Self::Off
}
pub fn update(&mut self, on: bool) {
*self = match *self {
Self::Start => match on {
true => Self::On,
false => Self::End,
},
Self::On => match on {
true => Self::On,
false => Self::End,
},
Self::End => match on {
true => Self::Start,
false => Self::Off,
},
Self::Off => match on {
true => Self::Start,
false => Self::Off,
},
}
}
pub fn end_frame(&mut self) {
match self {
Self::Start => *self = Self::On,
Self::End => *self = Self::Off,
_ => (),
}
}
}
impl EventLike for CursorSense {
type Event = CursorSenses;
fn into_event(self) -> Self::Event {
self.into()
}
}
impl Deref for CursorSenses {
type Target = Vec<CursorSense>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CursorSenses {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<CursorSense> for CursorSenses {
fn from(val: CursorSense) -> Self {
CursorSenses(vec![val])
}
}
impl BitOr for CursorSense {
type Output = CursorSenses;
fn bitor(self, rhs: Self) -> Self::Output {
CursorSenses(vec![self, rhs])
}
}
impl BitOr<CursorSense> for CursorSenses {
type Output = Self;
fn bitor(mut self, rhs: CursorSense) -> Self::Output {
self.0.push(rhs);
self
}
}
+75
View File
@@ -0,0 +1,75 @@
use iris_core::{
WidgetId,
util::{HashMap, HashSet},
};
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Key {
id: WidgetId,
ty: TypeId,
i: usize,
}
#[derive(Default)]
pub struct WidgetState {
widgets: HashMap<WidgetId, HashSet<(TypeId, usize)>>,
counts: HashMap<(WidgetId, TypeId), usize>,
map: HashMap<Key, Box<dyn Any>>,
}
impl WidgetState {
pub fn new() -> Self {
Self::default()
}
pub fn add<T: 'static>(&mut self, id: WidgetId, data: T) -> WeakState<T> {
let ty = TypeId::of::<T>();
let count = self.counts.entry((id, ty)).or_default();
let i = *count;
let key = Key { ty, i, id };
self.map.insert(key, Box::new(data));
self.widgets.entry(id).or_default().insert((ty, i));
*count += 1;
WeakState {
key,
_pd: PhantomData,
}
}
pub fn remove(&mut self, id: WidgetId) {
for &(ty, i) in self.widgets.get(&id).into_iter().flatten() {
self.map.remove(&Key { id, ty, i });
}
}
pub fn get<T: 'static>(&self, state: WeakState<T>) -> &T {
self.map.get(&state.key).unwrap().downcast_ref().unwrap()
}
pub fn get_mut<T: 'static>(&mut self, state: WeakState<T>) -> &mut T {
self.map
.get_mut(&state.key)
.unwrap()
.downcast_mut()
.unwrap()
}
}
#[derive(Clone, Copy)]
pub struct WeakState<T> {
key: Key,
_pd: PhantomData<T>,
}
pub trait HasWidgetState {
fn widget_state(&self) -> &WidgetState;
fn widget_state_mut(&mut self) -> &mut WidgetState;
}
impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState<T> {
type Output = &'a mut T;
extern "rust-call" fn call_once(self, (state,): (&'a mut WidgetState,)) -> Self::Output {
state.get_mut(self)
}
}
+82
View File
@@ -0,0 +1,82 @@
use iris_core::HasState;
use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use tokio::{
runtime::Runtime,
sync::mpsc::{
UnboundedReceiver as AsyncReceiver, UnboundedSender as AsyncSender,
unbounded_channel as async_channel,
},
};
use winit::window::Window;
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>,
window: Arc<Window>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
}
pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>,
}
impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f));
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv))
});
(
Self {
start,
msg_send: msgs,
window,
},
msgs_recv,
)
}
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
let send = self.msg_send.clone();
let window = self.window.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await;
window.request_redraw();
}));
}
}
async fn listen(mut recv: AsyncReceiver<BoxTask>) {
while let Some(task) = recv.recv().await {
tokio::spawn(task);
}
}
+87
View File
@@ -0,0 +1,87 @@
use iris_core::*;
use iris_macro::*;
use std::sync::Arc;
use crate::default::{TaskCtx, TaskUpdate, Tasks};
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, Self::Widget>,
) -> impl WidgetIdFn<Rsc, Self::Widget> {
move |rsc| {
let id = self.add(rsc);
rsc.register_event(id, event.into_event(), move |ctx, rsc| {
f(
EventIdCtx {
widget: id,
state: ctx.state,
data: ctx.data,
},
rsc,
);
});
id
}
}
}
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
widget_trait! {
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
fn task_on<'a, E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
self,
event: E,
f: F,
) -> impl WidgetIdFn<Rsc, WL::Widget>
where <E::Event as Event>::Data<'a>: Send,
for<'b> F::CallRefFuture<'b>: Send,
{
let f = Arc::new(f);
move |rsc| {
let id = self.add(rsc);
rsc.register_event(id, event.into_event(), move |_, rsc| {
let f = f.clone();
rsc.tasks_mut().spawn(async move |task| {
f(AsyncEventIdCtx {
widget: id,
task,
}).await;
});
});
id
}
}
}
pub trait HasTasks: Sized + HasState + HasEvents {
fn tasks_mut(&mut self) -> &mut Tasks<Self>;
fn spawn_task<F: AsyncFnOnce(TaskCtx<Self>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
self.tasks_mut().spawn(task);
}
}
pub trait AsyncWidgetEventFn<Rsc: HasEvents, W: ?Sized>:
AsyncFn(AsyncEventIdCtx<Rsc, W>) + Send + Sync + 'static
{
}
impl<Rsc: HasEvents, F: AsyncFn(AsyncEventIdCtx<Rsc, W>) + Send + Sync + 'static, W: ?Sized>
AsyncWidgetEventFn<Rsc, W> for F
{
}
pub struct AsyncEventIdCtx<Rsc: HasEvents, W: ?Sized> {
pub widget: WeakWidget<W>,
task: TaskCtx<Rsc>,
}
impl<Rsc: HasEvents, W: ?Sized> AsyncEventIdCtx<Rsc, W> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
self.task.update(f);
}
}
-20
View File
@@ -1,20 +0,0 @@
use crate::layout::{Ui, WidgetId, WidgetIdFn, WidgetLike};
pub trait WidgetAttr<W> {
type Input;
fn run(ui: &mut Ui, id: &WidgetId<W>, input: Self::Input);
}
pub trait Attrable<W, Tag> {
fn attr<A: WidgetAttr<W>>(self, input: A::Input) -> impl WidgetIdFn<W>;
}
impl<WL: WidgetLike<Tag>, Tag> Attrable<WL::Widget, Tag> for WL {
fn attr<A: WidgetAttr<WL::Widget>>(self, input: A::Input) -> impl WidgetIdFn<WL::Widget> {
|ui| {
let id = self.add(ui);
A::run(ui, &id, input);
id
}
}
}
-27
View File
@@ -1,27 +0,0 @@
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();
}
}
}
-215
View File
@@ -1,215 +0,0 @@
use std::{hash::Hash, rc::Rc};
use crate::{
layout::{IdFnTag, Ui, UiModule, WidgetId, WidgetIdFn, WidgetLike},
util::{HashMap, Id},
};
pub trait Event: Sized {
type Module<Ctx: 'static>: EventModule<Self, Ctx>;
type Data: Clone;
}
pub struct EventCtx<'a, Ctx, Data> {
pub ui: &'a mut Ui,
pub state: &'a mut Ctx,
pub data: Data,
}
pub type ECtx<'a, Ctx, Data, W> = EventIdCtx<'a, Ctx, Data, W>;
pub struct EventIdCtx<'a, Ctx, Data, W> {
pub id: &'a WidgetId<W>,
pub ui: &'a mut Ui,
pub state: &'a mut Ctx,
pub data: Data,
}
pub trait EventFn<Ctx, Data>: Fn(EventCtx<Ctx, Data>) + 'static {}
impl<F: Fn(EventCtx<Ctx, Data>) + 'static, Ctx, Data> EventFn<Ctx, Data> for F {}
pub trait WidgetEventFn<Ctx, Data, W>: Fn(EventIdCtx<Ctx, Data, W>) + 'static {}
impl<F: Fn(EventIdCtx<Ctx, Data, W>) + 'static, Ctx, Data, W> WidgetEventFn<Ctx, Data, W> for F {}
// TODO: naming in here is a bit weird like eventable
#[macro_export]
macro_rules! event_ctx {
($ty: ty) => {
mod local_event_trait {
use super::*;
#[allow(unused_imports)]
use $crate::prelude::*;
pub trait EventableCtx<W, Tag, Ctx: 'static> {
fn on<E: Event>(
self,
event: E,
f: impl WidgetEventFn<Ctx, E::Data, W>,
) -> impl WidgetIdFn<W> + EventableCtx<W, IdFnTag, Ctx>;
}
impl<WL: WidgetLike<Tag>, Tag> EventableCtx<WL::Widget, Tag, $ty> for WL {
fn on<E: Event>(
self,
event: E,
f: impl WidgetEventFn<$ty, E::Data, WL::Widget>,
) -> impl WidgetIdFn<WL::Widget> + EventableCtx<WL::Widget, IdFnTag, $ty> {
eventable::Eventable::on(self, event, f)
}
}
}
use local_event_trait::*;
};
}
pub use event_ctx;
pub mod eventable {
use super::*;
pub trait Eventable<W, Tag> {
fn on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl WidgetEventFn<Ctx, E::Data, W>,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>;
}
impl<WL: WidgetLike<Tag>, Tag> Eventable<WL::Widget, Tag> for WL {
fn on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl WidgetEventFn<Ctx, E::Data, WL::Widget>,
) -> impl WidgetIdFn<WL::Widget> {
move |ui| {
let id = self.add(ui);
let id_ = id.weak();
ui.register_event(&id, event, move |ctx| {
f(EventIdCtx {
id: &id_.strong(),
state: ctx.state,
data: ctx.data,
ui: ctx.ui,
});
});
id
}
}
}
}
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(EventCtx<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(EventCtx<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: EventCtx<Ctx, <E as Event>::Data>| {
for f in &fs {
f(EventCtx {
ui: ctx.ui,
state: ctx.state,
data: ctx.data.clone(),
})
}
})
} else {
None
}
}
}
impl<E: HashableEvent, Ctx: 'static> DefaultEventModule<E, Ctx> {
pub fn run_all(&self, event: E, ctx: EventCtx<Ctx, E::Data>)
where
E::Data: Clone,
{
if let Some(map) = self.map.get(&event) {
for fs in map.values() {
for f in fs {
f(EventCtx {
ui: ctx.ui,
state: ctx.state,
data: 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: 'static, W>(
&mut self,
ctx: &mut Ctx,
id: &WidgetId<W>,
event: E,
data: E::Data,
) {
if let Some(f) = self
.data
.modules
.get_mut::<E::Module<Ctx>>()
.run(&id.id, event)
{
f(EventCtx {
ui: self,
state: ctx,
data,
});
}
}
}
-180
View File
@@ -1,180 +0,0 @@
use std::{any::TypeId, marker::PhantomData, sync::mpsc::Sender};
use crate::{
layout::{Ui, WidgetLike},
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>,
_pd: PhantomData<W>,
}
#[repr(C)]
pub struct WeakWidgetId<W = AnyWidget> {
pub(super) ty: TypeId,
pub(super) id: Id,
counter: RefCounter,
send: Sender<Id>,
_pd: PhantomData<W>,
}
impl<W> PartialEq for WidgetId<W> {
fn eq(&self, other: &Self) -> bool {
self.ty == other.ty && self.id == other.id
}
}
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(),
_pd: PhantomData,
}
}
}
impl<W> WidgetId<W> {
pub(super) fn new(id: Id, ty: TypeId, send: Sender<Id>) -> Self {
Self {
ty,
id,
counter: RefCounter::new(),
send,
_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 weak(&self) -> WeakWidgetId<W> {
let Self {
ty,
id,
ref counter,
ref send,
_pd,
} = *self;
WeakWidgetId {
ty,
id,
counter: counter.quiet_clone(),
send: send.clone(),
_pd,
}
}
}
impl<W> WeakWidgetId<W> {
/// should guarantee that widget is still valid to prevent indexing failures
pub(crate) fn strong(&self) -> WidgetId<W> {
let Self {
ty,
id,
ref counter,
ref send,
_pd,
} = *self;
WidgetId {
ty,
id,
counter: counter.clone(),
send: send.clone(),
_pd,
}
}
}
impl<W> Drop for WidgetId<W> {
fn drop(&mut self) {
if self.counter.drop() {
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 {}
pub trait WidgetRet: FnOnce(&mut Ui) -> WidgetId<AnyWidget> {}
impl<F: FnOnce(&mut Ui) -> WidgetId<AnyWidget>> WidgetRet for F {}
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)
}
}
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()
}
}
pub trait IdLike<W> {
fn id(&self) -> Id;
}
impl<W> IdLike<W> for WidgetId<W> {
fn id(&self) -> Id {
self.id
}
}
-46
View File
@@ -1,46 +0,0 @@
//! 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) {}
}
-33
View File
@@ -1,33 +0,0 @@
mod color;
mod event;
mod id;
mod layer;
mod module;
mod num;
mod orientation;
mod painter;
mod text;
mod texture;
mod ui;
mod attr;
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 text::*;
pub use texture::*;
pub use ui::*;
pub use attr::*;
pub use vec2::*;
pub use widget::*;
pub use widgets::*;
pub type UiColor = Color<u8>;
-35
View File
@@ -1,35 +0,0 @@
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
View File
@@ -1,21 +0,0 @@
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
}
}
-591
View File
@@ -1,591 +0,0 @@
use crate::{
layout::{
Axis, Len, Modules, PrimitiveLayers, RenderedText, Size, TextAttrs, TextBuffer, TextData,
TextureHandle, Textures, UiRegion, UiVec2, Vec2, WidgetId, Widgets,
},
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
util::{HashMap, HashSet, Id, TrackedArena},
};
/// makes your surfaces look pretty
pub struct Painter<'a, 'c> {
ctx: &'a mut PainterCtx<'c>,
region: UiRegion,
mask: MaskIdx,
textures: Vec<TextureHandle>,
primitives: Vec<PrimitiveHandle>,
children: Vec<Id>,
children_width: HashMap<Id, (UiVec2, Len)>,
children_height: HashMap<Id, (UiVec2, Len)>,
pub layer: usize,
id: Id,
}
/// context for a painter; lets you draw and redraw widgets
struct PainterCtx<'a> {
pub widgets: &'a Widgets,
pub active: &'a mut HashMap<Id, WidgetInstance>,
pub layers: &'a mut PrimitiveLayers,
pub textures: &'a mut Textures,
pub masks: &'a mut TrackedArena<Mask, u32>,
pub text: &'a mut TextData,
pub output_size: Vec2,
pub modules: &'a mut Modules,
pub cache_width: HashMap<Id, (UiVec2, Len)>,
pub cache_height: HashMap<Id, (UiVec2, Len)>,
pub needs_redraw: HashSet<Id>,
draw_started: HashSet<Id>,
}
/// stores information for children about the highest level parent that needed their size
/// so that they can redraw the parent if their size changes
#[derive(Clone, Copy, Debug, Default)]
pub struct ResizeRef {
x: Option<(Id, (UiVec2, Len))>,
y: Option<(Id, (UiVec2, Len))>,
}
/// important non rendering data for retained drawing
#[derive(Debug)]
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: ResizeRef,
pub mask: MaskIdx,
pub layer: usize,
}
/// data to be stored in Ui to create PainterCtxs easily
#[derive(Default)]
pub struct PainterData {
pub widgets: Widgets,
pub active: HashMap<Id, WidgetInstance>,
pub layers: PrimitiveLayers,
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> {
/// redraws a widget that's currently active (drawn)
/// can be called on something already drawn or removed,
/// will just return if so
pub fn redraw(&mut self, id: Id) {
self.needs_redraw.remove(&id);
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.active.get(&id) else {
return;
};
let mut resize = active.resize;
// set resize back after redrawing
let finish = |s: &mut Self, resize| {
if let Some(active) = s.active.get_mut(&id) {
// might need to get_or_insert here instead of just assuming
active.resize = resize;
}
};
// check if a parent depends on the desired size of this, if so then redraw it first
// TODO: this is stupid having 2 of these, don't ask me what the consequences are
let mut ret = false;
if let Some((rid, (outer, old_desired))) = &mut resize.x {
let new_desired = SizeCtx {
source: id,
cache_width: &mut self.cache_width,
cache_height: &mut self.cache_height,
text: self.text,
textures: self.textures,
widgets: self.widgets,
outer: *outer,
output_size: self.output_size,
checked_width: &mut Default::default(),
checked_height: &mut Default::default(),
id,
}
.width_inner(id);
if new_desired != *old_desired {
// unsure if I need to walk down the tree here
self.redraw(*rid);
*old_desired = new_desired;
if self.draw_started.contains(&id) {
ret = true;
}
}
}
if let Some((rid, (outer, old_desired))) = &mut resize.y {
// NOTE: might need hack in Span here (or also do it properly here)
let new_desired = SizeCtx {
source: id,
cache_width: &mut self.cache_width,
cache_height: &mut self.cache_height,
text: self.text,
textures: self.textures,
widgets: self.widgets,
outer: *outer,
output_size: self.output_size,
checked_width: &mut Default::default(),
checked_height: &mut Default::default(),
id,
}
.height_inner(id);
if new_desired != *old_desired {
self.redraw(*rid);
*old_desired = new_desired;
if self.draw_started.contains(&id) {
ret = true;
}
}
}
if ret {
return finish(self, resize);
}
let Some(active) = self.remove(id) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
);
finish(self, resize);
}
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 = ResizeRef::default();
if let Some(active) = self.active.get_mut(&id)
&& !self.needs_redraw.contains(&id)
{
// check to see if we can skip drawing first
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;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id).unwrap();
old_children = active.children;
resize = active.resize;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
ctx: self,
children: Vec::new(),
children_width: Default::default(),
children_height: Default::default(),
};
painter.ctx.widgets.get_dyn_dynamic(id).draw(&mut painter);
let children_width = painter.children_width;
let children_height = painter.children_height;
// add to active
let instance = WidgetInstance {
id,
region,
parent,
textures: painter.textures,
primitives: painter.primitives,
children: painter.children,
resize,
mask: painter.mask,
layer,
};
// set resize for children who's size this widget depends on
for (cid, outer) in children_width {
if let Some(w) = self.active.get_mut(&cid)
&& w.resize.x.is_none()
{
w.resize.x = Some((id, outer))
}
}
for (cid, outer) in children_height {
if let Some(w) = self.active.get_mut(&cid)
&& w.resize.y.is_none()
{
w.resize.y = Some((id, outer))
}
}
// remove old children that weren't kept
for c in &old_children {
if !instance.children.contains(c) {
self.remove_rec(*c);
}
}
// update modules
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();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
for m in self.modules.iter_mut() {
m.on_move(active);
}
// children will not be changed, so this technically should not be needed
// probably need unsafe
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);
}
}
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 PainterData {
fn ctx(&mut self, needs_redraw: HashSet<Id>) -> PainterCtx<'_> {
PainterCtx {
widgets: &self.widgets,
active: &mut self.active,
layers: &mut self.layers,
textures: &mut self.textures,
text: &mut self.text,
output_size: self.output_size,
modules: &mut self.modules,
masks: &mut self.masks,
cache_width: Default::default(),
cache_height: Default::default(),
draw_started: Default::default(),
needs_redraw,
}
}
pub fn draw(&mut self, id: Id) {
let mut ctx = self.ctx(Default::default());
ctx.draw_started.clear();
ctx.layers.clear();
ctx.draw_inner(0, id, UiRegion::FULL, None, MaskIdx::NONE, None);
}
pub fn redraw(&mut self, ids: HashSet<Id>) {
let mut ctx = self.ctx(ids);
while let Some(&id) = ctx.needs_redraw.iter().next() {
ctx.redraw(id);
}
}
}
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) -> RenderedText {
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 len_axis<W>(&mut self, id: &WidgetId<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
}
}
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
SizeCtx {
text: self.ctx.text,
textures: self.ctx.textures,
widgets: self.ctx.widgets,
output_size: self.ctx.output_size,
checked_width: &mut self.children_width,
checked_height: &mut self.children_height,
cache_width: &mut self.ctx.cache_width,
cache_height: &mut self.ctx.cache_height,
source: self.id,
id: self.id,
outer: self.region.size(),
}
}
pub fn output_size(&self) -> Vec2 {
self.ctx.output_size
}
pub fn px_size(&mut self) -> Vec2 {
self.region.size().to_abs(self.ctx.output_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 fn label(&self) -> &str {
&self.ctx.widgets.data(&self.id).unwrap().label
}
pub fn id(&self) -> &Id {
&self.id
}
}
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub textures: &'a mut Textures,
source: Id,
widgets: &'a Widgets,
cache_width: &'a mut HashMap<Id, (UiVec2, Len)>,
cache_height: &'a mut HashMap<Id, (UiVec2, Len)>,
checked_width: &'a mut HashMap<Id, (UiVec2, Len)>,
checked_height: &'a mut HashMap<Id, (UiVec2, Len)>,
/// TODO: should this be pub? rn used for sized
pub outer: UiVec2,
output_size: Vec2,
id: Id,
}
impl SizeCtx<'_> {
pub fn id(&self) -> &Id {
&self.id
}
pub fn source(&self) -> &Id {
&self.source
}
fn width_inner(&mut self, id: Id) -> Len {
// first check cache
// TODO: is this needed? broken rn bc does not store children during upper size check,
// so if something actually using check_* hits cache it fails to add them
// if let Some(&(outer, len)) = self.cache_width.get(&id)
// && outer == self.outer
// {
// self.checked_width.insert(id, (self.outer, len));
// return len;
// }
// store self vars that need to be maintained
let self_outer = self.outer;
let self_id = self.id;
// get size of input id
self.id = id;
let len = self.widgets.get_dyn_dynamic(id).desired_width(self);
// restore vars & update cache + checked
self.outer = self_outer;
self.id = self_id;
self.cache_width.insert(id, (self.outer, len));
self.checked_width.insert(id, (self.outer, len));
len
}
// TODO: should be refactored to share code w width_inner
fn height_inner(&mut self, id: Id) -> Len {
// if let Some(&(outer, len)) = self.cache_height.get(&id)
// && outer == self.outer
// {
// self.checked_height.insert(id, (self.outer, len));
// return len;
// }
let self_outer = self.outer;
let self_id = self.id;
self.id = id;
let len = self.widgets.get_dyn_dynamic(id).desired_height(self);
self.outer = self_outer;
self.id = self_id;
self.cache_height.insert(id, (self.outer, len));
self.checked_height.insert(id, (self.outer, len));
len
}
pub fn width<W>(&mut self, id: &WidgetId<W>) -> Len {
self.width_inner(id.id)
}
pub fn height<W>(&mut self, id: &WidgetId<W>) -> Len {
self.height_inner(id.id)
}
pub fn len_axis<W>(&mut self, id: &WidgetId<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.width(id),
Axis::Y => self.height(id),
}
}
pub fn size<W>(&mut self, id: &WidgetId<W>) -> Size {
Size {
x: self.width(id),
y: self.height(id),
}
}
pub fn px_size(&mut self) -> Vec2 {
self.outer.to_abs(self.output_size)
}
pub fn output_size(&mut self) -> Vec2 {
self.output_size
}
pub fn draw_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
self.text.draw(buffer, attrs, self.textures)
}
pub fn label(&self, id: &Id) -> &String {
self.widgets.label(id)
}
}
-188
View File
@@ -1,188 +0,0 @@
use std::simd::{Simd, num::SimdUint};
use crate::layout::{Align, RegionAlign, TextureHandle, Textures, UiColor, Vec2};
use cosmic_text::{
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
SwashContent,
};
use image::{GenericImageView, RgbaImage};
/// TODO: properly wrap this
pub mod text_lib {
pub use cosmic_text::*;
}
pub struct TextData {
pub font_system: FontSystem,
pub swash_cache: SwashCache,
glyph_cache: Vec<(Placement, CacheKey, Color)>,
}
impl Default for TextData {
fn default() -> Self {
Self {
font_system: FontSystem::new(),
swash_cache: SwashCache::new(),
glyph_cache: Default::default(),
}
}
}
#[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 it's drawn)
pub align: RegionAlign,
}
impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics_and_size(
font_system,
Metrics::new(self.font_size, self.line_height),
width,
None,
);
let attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs);
for line in &mut buf.lines {
line.set_attrs_list(list.clone());
}
}
}
pub type TextBuffer = Buffer;
impl Default for TextAttrs {
fn default() -> Self {
let size = 14.0;
Self {
color: UiColor::WHITE,
font_size: size,
line_height: size * LINE_HEIGHT_MULT,
family: Family::SansSerif,
wrap: false,
align: Align::CENTER_LEFT,
}
}
}
pub const LINE_HEIGHT_MULT: f32 = 1.1;
impl TextData {
pub fn draw(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
textures: &mut Textures,
) -> RenderedText {
// TODO: either this or the layout stuff (or both) is super slow,
// should probably do texture packing and things if possible.
// very visible if you add just a couple of wrapping texts and resize window
// should also be timed to figure out exactly what points need to be sped up
// let mut pixels = HashMap::<_, [u8; 4]>::default();
let mut min_x = 0;
let mut min_y = 0;
let mut max_x = 0;
let mut max_y = 0;
let text_color = {
let c = attrs.color;
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
};
let mut max_width = 0.0f32;
let mut height = 0.0;
for run in buffer.layout_runs() {
for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((0., 0.), 1.0);
let glyph_color = match glyph.color_opt {
Some(some) => some,
None => text_color,
};
if let Some(img) = self
.swash_cache
.get_image(&mut self.font_system, physical_glyph.cache_key)
{
let mut pos = img.placement;
pos.left += physical_glyph.x;
pos.top = physical_glyph.y + run.line_y as i32 - pos.top;
min_x = min_x.min(pos.left);
min_y = min_y.min(pos.top);
max_x = max_x.max(pos.left + pos.width as i32);
max_y = max_y.max(pos.top + pos.height as i32);
self.glyph_cache
.push((pos, physical_glyph.cache_key, glyph_color));
}
}
max_width = max_width.max(run.line_w);
height += run.line_height;
}
let img_width = (max_x - min_x + 1) as u32;
let img_height = (max_y - min_y + 1) as u32;
let mut image = RgbaImage::new(img_width, img_height);
for (pos, key, color) in self.glyph_cache.drain(..) {
let img = self
.swash_cache
.get_image(&mut self.font_system, key)
.as_ref()
.unwrap();
let mut merge = |i, color: [u8; 4]| {
let i = i as i32;
let x = (i % pos.width as i32 + pos.left - min_x) as u32;
let y = (i / pos.width as i32 + pos.top - min_y) as u32;
let pixel = &mut image[(x, y)].0;
// TODO: no clue if proper alpha blending should be done
*pixel = Simd::from(color).saturating_add(Simd::from(*pixel)).into();
};
match img.content {
SwashContent::Mask => {
for (i, a) in img.data.iter().enumerate() {
let mut color = color.as_rgba();
color[3] = ((color[3] as u32 * *a as u32) / u8::MAX as u32) as u8;
merge(i, color);
}
}
SwashContent::SubpixelMask => todo!("subpixel mask text rendering"),
SwashContent::Color => {
let (colors, _) = img.data.as_chunks::<4>();
for (i, color) in colors.iter().enumerate() {
merge(i, *color);
}
}
}
}
let max_dim = 8192;
if image.width() > max_dim || image.height() > max_dim {
let width = image.width().min(max_dim);
let height = image.height().min(max_dim);
eprintln!(
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
image.dimensions(),
(width, height)
);
image = image.view(0, 0, width, height).to_image();
}
RenderedText {
handle: textures.add(image),
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
size: Vec2::new(max_width, height),
}
}
}
#[derive(Clone)]
pub struct RenderedText {
pub handle: TextureHandle,
pub top_left_offset: Vec2,
pub size: Vec2,
}
-232
View File
@@ -1,232 +0,0 @@
use image::DynamicImage;
use crate::{
core::{TextEdit, TextEditCtx},
layout::{
Event, EventFn, EventModule, IdLike, PainterData, PixelRegion, TextureHandle, Vec2, Widget,
WidgetId, WidgetInstance, WidgetLike,
},
util::{HashSet, Id},
};
use std::{
any::{Any, TypeId},
ops::{Index, IndexMut},
sync::mpsc::{Receiver, Sender, channel},
};
pub struct Ui {
// TODO: make this at least pub(super)
pub(crate) data: PainterData,
root: Option<WidgetId>,
updates: HashSet<Id>,
recv: Receiver<Id>,
pub(super) send: Sender<Id>,
full_redraw: bool,
resized: bool,
}
impl Ui {
pub fn add<W: Widget, Tag>(&mut self, w: impl WidgetLike<Tag, Widget = W>) -> WidgetId<W> {
w.add(self)
}
/// 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.new_id();
self.data.widgets.insert(id.id, w);
id
}
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)
}
fn new_id<W: Widget>(&mut self) -> WidgetId<W> {
WidgetId::new(
self.data.widgets.reserve(),
TypeId::of::<W>(),
self.send.clone(),
)
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.data.textures.add(image)
}
pub fn register_event<W, E: Event, Ctx: 'static>(
&mut self,
id: &WidgetId<W>,
event: E,
f: impl EventFn<Ctx, E::Data>,
) {
self.data
.modules
.get_mut::<E::Module<Ctx>>()
.register(id.id, event, f);
}
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();
if let Some(root) = &self.root {
self.data.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) {
self.data.redraw(std::mem::take(&mut self.updates));
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.insert(id.id());
TextEditCtx {
text: self.data.widgets.get_mut(id).unwrap(),
font_system: &mut self.data.text.font_system,
}
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.data.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
}
}
pub fn window_region<W>(&self, id: &impl IdLike<W>) -> Option<PixelRegion> {
let region = self.data.active.get(&id.id())?.region;
Some(region.to_px(self.data.output_size))
}
pub fn debug(&self, label: &str) -> impl Iterator<Item = &WidgetInstance> {
self.data.active.iter().filter_map(move |(id, inst)| {
let l = &self.data.widgets.label(id);
if *l == label { Some(inst) } else { None }
})
}
}
impl<W: Widget> Index<&WidgetId<W>> for Ui {
type Output = W;
fn index(&self, id: &WidgetId<W>) -> &Self::Output {
self.get(id).unwrap()
}
}
impl<W: Widget> IndexMut<&WidgetId<W>> for Ui {
fn index_mut(&mut self, id: &WidgetId<W>) -> &mut Self::Output {
self.updates.insert(id.id);
self.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,
}
}
}
-160
View File
@@ -1,160 +0,0 @@
use crate::{
core::WidgetPtr,
layout::{Len, Painter, SizeCtx, Ui, WidgetId, WidgetIdFn},
};
use std::{any::Any, marker::PhantomData};
pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter);
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
}
impl Widget for () {
fn draw(&mut self, _: &mut Painter) {}
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
}
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
Len::ZERO
}
}
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)
}
}
fn set_root(self, ui: &mut Ui)
where
Self: Sized,
{
ui.set_root(self);
}
fn set_ptr(self, ptr: &WidgetId<WidgetPtr>, ui: &mut Ui)
where
Self: Sized,
{
ui[ptr].inner = Some(self.add(ui).any());
}
}
/// 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 {}
impl<W: Widget, F: FnOnce(&mut Ui) -> W> WidgetLike<FnTag> for F {
type Widget = W;
fn add(self, ui: &mut Ui) -> WidgetId<W> {
self(ui).add(ui)
}
}
impl<W: Widget> WidgetLike<WidgetTag> for W {
type Widget = W;
fn add(self, ui: &mut Ui) -> WidgetId<W> {
ui.add_widget(self)
}
}
pub struct WidgetArr<const LEN: usize, Ws> {
pub arr: [WidgetId; LEN],
_pd: PhantomData<Ws>,
}
impl<const LEN: usize, Ws> WidgetArr<LEN, Ws> {
pub fn new(arr: [WidgetId; LEN]) -> Self {
Self {
arr,
_pd: PhantomData,
}
}
}
pub struct ArrTag;
pub trait WidgetArrLike<const LEN: usize, Tag> {
type Ws;
fn ui(self, ui: &mut Ui) -> WidgetArr<LEN, Self::Ws>;
}
impl<const LEN: usize, Ws> WidgetArrLike<LEN, ArrTag> for WidgetArr<LEN, Ws> {
type Ws = 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;$($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 ($($W,)*) = self;
WidgetArr::new(
[$($W.add(ui).cast_type(),)*],
)
}
}
};
}
impl_widget_arr!(1;A);
impl_widget_arr!(2;A B);
impl_widget_arr!(3;A B C);
impl_widget_arr!(4;A B C D);
impl_widget_arr!(5;A B C D E);
impl_widget_arr!(6;A B C D E F);
impl_widget_arr!(7;A B C D E F G);
impl_widget_arr!(8;A B C D E F G H);
impl_widget_arr!(9;A B C D E F G H I);
impl_widget_arr!(10;A B C D E F G H I J);
impl_widget_arr!(11;A B C D E F G H I J K);
impl_widget_arr!(12;A B C D E F G H I J K L);
pub trait WidgetOption {
fn get(self, ui: &mut Ui) -> Option<WidgetId>;
}
impl WidgetOption for () {
fn get(self, _: &mut Ui) -> Option<WidgetId> {
None
}
}
impl<F: FnOnce(&mut Ui) -> Option<WidgetId>> WidgetOption for F {
fn get(self, ui: &mut Ui) -> Option<WidgetId> {
self(ui)
}
}
-107
View File
@@ -1,107 +0,0 @@
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) {
let mut label = std::any::type_name::<W>().to_string();
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) {
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1;
}
self.insert_any(id, Box::new(widget), label);
}
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>;
+18 -17
View File
@@ -1,24 +1,25 @@
#![feature(macro_metavar_expr_concat)]
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(unboxed_closures)]
#![feature(fn_traits)]
#![feature(const_cmp)]
#![feature(const_destruct)]
#![feature(portable_simd)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)]
#![feature(unsize)]
#![feature(option_into_flat_iter)]
#![feature(async_fn_traits)]
pub mod core;
pub mod layout;
pub mod render;
pub mod util;
pub mod winit;
pub mod default;
pub mod event;
pub mod widget;
pub use iris_core as core;
pub use iris_macro as macros;
pub mod prelude {
pub use crate::core::*;
pub use crate::layout::*;
pub use crate::render::*;
use super::*;
pub use default::*;
pub use event::*;
pub use iris_core::*;
pub use iris_macro::*;
pub use widget::*;
pub use iris_core::util::Vec2;
pub use len_fns::*;
}
-16
View File
@@ -1,16 +0,0 @@
mod arena;
mod borrow;
mod change;
mod id;
mod math;
mod refcount;
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>;
File renamed without changes.
+3 -3
View File
@@ -19,10 +19,10 @@ impl Widget for Image {
}
}
pub fn image(image: impl LoadableImage) -> impl WidgetFn<Image> {
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
let image = image.get_image().expect("Failed to load image");
move |ui| Image {
handle: ui.add_texture(image),
move |state| Image {
handle: state.ui_mut().textures.add(image),
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Masked {
pub inner: WidgetId,
pub inner: StrongWidget,
}
impl Widget for Masked {
-2
View File
@@ -3,7 +3,6 @@ mod mask;
mod position;
mod ptr;
mod rect;
mod sense;
mod text;
mod trait_fns;
@@ -12,6 +11,5 @@ pub use mask::*;
pub use position::*;
pub use ptr::*;
pub use rect::*;
pub use sense::*;
pub use text::*;
pub use trait_fns::*;
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Aligned {
pub inner: WidgetId,
pub inner: StrongWidget,
pub align: Align,
}
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct LayerOffset {
pub inner: WidgetId,
pub inner: StrongWidget,
pub offset: usize,
}
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct MaxSize {
pub inner: WidgetId,
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
File renamed without changes.
@@ -1,7 +1,7 @@
use crate::prelude::*;
pub struct Offset {
pub inner: WidgetId,
pub inner: StrongWidget,
pub amt: UiVec2,
}
@@ -2,7 +2,7 @@ use crate::prelude::*;
pub struct Pad {
pub padding: Padding,
pub inner: WidgetId,
pub inner: StrongWidget,
}
impl Widget for Pad {
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more