Compare commits

..
Author SHA1 Message Date
iris 1e4a7f8cf5 naw 2025-12-29 20:11:35 -05:00
iris 93291badc1 (BROKEN) start on removing desired size 2025-12-23 23:48:35 -05:00
73 changed files with 2291 additions and 3428 deletions

No files matched your search

Generated
+466 -743
View File
File diff suppressed because it is too large. Load diff
+6 -9
View File
@@ -1,5 +1,6 @@
[package] [package]
name = "iris" name = "iris"
default-run = "test"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
@@ -8,16 +9,13 @@ edition.workspace = true
[dependencies] [dependencies]
iris-core = { workspace = true } iris-core = { workspace = true }
iris-macro = { workspace = true } iris-macro = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
unicode-segmentation = { workspace = true }
winit = { workspace = true } winit = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] } arboard = { workspace = true, features = ["wayland-data-control"] }
pollster = { workspace = true } pollster = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
image = { 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] [workspace]
members = ["core", "macro"] members = ["core", "macro"]
@@ -29,13 +27,12 @@ edition = "2024"
[workspace.dependencies] [workspace.dependencies]
pollster = "0.4.0" pollster = "0.4.0"
winit = "0.30.12" winit = "0.30.12"
wgpu = "28.0.0" wgpu = "27.0.1"
bytemuck = "1.23.1" bytemuck = "1.23.1"
image = "0.25.6" image = "0.25.6"
parley = "0.11.1" cosmic-text = "0.15.0"
swash = "0.2.10" unicode-segmentation = "1.12.0"
fxhash = "0.2.1" fxhash = "0.2.1"
arboard = "3.6.1" arboard = "3.6.1"
iris-core = { path = "core" } iris-core = { path = "core" }
iris-macro = { path = "macro" } iris-macro = { path = "macro" }
tokio = "1.49.0"
+2 -2
View File
@@ -4,9 +4,9 @@ version.workspace = true
edition.workspace = true edition.workspace = true
[dependencies] [dependencies]
winit = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
image = { workspace = true } image = { workspace = true }
parley = { workspace = true } cosmic-text = { workspace = true }
swash = { workspace = true }
fxhash = { workspace = true } fxhash = { workspace = true }
+13 -11
View File
@@ -1,22 +1,24 @@
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike}; use crate::{HasUi, StateLike, WidgetIdFn, WidgetLike, WidgetRef};
pub trait WidgetAttr<Rsc, W: ?Sized> { pub trait WidgetAttr<State, W: ?Sized> {
type Input; type Input;
fn run(rsc: &mut Rsc, id: WeakWidget<W>, input: Self::Input); fn run(state: &mut State, id: WidgetRef<W>, input: Self::Input);
} }
pub trait Attrable<Rsc, W: ?Sized, Tag> { pub trait Attrable<State, W: ?Sized, Tag> {
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>; fn attr<A: WidgetAttr<State, W>>(self, input: A::Input) -> impl WidgetIdFn<State, W>;
} }
impl<Rsc: UiRsc, WL: WidgetLike<Rsc, Tag>, Tag> Attrable<Rsc, WL::Widget, Tag> for WL { impl<State: HasUi + StateLike<State>, WL: WidgetLike<State, Tag>, Tag>
fn attr<A: WidgetAttr<Rsc, WL::Widget>>( Attrable<State, WL::Widget, Tag> for WL
{
fn attr<A: WidgetAttr<State, WL::Widget>>(
self, self,
input: A::Input, input: A::Input,
) -> impl WidgetIdFn<Rsc, WL::Widget> { ) -> impl WidgetIdFn<State, WL::Widget> {
|rsc| { |state| {
let id = self.add(rsc); let id = self.add(state);
A::run(rsc, id, input); A::run(state, id, input);
id id
} }
} }
+109 -11
View File
@@ -1,18 +1,116 @@
use crate::{HasEvents, WeakWidget, Widget}; use crate::{HasEvents, HasState, HasUi, StateLike, Ui, Widget, WidgetRef};
use std::ops::{Deref, DerefMut};
pub struct EventCtx<'a, Rsc: HasEvents, Data> { pub struct EventCtx<'a, State, Data> {
pub state: &'a mut Rsc::State, pub state: &'a mut State,
pub data: Data, pub data: &'a mut Data,
} }
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> { pub struct EventIdCtx<'a, State, Data, W: ?Sized> {
pub widget: WeakWidget<W>, pub widget: WidgetRef<W>,
pub state: &'a mut Rsc::State, pub state: &'a mut State,
pub data: Data, pub data: &'a mut Data,
} }
impl<Rsc: HasEvents, Data, W: Widget> EventIdCtx<'_, Rsc, Data, W> { impl<State: HasUi, Data, W: ?Sized> Deref for EventIdCtx<'_, State, Data, W> {
pub fn widget<'a>(&self, rsc: &'a mut Rsc) -> &'a mut W { type Target = State;
&mut rsc.ui_mut().widgets[self.widget]
fn deref(&self) -> &Self::Target {
self.state
} }
} }
impl<State: HasUi, Data, W: ?Sized> DerefMut for EventIdCtx<'_, State, Data, W> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.state
}
}
impl<State: HasUi, Data, W: Widget> EventIdCtx<'_, State, Data, W> {
pub fn widget(&mut self) -> &mut W {
&mut self.state.get_mut()[self.widget]
}
}
impl<State: HasUi, Data, W: Widget> HasUi for EventIdCtx<'_, State, Data, W> {
fn get(&self) -> &Ui {
self.state.ui()
}
fn get_mut(&mut self) -> &mut Ui {
self.state.ui_mut()
}
}
impl<State: HasUi, Data, W: Widget> HasState for EventIdCtx<'_, State, Data, W> {
type State = State;
}
impl<State: HasEvents<State = State>, Data, W: Widget> HasEvents
for EventIdCtx<'_, State, Data, W>
{
fn get(&self) -> &super::EventManager<Self::State> {
self.state.events()
}
fn get_mut(&mut self) -> &mut super::EventManager<Self::State> {
self.state.events_mut()
}
}
impl<State, Data, W: Widget> StateLike<State> for EventIdCtx<'_, State, Data, W> {
fn as_state(&mut self) -> &mut State {
self.state
}
}
// fn test() {
// use crate::*;
// struct ClientRsc;
// impl<State, Data> HasUi for EventCtx<'_, State, Data> {
// fn get(&self) -> &Ui {
// todo!()
// }
//
// fn get_mut(&mut self) -> &mut Ui {
// todo!()
// }
// }
// fn on(_: impl for<'a> EventFn<ClientRsc, &'a mut i32>) {}
//
// pub trait WidgetLike<State: HasUi, Tag>: Sized {
// type Widget: Widget + ?Sized + std::marker::Unsize<dyn Widget>;
//
// fn add(self, state: &mut State) -> WidgetHandle<Self::Widget>;
//
// fn with_id<W2>(
// self,
// f: impl FnOnce(&mut State, WidgetHandle<Self::Widget>) -> WidgetHandle<W2>,
// ) -> impl WidgetIdFn<State, W2> {
// move |state| {
// let id = self.add(state);
// f(state, id)
// }
// }
//
// fn set_root(self, state: &mut State) {
// state.get_mut().root = Some(self.add(state));
// }
//
// fn handles(self, state: &mut State) -> WidgetHandles<Self::Widget> {
// self.add(state).handles()
// }
// }
//
// pub struct WidgetTag;
// impl<State: HasUi, W: Widget> WidgetLike<State, WidgetTag> for W {
// type Widget = W;
// fn add(self, state: &mut State) -> WidgetHandle<W> {
// state.get_mut().add_widget(self)
// }
// }
//
// on(move |ctx| {
// ().add(ctx);
// });
// }
+33 -39
View File
@@ -1,16 +1,16 @@
use crate::{ use crate::{
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId, ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, IdLike, LayerId, Widget,
WeakWidget, WidgetEventFn, WidgetId, WidgetEventFn, WidgetId, WidgetRef,
util::{HashMap, HashSet, TypeMap}, util::{HashMap, HashSet, TypeMap},
}; };
use std::{any::TypeId, rc::Rc}; use std::{any::TypeId, rc::Rc};
pub struct EventManager<Rsc> { pub struct EventManager<State> {
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>, widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
types: TypeMap<dyn EventManagerLike<Rsc>>, types: TypeMap<dyn EventManagerLike<State>>,
} }
impl<Rsc> Default for EventManager<Rsc> { impl<State> Default for EventManager<State> {
fn default() -> Self { fn default() -> Self {
Self { Self {
widget_to_types: Default::default(), widget_to_types: Default::default(),
@@ -19,27 +19,26 @@ impl<Rsc> Default for EventManager<Rsc> {
} }
} }
impl<Rsc: HasEvents + 'static> EventManager<Rsc> { impl<State: 'static> EventManager<State> {
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<Rsc, E::Event> { pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<State, E::Event> {
self.types.type_or_default() self.types.type_or_default()
} }
pub fn register<I: IdLike + 'static, E: EventLike>( pub fn register<W: Widget + ?Sized, E: EventLike>(
&mut self, &mut self,
id: I, id: WidgetRef<W>,
event: E, event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, I::Widget>, f: impl for<'a> WidgetEventFn<State, <E::Event as Event>::Data<'a>, W>,
) { ) {
let i = id.id();
self.get_type::<E>().register(id, event, f); self.get_type::<E>().register(id, event, f);
self.widget_to_types self.widget_to_types
.entry(i) .entry(id.id())
.or_default() .or_default()
.insert(Self::type_key::<E>()); .insert(Self::type_key::<E>());
} }
pub fn type_key<E: EventLike>() -> TypeId { pub fn type_key<E: EventLike>() -> TypeId {
TypeId::of::<TypeEventManager<Rsc, E::Event>>() TypeId::of::<TypeEventManager<State, E::Event>>()
} }
} }
@@ -49,7 +48,7 @@ pub trait EventsLike {
fn undraw(&mut self, active: &ActiveData); fn undraw(&mut self, active: &ActiveData);
} }
impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> { impl<State: 'static> EventsLike for EventManager<State> {
fn remove(&mut self, id: WidgetId) { fn remove(&mut self, id: WidgetId) {
for t in self.widget_to_types.get(&id).into_flat_iter() { for t in self.widget_to_types.get(&id).into_flat_iter() {
self.types.get_mut(t).unwrap().remove(id); self.types.get_mut(t).unwrap().remove(id);
@@ -75,14 +74,14 @@ pub trait EventManagerLike<State> {
fn undraw(&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>>>); type EventData<State, E> = (E, Rc<dyn for<'a> EventFn<State, <E as Event>::Data<'a>>>);
pub struct TypeEventManager<Rsc: HasEvents, E: Event> { pub struct TypeEventManager<State, E: Event> {
// TODO: reduce visiblity!! // TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>, pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>, map: HashMap<WidgetId, Vec<EventData<State, E>>>,
} }
impl<Rsc: HasEvents, E: Event> EventManagerLike<Rsc> for TypeEventManager<Rsc, E> { impl<State, E: Event> EventManagerLike<State> for TypeEventManager<State, E> {
fn remove(&mut self, id: WidgetId) { fn remove(&mut self, id: WidgetId) {
self.map.remove(&id); self.map.remove(&id);
for layer in self.active.values_mut() { for layer in self.active.values_mut() {
@@ -103,7 +102,7 @@ impl<Rsc: HasEvents, E: Event> EventManagerLike<Rsc> for TypeEventManager<Rsc, E
} }
} }
impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> { impl<State, E: Event> Default for TypeEventManager<State, E> {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: Default::default(), active: Default::default(),
@@ -112,25 +111,23 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
} }
} }
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> { impl<State: 'static, E: Event> TypeEventManager<State, E> {
fn register<I: IdLike + 'static>( fn register<W: Widget + ?Sized>(
&mut self, &mut self,
widget: I, widget: WidgetRef<W>,
event: impl EventLike<Event = E>, event: impl EventLike<Event = E>,
f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, I::Widget>, f: impl for<'a> WidgetEventFn<State, E::Data<'a>, W>,
) { ) {
let event = event.into_event(); let event = event.into_event();
self.map.entry(widget.id()).or_default().push(( self.map.entry(widget.id()).or_default().push((
event, event,
Rc::new(move |ctx, rsc| { Rc::new(move |ctx| {
f( let mut test = EventIdCtx {
EventIdCtx { widget,
widget: WeakWidget::new(widget.id()),
state: ctx.state, state: ctx.state,
data: ctx.data, data: ctx.data,
}, };
rsc, f(&mut test);
);
}), }),
)); ));
} }
@@ -138,18 +135,15 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
pub fn run_fn<'a>( pub fn run_fn<'a>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a { ) -> impl for<'b> FnOnce(EventCtx<'_, State, E::Data<'b>>) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default(); let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| { move |ctx| {
for (e, f) in fs { for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) { if e.should_run(ctx.data) {
f( f(&mut EventCtx {
EventCtx {
state: ctx.state, state: ctx.state,
data, data: ctx.data,
}, })
rsc,
)
} }
} }
} }
+9 -12
View File
@@ -7,11 +7,11 @@ pub use manager::*;
pub use rsc::*; pub use rsc::*;
pub trait Event: Sized + 'static + Clone { pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = (); type Data<'a> = ();
type State: Default = (); type State: Default = ();
#[allow(unused_variables)] #[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
Some(data.clone()) true
} }
} }
@@ -28,17 +28,14 @@ impl<E: Event> EventLike for E {
} }
} }
pub trait EventFn<Rsc: HasEvents, Data>: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static {} pub trait EventFn<State, Data>: Fn(&mut EventCtx<State, Data>) + 'static {}
impl<Rsc: HasEvents, F: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static, Data> EventFn<Rsc, Data> impl<State, F: Fn(&mut EventCtx<State, Data>) + 'static, Data> EventFn<State, Data> for F {}
for F
{
}
pub trait WidgetEventFn<Rsc: HasEvents, Data, W: ?Sized>: pub trait WidgetEventFn<State, Data, W: ?Sized>:
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static Fn(&mut EventIdCtx<State, Data, W>) + 'static
{ {
} }
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized> impl<State, F: Fn(&mut EventIdCtx<State, Data, W>) + 'static, Data, W: ?Sized>
WidgetEventFn<Rsc, Data, W> for F WidgetEventFn<State, Data, W> for F
{ {
} }
+21 -14
View File
@@ -1,34 +1,41 @@
use crate::{ use crate::{
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn, Event, EventCtx, EventLike, EventManager, HasUi, IdLike, Widget, WidgetEventFn, WidgetRef,
}; };
pub trait HasState: 'static { pub trait HasState {
type State; type State: HasUi;
} }
pub trait HasEvents: Sized + UiRsc + HasState { pub trait HasEvents: Sized + HasState + HasUi {
fn events(&self) -> &EventManager<Self>; fn get(&self) -> &EventManager<Self::State>;
fn events_mut(&mut self) -> &mut EventManager<Self>; fn get_mut(&mut self) -> &mut EventManager<Self::State>;
fn events(&self) -> &EventManager<Self::State> {
HasEvents::get(self)
}
fn events_mut(&mut self) -> &mut EventManager<Self::State> {
HasEvents::get_mut(self)
}
fn register_event<W: Widget + ?Sized, E: EventLike>( fn register_event<W: Widget + ?Sized, E: EventLike>(
&mut self, &mut self,
id: WeakWidget<W>, id: WidgetRef<W>,
event: E, event: E,
f: impl for<'a> WidgetEventFn<Self, <E::Event as Event>::Data<'a>, W>, f: impl for<'a> WidgetEventFn<Self::State, <E::Event as Event>::Data<'a>, W>,
) { ) where
Self::State: 'static,
{
self.events_mut().register(id, event, f); self.events_mut().register(id, event, f);
} }
} }
pub trait RunEvents: HasEvents { pub trait RunEvents: HasEvents + HasState<State = Self> + 'static {
fn run_event<E: EventLike>( fn run_event<E: EventLike>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
data: <E::Event as Event>::Data<'_>, data: &mut <E::Event as Event>::Data<'_>,
state: &mut Self::State,
) { ) {
let f = self.events_mut().get_type::<E>().run_fn(id); let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self) f(EventCtx { state: self, data })
} }
} }
impl<T: HasEvents> RunEvents for T {} impl<T: HasEvents + HasState<State = Self> + 'static> RunEvents for T {}
+3
View File
@@ -2,9 +2,12 @@
#![feature(const_ops)] #![feature(const_ops)]
#![feature(const_trait_impl)] #![feature(const_trait_impl)]
#![feature(const_convert)] #![feature(const_convert)]
#![feature(map_try_insert)]
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_cmp)]
#![feature(const_destruct)] #![feature(const_destruct)]
#![feature(portable_simd)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
+5 -5
View File
@@ -5,19 +5,19 @@ pub const trait UiNum {
fn to_f32(self) -> f32; fn to_f32(self) -> f32;
} }
const impl UiNum for f32 { impl const UiNum for f32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self self
} }
} }
const impl UiNum for u32 { impl const UiNum for u32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self as f32 self as f32
} }
} }
const impl UiNum for i32 { impl const UiNum for i32 {
fn to_f32(self) -> f32 { fn to_f32(self) -> f32 {
self as f32 self as f32
} }
@@ -27,7 +27,7 @@ pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
Vec2::new(x.to_f32(), y.to_f32()) Vec2::new(x.to_f32(), y.to_f32())
} }
const impl<T: const UiNum + Copy> From<T> for Vec2 { impl<T: const UiNum + Copy> const From<T> for Vec2 {
fn from(v: T) -> Self { fn from(v: T) -> Self {
Self { Self {
x: v.to_f32(), x: v.to_f32(),
@@ -36,7 +36,7 @@ const impl<T: const UiNum + Copy> From<T> for Vec2 {
} }
} }
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2 impl<T: const UiNum, U: const UiNum> const From<(T, U)> for Vec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
+17 -3
View File
@@ -2,7 +2,7 @@ use crate::vec2;
use super::*; use super::*;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Align { pub struct Align {
pub x: Option<AxisAlign>, pub x: Option<AxisAlign>,
pub y: Option<AxisAlign>, pub y: Option<AxisAlign>,
@@ -24,13 +24,26 @@ impl Align {
pub const TOP: CardinalAlign = CardinalAlign::TOP; pub const TOP: CardinalAlign = CardinalAlign::TOP;
pub const V_CENTER: CardinalAlign = CardinalAlign::V_CENTER; pub const V_CENTER: CardinalAlign = CardinalAlign::V_CENTER;
pub const BOT: CardinalAlign = CardinalAlign::BOT; pub const BOT: CardinalAlign = CardinalAlign::BOT;
pub const NONE: Align = Align { x: None, y: None };
pub fn tuple(&self) -> (Option<AxisAlign>, Option<AxisAlign>) { pub fn tuple(&self) -> (Option<AxisAlign>, Option<AxisAlign>) {
(self.x, self.y) (self.x, self.y)
} }
/// naming is a bit inconsistent w option,
/// normally would return Self, but can't see
/// that being needed atm and would wanna do
/// a trait if so, so they can both be named .or
/// (because Self impls From<RegionAlign>)
pub fn or(&self, other: RegionAlign) -> RegionAlign {
RegionAlign {
x: self.x.unwrap_or(other.x),
y: self.y.unwrap_or(other.x),
}
}
} }
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AxisAlign { pub enum AxisAlign {
Neg, Neg,
Center, Center,
@@ -106,6 +119,7 @@ impl UiVec2 {
} }
} }
/// aligns this as a size within a region
pub fn align(&self, align: RegionAlign) -> UiRegion { pub fn align(&self, align: RegionAlign) -> UiRegion {
UiRegion { UiRegion {
x: self.x.align(align.x), x: self.x.align(align.x),
@@ -187,7 +201,7 @@ impl From<CardinalAlign> for Align {
} }
} }
const impl From<RegionAlign> for UiVec2 { impl const From<RegionAlign> for UiVec2 {
fn from(align: RegionAlign) -> Self { fn from(align: RegionAlign) -> Self {
Self::rel(align.rel()) Self::rel(align.rel())
} }
+2 -2
View File
@@ -74,14 +74,14 @@ pub const trait AxisT {
} }
pub struct XAxis; pub struct XAxis;
const impl AxisT for XAxis { impl const AxisT for XAxis {
fn get() -> Axis { fn get() -> Axis {
Axis::X Axis::X
} }
} }
pub struct YAxis; pub struct YAxis;
const impl AxisT for YAxis { impl const AxisT for YAxis {
fn get() -> Axis { fn get() -> Axis {
Axis::Y Axis::Y
} }
+13 -3
View File
@@ -124,13 +124,13 @@ impl Display for UiVec2 {
impl_op!(UiVec2 Add add; x y); impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 { impl const From<Vec2> for UiVec2 {
fn from(abs: Vec2) -> Self { fn from(abs: Vec2) -> Self {
Self::abs(abs) Self::abs(abs)
} }
} }
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2 impl<T: const UiNum, U: const UiNum> const From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
@@ -140,12 +140,22 @@ where
} }
#[repr(C)] #[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct UiScalar { pub struct UiScalar {
pub rel: f32, pub rel: f32,
pub abs: f32, pub abs: f32,
} }
// TODO: unknown exactly what these should be
const REL_EPSILON: f32 = 0.00001;
const ABS_EPSILON: f32 = 0.1;
impl PartialEq for UiScalar {
fn eq(&self, other: &Self) -> bool {
(self.rel - other.rel).abs() < REL_EPSILON && (self.abs - other.abs).abs() < ABS_EPSILON
}
}
impl Eq for UiScalar {} impl Eq for UiScalar {}
impl Hash for UiScalar { impl Hash for UiScalar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+2 -9
View File
@@ -10,13 +10,6 @@ pub struct Color<T> {
pub a: T, pub a: T,
} }
/// Defaults to visible, unstyled text for Parley's brush requirement.
impl<T: ColorNum> Default for Color<T> {
fn default() -> Self {
Self::BLACK
}
}
impl<T: ColorNum> Color<T> { impl<T: ColorNum> Color<T> {
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN); pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX); pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
@@ -151,7 +144,7 @@ impl ColorNum for f32 {
unsafe impl bytemuck::Pod for Color<u8> {} unsafe impl bytemuck::Pod for Color<u8> {}
const impl F32Conversion for f32 { impl const F32Conversion for f32 {
fn to(self) -> f32 { fn to(self) -> f32 {
self self
} }
@@ -160,7 +153,7 @@ const impl F32Conversion for f32 {
} }
} }
const impl F32Conversion for u8 { impl const F32Conversion for u8 {
fn to(self) -> f32 { fn to(self) -> f32 {
self as f32 / 255.0 self as f32 / 255.0
} }
+141 -210
View File
@@ -1,66 +1,60 @@
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2}; use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
use parley::{ use cosmic_text::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, SwashContent,
};
use swash::{
FontRef,
scale::{Render, ScaleContext, Source, StrikeWith},
zeno::{Format, Vector},
}; };
use image::{GenericImageView, RgbaImage};
use std::simd::{Simd, num::SimdUint};
/// TODO: properly wrap this
pub mod text_lib {
pub use cosmic_text::*;
}
/// Shared font, layout, rasterization, and glyph-atlas state.
pub struct TextData { pub struct TextData {
pub font_cx: FontContext, pub font_system: FontSystem,
pub layout_cx: LayoutContext<UiColor>, pub swash_cache: SwashCache,
scale_cx: ScaleContext, glyph_cache: Vec<(Placement, CacheKey, Color)>,
pub atlas: GlyphAtlas,
} }
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
font_cx: FontContext::new(), font_system: FontSystem::new(),
layout_cx: LayoutContext::new(), swash_cache: SwashCache::new(),
scale_cx: ScaleContext::new(), glyph_cache: Default::default(),
atlas: GlyphAtlas::default(),
} }
} }
} }
/// An owned font family that can be stored by a widget. #[derive(Clone, Copy)]
#[derive(Clone, PartialEq)]
pub enum Family {
SansSerif,
Serif,
Monospace,
Named(String),
}
impl Family {
fn family(&self) -> FontFamily<'_> {
let name = match self {
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
};
FontFamily::Single(name)
}
}
#[derive(Clone, PartialEq)]
pub struct TextAttrs { pub struct TextAttrs {
pub color: UiColor, pub color: UiColor,
pub font_size: f32, pub font_size: f32,
pub line_height: f32, pub line_height: f32,
pub family: Family, pub family: Family<'static>,
pub wrap: bool, pub wrap: bool,
/// Alignment within the text's region. /// inner alignment of text region (within where it's drawn)
pub align: RegionAlign, pub align: RegionAlign,
} }
pub const LINE_HEIGHT_MULT: f32 = 1.1; impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics_and_size(
font_system,
Metrics::new(self.font_size, self.line_height),
width,
None,
);
let attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs);
for line in &mut buf.lines {
line.set_attrs_list(list.clone());
}
}
}
pub type TextBuffer = Buffer;
impl Default for TextAttrs { impl Default for TextAttrs {
fn default() -> Self { fn default() -> Self {
@@ -76,181 +70,118 @@ impl Default for TextAttrs {
} }
} }
/// Keeps text and its corresponding layout from getting out of sync. pub const LINE_HEIGHT_MULT: f32 = 1.1;
pub struct TextBuffer {
text: String,
layout: Layout<UiColor>,
/// The inputs used to build the cached layout.
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 { impl TextData {
/// Rasterizes uncached glyphs and places them relative to the text origin. pub fn draw(
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
}
/// Cached glyph placement for a laid-out string.
#[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, &mut self,
buffer: &mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>,
textures: &mut Textures, textures: &mut Textures,
) -> RenderedText { ) -> RenderedText {
buffer.shape(self, attrs, width); // TODO: either this or the layout stuff (or both) is super slow,
let glyphs = self.place(buffer, textures); // 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 { RenderedText {
glyphs: std::sync::Arc::new(glyphs), handle: textures.add(image),
size: buffer.size(), top_left_offset: Vec2::new(min_x as f32, min_y as f32),
color: attrs.color, size: Vec2::new(max_width, height),
} }
} }
} }
#[derive(Clone)]
pub struct RenderedText {
pub handle: TextureHandle,
pub top_left_offset: Vec2,
pub size: Vec2,
}
-28
View File
@@ -29,25 +29,14 @@ pub struct Textures {
pub enum TextureUpdate<'a> { pub enum TextureUpdate<'a> {
Push(&'a DynamicImage), Push(&'a DynamicImage),
Set(u32, &'a DynamicImage), Set(u32, &'a DynamicImage),
/// Overwrite one rectangle without uploading the entire texture.
Patch(u32, PatchRect, &'a DynamicImage),
Free(u32), Free(u32),
PushFree, PushFree,
SetFree, SetFree,
} }
#[derive(Debug, Clone, Copy)]
pub struct PatchRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
enum Update { enum Update {
Push(u32), Push(u32),
Set(u32), Set(u32),
Patch(u32, PatchRect),
Free(u32), Free(u32),
} }
@@ -92,19 +81,6 @@ impl Textures {
} }
} }
/// The stored image for a handle, to be written into before `patch`.
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) { pub fn free(&mut self) {
for idx in self.recv.try_iter() { for idx in self.recv.try_iter() {
self.images[idx as usize] = None; self.images[idx as usize] = None;
@@ -123,10 +99,6 @@ impl Textures {
.as_ref() .as_ref()
.map(|img| TextureUpdate::Set(i, img)) .map(|img| TextureUpdate::Set(i, img))
.unwrap_or(TextureUpdate::SetFree), .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), Update::Free(i) => TextureUpdate::Free(i),
}) })
} }
-210
View File
@@ -1,210 +0,0 @@
//! Packs reusable rasterized glyphs into shared texture pages.
use crate::{
PatchRect, TextureHandle, Textures,
util::{HashMap, Vec2},
};
use image::RgbaImage;
use swash::scale::image::{Content, Image};
/// A 1024-pixel RGBA8 page occupies 4 MiB.
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;
/// Includes every input that can change the rasterized pixels.
#[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,
/// Shelf packing is effective because glyphs at one size have similar heights.
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()
}
/// Returns `None` when a glyph has no pixels or cannot fit on a page.
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 {
// Never silently crop an oversized glyph.
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)
}
/// Record that a glyph has no pixels, so it is not re-rasterised.
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 {
// On the current shelf, or on a new one above it.
(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]));
}
}
}
}
}
/// Where a glyph goes on screen, in pixels relative to the text's origin.
#[derive(Clone, Copy)]
pub struct PlacedGlyph {
pub entry: GlyphEntry,
pub offset: Vec2,
}
+12 -23
View File
@@ -1,23 +1,22 @@
use std::num::NonZero; use std::num::NonZero;
use crate::{ use crate::{
UiData, UiRenderState, Ui,
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
util::{HashMap, Vec2}, util::HashMap,
}; };
use data::WindowUniform; use data::WindowUniform;
use wgpu::{ use wgpu::{
util::{BufferInitDescriptor, DeviceExt}, util::{BufferInitDescriptor, DeviceExt},
*, *,
}; };
use winit::dpi::PhysicalSize;
mod atlas;
mod data; mod data;
mod primitive; mod primitive;
mod texture; mod texture;
mod util; mod util;
pub use atlas::*;
pub use data::{Mask, MaskIdx}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
@@ -60,18 +59,12 @@ impl UiRenderNode {
} }
} }
pub fn update( pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) {
&mut self,
device: &Device,
queue: &Queue,
ui: &mut UiData,
ui_render: &mut UiRenderState,
) {
self.active.clear(); self.active.clear();
for (i, primitives) in ui_render.layers.iter_mut() { for (i, primitives) in ui.layers.iter_mut() {
self.active.push(i); self.active.push(i);
for change in primitives.apply_free() { for change in primitives.apply_free() {
if let Some(inst) = ui_render.active.get_mut(&change.id) { if let Some(inst) = ui.active.get_mut(&change.id) {
for h in &mut inst.primitives { for h in &mut inst.primitives {
if h.layer == i && h.inst_idx == change.old { if h.layer == i && h.inst_idx == change.old {
h.inst_idx = change.new; h.inst_idx = change.new;
@@ -119,11 +112,10 @@ impl UiRenderNode {
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
let size = size.into();
let slice = &[WindowUniform { let slice = &[WindowUniform {
width: size.x, width: size.width as f32,
height: size.y, height: size.height as f32,
}]; }];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
@@ -139,10 +131,7 @@ impl UiRenderNode {
source: ShaderSource::Wgsl(SHAPE_SHADER.into()), source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
}); });
let window_uniform = WindowUniform { let window_uniform = WindowUniform::default();
width: config.width as f32,
height: config.height as f32,
};
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
contents: bytemuck::cast_slice(&[window_uniform]), contents: bytemuck::cast_slice(&[window_uniform]),
@@ -194,7 +183,7 @@ impl UiRenderNode {
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
label: Some("UI Shape Pipeline Layout"), label: Some("UI Shape Pipeline Layout"),
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
immediate_size: 0, push_constant_ranges: &[],
}); });
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("UI Shape Pipeline"), label: Some("UI Shape Pipeline"),
@@ -230,7 +219,7 @@ impl UiRenderNode {
mask: !0, mask: !0,
alpha_to_coverage_enabled: false, alpha_to_coverage_enabled: false,
}, },
multiview_mask: None, multiview: None,
cache: None, cache: None,
}); });
+1 -19
View File
@@ -93,8 +93,7 @@ macro_rules! primitives {
} }
)* )*
}; };
// Preserve the whitespace-separated token shape across recursion. (@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
(@count $t:tt) => { 1 }; (@count $t:tt) => { 1 };
} }
@@ -202,7 +201,6 @@ impl PrimitiveHandle {
primitives!( primitives!(
rects: RectPrimitive => 0, rects: RectPrimitive => 0,
textures: TexturePrimitive => 1, textures: TexturePrimitive => 1,
glyphs: GlyphPrimitive => 2,
); );
#[repr(C)] #[repr(C)]
@@ -232,22 +230,6 @@ pub struct TexturePrimitive {
pub sampler_idx: u32, pub sampler_idx: u32,
} }
/// An atlas subrectangle, tinted for masks or unchanged for color glyphs.
#[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> { pub struct PrimitiveVec<T> {
vec: Vec<T>, vec: Vec<T>,
free: Vec<usize>, free: Vec<usize>,
-26
View File
@@ -1,6 +1,5 @@
const RECT: u32 = 0u; const RECT: u32 = 0u;
const TEXTURE: u32 = 1u; const TEXTURE: u32 = 1u;
const GLYPH: u32 = 2u;
@group(0) @binding(0) @group(0) @binding(0)
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@@ -8,8 +7,6 @@ var<uniform> window: WindowUniform;
var<storage> rects: array<Rect>; var<storage> rects: array<Rect>;
@group(1) @binding(TEXTURE) @group(1) @binding(TEXTURE)
var<storage> textures: array<TextureInfo>; var<storage> textures: array<TextureInfo>;
@group(1) @binding(GLYPH)
var<storage> glyphs: array<GlyphInfo>;
struct Rect { struct Rect {
color: u32, color: u32,
@@ -23,15 +20,6 @@ struct TextureInfo {
sampler_idx: u32, 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 { struct Mask {
x: UiSpan, x: UiSpan,
y: UiSpan, y: UiSpan,
@@ -137,9 +125,6 @@ fn fs_main(
case TEXTURE: { case TEXTURE: {
color = draw_texture(region, textures[i]); color = draw_texture(region, textures[i]);
} }
case GLYPH: {
color = draw_glyph(region, glyphs[i]);
}
default: { default: {
color = vec4(1.0, 0.0, 1.0, 1.0); color = vec4(1.0, 0.0, 1.0, 1.0);
} }
@@ -163,17 +148,6 @@ fn draw_texture(region: Region, info: TextureInfo) -> vec4<f32> {
return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv); 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> { fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
var color = unpack4x8unorm(rect.color); var color = unpack4x8unorm(rect.color);
+13 -75
View File
@@ -1,13 +1,11 @@
use image::{DynamicImage, EncodableLayout, GenericImageView}; use image::{DynamicImage, EncodableLayout};
use wgpu::{util::DeviceExt, *}; use wgpu::{util::DeviceExt, *};
use crate::{PatchRect, TextureUpdate, Textures}; use crate::{TextureUpdate, Textures};
pub struct GpuTextures { pub struct GpuTextures {
device: Device, device: Device,
queue: Queue, queue: Queue,
/// Parallel to `views`; patches require textures rather than views.
textures: Vec<Option<Texture>>,
views: Vec<TextureView>, views: Vec<TextureView>,
view_count: usize, view_count: usize,
samplers: Vec<Sampler>, samplers: Vec<Sampler>,
@@ -19,95 +17,37 @@ impl GpuTextures {
pub fn update(&mut self, textures: &mut Textures) -> bool { pub fn update(&mut self, textures: &mut Textures) -> bool {
let mut changed = false; let mut changed = false;
for update in textures.updates() { for update in textures.updates() {
changed = true;
match update { match update {
TextureUpdate::Push(image) => { TextureUpdate::Push(image) => self.push(image),
self.push(image); TextureUpdate::Set(i, image) => self.set(i, image),
changed = true; TextureUpdate::SetFree => self.view_count += 1,
} TextureUpdate::Free(i) => self.free(i),
TextureUpdate::Set(i, image) => { TextureUpdate::PushFree => self.push_free(),
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 changed
} }
fn set(&mut self, i: u32, image: &DynamicImage) { fn set(&mut self, i: u32, image: &DynamicImage) {
self.view_count += 1; self.view_count += 1;
let (texture, view) = self.create(image); let view = self.create_view(image);
self.textures[i as usize] = Some(texture);
self.views[i as usize] = view; self.views[i as usize] = view;
} }
fn free(&mut self, i: u32) { fn free(&mut self, i: u32) {
self.view_count -= 1; self.view_count -= 1;
self.textures[i as usize] = None;
self.views[i as usize] = self.null_view.clone(); self.views[i as usize] = self.null_view.clone();
} }
fn push(&mut self, image: &DynamicImage) { fn push(&mut self, image: &DynamicImage) {
self.view_count += 1; self.view_count += 1;
let (texture, view) = self.create(image); let view = self.create_view(image);
self.textures.push(Some(texture));
self.views.push(view); self.views.push(view);
} }
fn push_free(&mut self) { fn push_free(&mut self) {
self.view_count += 1; self.view_count += 1;
self.textures.push(None);
self.views.push(self.null_view.clone()); self.views.push(self.null_view.clone());
} }
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { fn create_view(&self, image: &DynamicImage) -> TextureView {
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 image = image.to_rgba8();
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
let texture = self.device.create_texture_with_data( let texture = self.device.create_texture_with_data(
@@ -123,14 +63,13 @@ impl GpuTextures {
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm, format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, usage: TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
}, },
wgt::TextureDataOrder::MipMajor, wgt::TextureDataOrder::MipMajor,
image.as_bytes(), image.as_bytes(),
); );
let view = texture.create_view(&TextureViewDescriptor::default()); texture.create_view(&TextureViewDescriptor::default())
(texture, view)
} }
pub fn new(device: &Device, queue: &Queue) -> Self { pub fn new(device: &Device, queue: &Queue) -> Self {
@@ -138,7 +77,6 @@ impl GpuTextures {
Self { Self {
device: device.clone(), device: device.clone(),
queue: queue.clone(), queue: queue.clone(),
textures: Vec::new(),
views: Vec::new(), views: Vec::new(),
samplers: vec![default_sampler(device)], samplers: vec![default_sampler(device)],
no_views: vec![null_view.clone()], no_views: vec![null_view.clone()],
+3 -1
View File
@@ -1,14 +1,16 @@
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId}; use crate::{Align, LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
/// important non rendering data for retained drawing /// important non rendering data for retained drawing
#[derive(Debug)] #[derive(Debug)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
pub region: UiRegion, pub region: UiRegion,
pub region_used: UiRegion,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
pub align: Align,
} }
-18
View File
@@ -1,18 +0,0 @@
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();
}
}
+209
View File
@@ -0,0 +1,209 @@
use crate::{
ActiveData, Align, Axis, EventsLike, Painter, Ui, UiRegion, WidgetId,
render::MaskIdx,
util::{HashSet, forget_ref},
};
use std::ops::{Deref, DerefMut};
/// state maintained between widgets during painting
pub struct DrawState<'a> {
pub(super) ui: &'a mut Ui,
pub(super) events: &'a mut dyn EventsLike,
draw_started: HashSet<WidgetId>,
}
impl<'a> DrawState<'a> {
pub fn new(ui: &'a mut Ui, events: &'a mut dyn EventsLike) -> Self {
Self {
ui,
events,
draw_started: Default::default(),
}
}
pub fn redraw_updates(&mut self) {
while let Some(&id) = self.widgets.needs_redraw.iter().next() {
self.redraw(id);
}
self.ui.free(self.events);
}
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId) {
self.widgets.needs_redraw.remove(&id);
let Some(active) = self.active.get(&id) else {
return;
};
let ActiveData {
id,
region,
parent,
mask,
layer,
align,
..
} = *active;
self.draw_inner(layer, id, region, parent, mask, align);
}
pub fn redraw_all(&mut self) {
// free all resources & cache
for (_, active) in self.ui.active.drain() {
self.events.undraw(&active);
}
self.ui.free(self.events);
self.layers.clear();
self.widgets.needs_redraw.clear();
if let Some(id) = &self.ui.root {
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, Align::NONE);
}
}
pub(super) fn draw_inner(
&mut self,
layer: usize,
id: WidgetId,
region: UiRegion,
parent: Option<WidgetId>,
mask: MaskIdx,
align: Align,
) {
let mut old_children = Vec::new();
if let Some(active) = self.ui.active.get_mut(&id) {
// check to see if we can skip drawing first, and just need to move
if !self.ui.widgets.needs_redraw.contains(&id) {
if active.region == region {
return;
} else if active.region.size() == region.size() {
self.mov(id, active.region, region);
return;
}
}
let active = self.remove(id, false).unwrap();
old_children = active.children;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
state: self,
region,
region_used: region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
};
let mut widget = painter.state.widgets.get_dyn(id);
widget.draw(&mut painter);
let walign = painter.state.widgets.data(id).unwrap().align;
let align = align.or(walign.into());
let Painter {
region_used,
state: _,
region,
mask,
textures,
primitives,
children,
layer,
id,
} = painter;
// remove old children that weren't kept
for c in &old_children {
if !children.contains(c) {
self.remove_rec(*c);
}
}
// add to active
self.active.insert(
id,
ActiveData {
id,
region,
parent,
textures,
primitives,
children,
region_used,
mask,
layer,
},
);
// TODO: unsure if there's a better way, currently using epsilon
// in PartialEq impl for UiScalar
let target = region_used.size().align(align);
if region_used != target {
self.mov(id, from, to);
}
self.events.draw(self.active.get(&id).unwrap());
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.ui.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.ui.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.layers.free(h);
if mask != MaskIdx::NONE {
self.masks.remove(mask);
}
}
active.textures.clear();
self.textures.free();
if undraw {
self.events.undraw(active);
}
}
active
}
fn remove_rec(&mut self, id: WidgetId) -> Option<ActiveData> {
let inst = self.remove(id, true);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c);
}
}
inst
}
}
impl Deref for DrawState<'_> {
type Target = Ui;
fn deref(&self) -> &Self::Target {
self.ui
}
}
impl DerefMut for DrawState<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ui
}
}
+195 -31
View File
@@ -1,47 +1,211 @@
use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena}; use crate::{
EventsLike, IdLike, Mask, PixelRegion, PrimitiveLayers, TextData, TextureHandle, Textures,
Widget, WidgetHandle, WidgetId, Widgets,
ui::draw_state::DrawState,
util::{HashMap, TrackedArena, Vec2},
};
use image::DynamicImage;
use std::{
ops::{Index, IndexMut},
sync::mpsc::{Receiver, channel},
};
mod active; mod active;
mod cache; mod draw_state;
mod painter; mod painter;
mod render_state; mod state;
mod size;
pub use active::*; pub use active::*;
pub use painter::Painter; pub use painter::Painter;
pub use render_state::*;
pub use size::*;
#[derive(Default)] pub struct Ui {
pub struct UiData { // TODO: edit visibilities
pub widgets: Widgets, pub widgets: Widgets,
// retained painter state
pub active: HashMap<WidgetId, ActiveData>,
pub layers: PrimitiveLayers,
pub textures: Textures, pub textures: Textures,
pub text: TextData, pub text: TextData,
output_size: Vec2,
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
pub root: Option<WidgetHandle>,
old_root: Option<WidgetId>,
recv: Receiver<WidgetId>,
resized: bool,
} }
pub trait UiRsc { pub trait HasUi: Sized {
fn ui(&self) -> &UiData; fn get(&self) -> &Ui;
fn ui_mut(&mut self) -> &mut UiData; fn get_mut(&mut self) -> &mut Ui;
fn ui(&self) -> &Ui {
#[allow(unused_variables)] self.get()
fn on_add(&mut self, id: WeakWidget) {} }
#[allow(unused_variables)] fn ui_mut(&mut self) -> &mut Ui {
fn on_remove(&mut self, id: WidgetId) {} self.get_mut()
#[allow(unused_variables)] }
fn on_draw(&mut self, active: &ActiveData) {} }
#[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {} impl HasUi for Ui {
fn get(&self) -> &Ui {
fn widgets(&self) -> &Widgets { self
&self.ui().widgets }
fn get_mut(&mut self) -> &mut Ui {
self
}
}
impl Ui {
/// useful for debugging
pub fn set_label(&mut self, id: impl IdLike, label: String) {
self.widgets.data_mut(id.id()).unwrap().label = label;
}
pub fn label(&self, id: impl IdLike) -> &String {
&self.widgets.data(id.id()).unwrap().label
}
pub fn new() -> Self {
Self::default()
}
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
where
I::Widget: Sized + Widget,
{
self.widgets.get(id)
}
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
where
I::Widget: Sized + Widget,
{
self.widgets.get_mut(id)
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.textures.add(image)
}
pub fn resize(&mut self, size: impl Into<Vec2>) {
self.output_size = size.into();
self.resized = true;
}
pub fn update(&mut self, events: &mut dyn EventsLike) {
if !self.widgets.waiting.is_empty() {
let len = self.widgets.waiting.len();
let all: Vec<_> = self
.widgets
.waiting
.iter()
.map(|&w| format!("'{}' ({w:?})", self.label(w)))
.collect();
panic!(
"{len} widget(s) were never upgraded\n\
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
weak widgets: {all:#?}"
);
}
if self.root_changed() {
DrawState::new(self, events).redraw_all();
self.old_root = self.root.as_ref().map(|r| r.id());
} else if self.widgets.has_updates() {
DrawState::new(self, events).redraw_updates();
}
if self.resized {
self.resized = false;
DrawState::new(self, events).redraw_all();
}
}
/// free any resources that don't have references anymore
fn free(&mut self, events: &mut dyn EventsLike) {
for id in self.recv.try_iter() {
events.remove(id);
self.widgets.delete(id);
}
self.textures.free();
}
pub fn root_changed(&self) -> bool {
self.root.as_ref().map(|r| r.id()) != self.old_root
}
pub fn needs_redraw(&self) -> bool {
self.root_changed() || self.widgets.has_updates()
}
pub fn num_widgets(&self) -> usize {
self.widgets.len()
}
pub fn active_widgets(&self) -> usize {
self.active.len()
}
pub fn debug_layers(&self) {
for ((idx, depth), primitives) in self.layers.iter_depth() {
let indent = " ".repeat(depth * 2);
let len = primitives.instances().len();
print!("{indent}{idx}: {len} primitives");
if len >= 1 {
print!(" ({})", primitives.instances()[0].binding);
}
println!();
}
}
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let region = self.active.get(&id.id())?.region;
Some(region.to_px(self.output_size))
}
pub fn debug(&self, label: &str) -> impl Iterator<Item = &ActiveData> {
self.active.iter().filter_map(move |(&id, inst)| {
let l = self.widgets.label(id);
if l == label { Some(inst) } else { None }
})
}
}
impl<I: IdLike> Index<I> for Ui
where
I::Widget: Sized + Widget,
{
type Output = I::Widget;
fn index(&self, id: I) -> &Self::Output {
self.get(&id).unwrap()
}
}
impl<I: IdLike> IndexMut<I> for Ui
where
I::Widget: Sized + Widget,
{
fn index_mut(&mut self, id: I) -> &mut Self::Output {
self.get_mut(&id).unwrap()
}
}
impl Default for Ui {
fn default() -> Self {
let (send, recv) = channel();
Self {
widgets: Widgets::new(send),
active: Default::default(),
layers: Default::default(),
masks: Default::default(),
text: Default::default(),
textures: Default::default(),
output_size: Vec2::ZERO,
root: None,
old_root: None,
recv,
resized: false,
} }
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();
} }
} }
+26 -81
View File
@@ -1,15 +1,15 @@
use crate::{ use crate::{
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, RenderedText, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, WidgetHandle, WidgetId,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, ui::draw_state::DrawState,
util::Vec2, util::Vec2,
}; };
/// makes your surfaces look pretty /// makes your surfaces look pretty
pub struct Painter<'a> { pub struct Painter<'a, 'b> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut DrawState<'b>,
pub(super) rsc: &'a mut dyn UiRsc,
pub region_used: UiRegion,
pub(super) region: UiRegion, pub(super) region: UiRegion,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
@@ -19,7 +19,7 @@ pub struct Painter<'a> {
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
impl<'a> Painter<'a> { impl<'a, 'c> Painter<'a, 'c> {
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
@@ -32,7 +32,7 @@ impl<'a> Painter<'a> {
); );
if self.mask != MaskIdx::NONE { if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy: // TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask); self.state.masks.push_ref(self.mask);
} }
self.primitives.push(h); self.primitives.push(h);
} }
@@ -48,31 +48,28 @@ impl<'a> Painter<'a> {
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { region }); self.mask = self.state.masks.push(Mask { region });
} }
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) { pub fn widget<W: ?Sized>(&mut self, id: &WidgetHandle<W>) {
self.widget_at(id, self.region); self.widget_at(id, self.region);
} }
pub fn rest_hint(&self, id: &WidgetHandle) -> f32 {
self.state.widgets.data(id).unwrap().align
}
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas. /// Useful for drawing child widgets in select areas.
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) { pub fn widget_within<W: ?Sized>(&mut self, id: &WidgetHandle<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region)); self.widget_at(id, region.within(&self.region));
} }
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) { fn widget_at<W: ?Sized>(&mut self, id: &WidgetHandle<W>, region: UiRegion) {
self.children.push(id.id()); self.children.push(id.id());
self.state.draw_inner( self.state
self.layer, .draw_inner(self.layer, id.id(), region, Some(self.id), self.mask);
id.id(),
region,
Some(self.id),
self.mask,
None,
self.rsc,
);
} }
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
@@ -85,66 +82,18 @@ impl<'a> Painter<'a> {
self.primitive(handle.primitive()); self.primitive(handle.primitive());
} }
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { /// returns (handle, offset from top left)
self.textures.push(handle.clone()); pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
self.primitive_at(handle.primitive(), region); self.state
} .ui
.text
pub fn render_text( .draw(buffer, attrs, &mut self.state.ui.textures)
&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)
}
/// Draws one atlas-sampling quad per glyph.
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 { pub fn region(&self) -> UiRegion {
self.region 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 { pub fn output_size(&self) -> Vec2 {
self.state.output_size self.state.output_size
} }
@@ -154,7 +103,7 @@ impl<'a> Painter<'a> {
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
&mut self.rsc.ui_mut().text &mut self.state.text
} }
pub fn child_layer(&mut self) { pub fn child_layer(&mut self) {
@@ -166,14 +115,10 @@ impl<'a> Painter<'a> {
} }
pub fn label(&self) -> &str { pub fn label(&self) -> &str {
&self.rsc.widgets().data(self.id).unwrap().label &self.state.widgets.data(self.id).unwrap().label
} }
pub fn id(&self) -> &WidgetId { pub fn id(&self) -> &WidgetId {
&self.id &self.id
} }
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
self.state.size_ctx(self.id, self.region.size(), self.rsc)
}
} }
-324
View File
@@ -1,324 +0,0 @@
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
@@ -1,91 +0,0 @@
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)
}
}
+3
View File
@@ -0,0 +1,3 @@
// pub struct DynState {
//
// }
+8 -9
View File
@@ -9,16 +9,15 @@ pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self; fn div_or(self, rhs: Self, other: Self) -> Self;
} }
const impl DivOr for f32 { impl const DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self { fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs; let res = self / rhs;
if res.is_nan() { other } else { res } if res.is_nan() { other } else { res }
} }
} }
const impl< impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy, LerpUtil for T
> LerpUtil for T
{ {
/// linear interpolation /// linear interpolation
/// from * (1.0 - self) + to * self /// from * (1.0 - self) + to * self
@@ -38,7 +37,7 @@ macro_rules! impl_op {
use super::*; use super::*;
#[allow(unused_imports)] #[allow(unused_imports)]
use std::ops::*; use std::ops::*;
const impl $op for $T { impl const $op for $T {
type Output = Self; type Output = Self;
fn $fn(self, rhs: Self) -> Self::Output { fn $fn(self, rhs: Self) -> Self::Output {
@@ -47,12 +46,12 @@ macro_rules! impl_op {
} }
} }
} }
const impl $opa for $T { impl const $opa for $T {
fn $fna(&mut self, rhs: Self) { fn $fna(&mut self, rhs: Self) {
*self = self.$fn(rhs); *self = self.$fn(rhs);
} }
} }
const impl $op<f32> for $T { impl const $op<f32> for $T {
type Output = Self; type Output = Self;
fn $fn(self, rhs: f32) -> Self::Output { fn $fn(self, rhs: f32) -> Self::Output {
@@ -61,7 +60,7 @@ macro_rules! impl_op {
} }
} }
} }
const impl $op<$T> for f32 { impl const $op<$T> for f32 {
type Output = $T; type Output = $T;
fn $fn(self, rhs: $T) -> Self::Output { fn $fn(self, rhs: $T) -> Self::Output {
@@ -70,7 +69,7 @@ macro_rules! impl_op {
} }
} }
} }
const impl $opa<f32> for $T { impl const $opa<f32> for $T {
fn $fna(&mut self, rhs: f32) { fn $fna(&mut self, rhs: f32) {
*self = self.$fn(rhs); *self = self.$fn(rhs);
} }
+1 -1
View File
@@ -67,7 +67,7 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; x y); impl_op!(Vec2 Mul mul; x y);
impl_op!(Vec2 Div div; x y); impl_op!(Vec2 Div div; x y);
const impl DivOr for Vec2 { impl const DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self { fn div_or(self, rhs: Self, other: Self) -> Self {
Self { Self {
x: self.x.div_or(rhs.x, other.x), x: self.x.div_or(rhs.x, other.x),
+7 -1
View File
@@ -1,8 +1,12 @@
use crate::Widget; use crate::{RegionAlign, Widget};
pub struct WidgetData { pub struct WidgetData {
pub widget: Box<dyn Widget>, pub widget: Box<dyn Widget>,
pub label: String, pub label: String,
/// alignment used if this does not fill up container
/// and there is no enforced alignment from parent
pub align: RegionAlign,
pub rest_len: Option<f32>,
/// dynamic borrow checking /// dynamic borrow checking
pub borrowed: bool, pub borrowed: bool,
} }
@@ -15,7 +19,9 @@ impl WidgetData {
} }
Self { Self {
widget: Box::new(widget), widget: Box::new(widget),
align: RegionAlign::CENTER,
label, label,
rest_len: None,
borrowed: false, borrowed: false,
} }
} }
+36 -29
View File
@@ -1,7 +1,7 @@
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender}; use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
use crate::{ use crate::{
UiRsc, Widget, HasUi, Widget,
util::{RefCounter, SlotId}, util::{RefCounter, SlotId},
}; };
@@ -12,7 +12,7 @@ pub type WidgetId = SlotId;
/// a signal is sent to the owning UI to clean up the resources. /// 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? /// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
pub struct StrongWidget<W: ?Sized = dyn Widget> { pub struct WidgetHandle<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId, pub(super) id: WidgetId,
counter: RefCounter, counter: RefCounter,
send: Sender<WidgetId>, send: Sender<WidgetId>,
@@ -21,19 +21,24 @@ pub struct StrongWidget<W: ?Sized = dyn Widget> {
/// A weak handle to a widget. /// A weak handle to a widget.
/// Will not keep it alive, but can still be used for indexing like WidgetHandle. /// Will not keep it alive, but can still be used for indexing like WidgetHandle.
pub struct WeakWidget<W: ?Sized = dyn Widget> { pub struct WidgetRef<W: ?Sized = dyn Widget> {
pub(super) id: WidgetId, pub(super) id: WidgetId,
#[allow(unused)] #[allow(unused)]
ty: *const W, ty: *const W,
} }
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> { pub struct WidgetHandles<W: ?Sized = dyn Widget> {
pub fn any(self) -> StrongWidget<dyn Widget> { pub h: WidgetHandle<W>,
pub r: WidgetRef<W>,
}
impl<W: Widget + ?Sized + Unsize<dyn Widget>> WidgetHandle<W> {
pub fn any(self) -> WidgetHandle<dyn Widget> {
self self
} }
} }
impl<W: ?Sized> StrongWidget<W> { impl<W: ?Sized> WidgetHandle<W> {
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self { pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
Self { Self {
id, id,
@@ -51,13 +56,18 @@ impl<W: ?Sized> StrongWidget<W> {
self.counter.refs() self.counter.refs()
} }
pub fn weak(&self) -> WeakWidget<W> { pub fn weak(&self) -> WidgetRef<W> {
let Self { ty, id, .. } = *self; let Self { ty, id, .. } = *self;
WeakWidget { ty, id } WidgetRef { ty, id }
}
pub fn handles(self) -> WidgetHandles<W> {
let r = self.weak();
WidgetHandles { h: self, r }
} }
} }
impl<W: ?Sized> WeakWidget<W> { impl<W: ?Sized> WidgetRef<W> {
pub(crate) fn new(id: WidgetId) -> Self { pub(crate) fn new(id: WidgetId) -> Self {
Self { id, ty: null_ptr() } Self { id, ty: null_ptr() }
} }
@@ -67,12 +77,12 @@ impl<W: ?Sized> WeakWidget<W> {
} }
#[track_caller] #[track_caller]
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> { pub fn upgrade(self, ui: &mut impl HasUi) -> WidgetHandle<W> {
ui.widgets_mut().upgrade(self) ui.ui_mut().widgets.upgrade(self)
} }
} }
impl<W: ?Sized> Drop for StrongWidget<W> { impl<W: ?Sized> Drop for WidgetHandle<W> {
fn drop(&mut self) { fn drop(&mut self) {
if self.counter.drop() { if self.counter.drop() {
let _ = self.send.send(self.id); let _ = self.send.send(self.id);
@@ -80,29 +90,29 @@ impl<W: ?Sized> Drop for StrongWidget<W> {
} }
} }
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {} pub trait WidgetIdFn<State, W: ?Sized = dyn Widget>: FnOnce(&mut State) -> WidgetRef<W> {}
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {} impl<State, W: ?Sized, F: FnOnce(&mut State) -> WidgetRef<W>> WidgetIdFn<State, W> for F {}
pub trait IdLike { pub trait IdLike {
type Widget: ?Sized; type Widget: ?Sized;
fn id(&self) -> WidgetId; fn id(&self) -> WidgetId;
} }
impl<W: ?Sized> IdLike for &StrongWidget<W> { impl<W: ?Sized> IdLike for &WidgetHandle<W> {
type Widget = W; type Widget = W;
fn id(&self) -> WidgetId { fn id(&self) -> WidgetId {
self.id self.id
} }
} }
impl<W: ?Sized> IdLike for StrongWidget<W> { impl<W: ?Sized> IdLike for WidgetHandle<W> {
type Widget = W; type Widget = W;
fn id(&self) -> WidgetId { fn id(&self) -> WidgetId {
self.id self.id
} }
} }
impl<W: ?Sized> IdLike for WeakWidget<W> { impl<W: ?Sized> IdLike for WidgetRef<W> {
type Widget = W; type Widget = W;
fn id(&self) -> WidgetId { fn id(&self) -> WidgetId {
self.id self.id
@@ -116,38 +126,38 @@ impl IdLike for WidgetId {
} }
} }
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<StrongWidget<U>> for StrongWidget<T> {} impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetHandle<U>> for WidgetHandle<T> {}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WeakWidget<U>> for WeakWidget<T> {} impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetRef<U>> for WidgetRef<T> {}
impl<W: ?Sized> Clone for WeakWidget<W> { impl<W: ?Sized> Clone for WidgetRef<W> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
*self *self
} }
} }
impl<W: ?Sized> Copy for WeakWidget<W> {} impl<W: ?Sized> Copy for WidgetRef<W> {}
impl<W: ?Sized> PartialEq for WeakWidget<W> { impl<W: ?Sized> PartialEq for WidgetRef<W> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.id == other.id self.id == other.id
} }
} }
impl<W> PartialEq for StrongWidget<W> { impl<W> PartialEq for WidgetHandle<W> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.id == other.id self.id == other.id
} }
} }
impl<W> std::fmt::Debug for StrongWidget<W> { impl<W> std::fmt::Debug for WidgetHandle<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f) self.id.fmt(f)
} }
} }
impl<'a, W: Widget + 'a, State: UiRsc> FnOnce<(&'a mut State,)> for WeakWidget<W> { impl<'a, W: Widget + 'a, State: HasUi> FnOnce<(&'a mut State,)> for WidgetRef<W> {
type Output = &'a mut W; type Output = &'a mut W;
extern "rust-call" fn call_once(self, args: (&'a mut State,)) -> Self::Output { extern "rust-call" fn call_once(self, args: (&'a mut State,)) -> Self::Output {
&mut args.0.widgets_mut()[self] &mut args.0.ui_mut()[self]
} }
} }
@@ -159,6 +169,3 @@ fn null_ptr<W: ?Sized>() -> *const W {
unsafe { std::mem::transmute_copy(&[0usize; 1]) } unsafe { std::mem::transmute_copy(&[0usize; 1]) }
} }
} }
unsafe impl<W: ?Sized> Send for WeakWidget<W> {}
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
+32 -21
View File
@@ -1,44 +1,55 @@
use crate::UiRsc; use crate::{HasUi, Ui};
use super::*; use super::*;
use std::marker::Unsize; use std::marker::Unsize;
pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized { pub trait StateLike<State> {
fn as_state(&mut self) -> &mut State;
}
impl StateLike<Ui> for Ui {
fn as_state(&mut self) -> &mut Ui {
self
}
}
pub trait WidgetLike<State: HasUi + StateLike<State>, Tag>: Sized {
type Widget: Widget + ?Sized + Unsize<dyn Widget>; type Widget: Widget + ?Sized + Unsize<dyn Widget>;
fn add(self, rsc: &mut Rsc) -> WeakWidget<Self::Widget>; fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<Self::Widget>;
fn add_strong(self, rsc: &mut Rsc) -> StrongWidget<Self::Widget> { fn add_strong(self, state: &mut impl StateLike<State>) -> WidgetHandle<Self::Widget> {
self.add(rsc).upgrade(rsc) self.add(state).upgrade(state.as_state().ui_mut())
} }
fn with_id<W2>( fn with_id<W2>(
self, self,
f: impl FnOnce(&mut Rsc, WeakWidget<Self::Widget>) -> WeakWidget<W2>, f: impl FnOnce(&mut State, WidgetRef<Self::Widget>) -> WidgetRef<W2>,
) -> impl WidgetIdFn<Rsc, W2> { ) -> impl WidgetIdFn<State, W2> {
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
f(state, id) f(state, id)
} }
} }
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) { fn set_root(self, state: &mut impl StateLike<State>) {
let id = self.add_strong(rsc); let id = self.add(state);
root.set_root(id); let ui = state.as_state().ui_mut();
ui.root = Some(id.upgrade(ui));
}
fn handles(self, state: &mut impl StateLike<State>) -> WidgetHandles<Self::Widget> {
self.add(state).upgrade(state.as_state().ui_mut()).handles()
} }
} }
pub trait HasRoot { pub trait WidgetArrLike<State, const LEN: usize, Tag> {
fn set_root(&mut self, root: StrongWidget);
}
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
#[track_caller] #[track_caller]
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>; fn add(self, state: &mut impl StateLike<State>) -> WidgetArr<LEN>;
} }
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> { impl<State, const LEN: usize> WidgetArrLike<State, LEN, ArrTag> for WidgetArr<LEN> {
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> { fn add(self, _: &mut impl StateLike<State>) -> WidgetArr<LEN> {
self self
} }
} }
@@ -49,12 +60,12 @@ macro_rules! impl_widget_arr {
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*); impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
}; };
($n:expr;$($W:ident)*;$($Tag:ident)*) => { ($n:expr;$($W:ident)*;$($Tag:ident)*) => {
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> WidgetArrLike<Rsc, $n, ($($Tag,)*)> for ($($W,)*) { impl<State: HasUi + StateLike<State>, $($W: WidgetLike<State, $Tag>,$Tag,)*> WidgetArrLike<State, $n, ($($Tag,)*)> for ($($W,)*) {
fn add(self, rsc: &mut Rsc) -> WidgetArr<$n> { fn add(self, state: &mut impl StateLike<State>) -> WidgetArr<$n> {
#[allow(non_snake_case)] #[allow(non_snake_case)]
let ($($W,)*) = self; let ($($W,)*) = self;
WidgetArr::new( WidgetArr::new(
[$($W.add(rsc).upgrade(rsc),)*], [$($W.add(state).upgrade(state.as_state().ui_mut()),)*],
) )
} }
} }
+7 -30
View File
@@ -1,47 +1,24 @@
use crate::{Axis, AxisT, Len, Painter, SizeCtx}; use crate::Painter;
use std::any::Any; use std::any::Any;
mod data; mod data;
mod handle; mod handle;
mod like; mod like;
mod tag; mod tag;
mod view;
mod widgets; mod widgets;
pub use data::*; pub use data::*;
pub use handle::*; pub use handle::*;
pub use like::*; pub use like::*;
pub use tag::*; pub use tag::*;
pub use view::*;
pub use widgets::*; pub use widgets::*;
pub trait Widget: Any { pub trait Widget: Any {
fn draw(&mut self, painter: &mut Painter); 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 () { impl Widget for () {
fn draw(&mut self, _: &mut Painter) {} 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 { impl dyn Widget {
@@ -61,27 +38,27 @@ pub trait WidgetFn<State, W: Widget + ?Sized>: FnOnce(&mut State) -> W {}
impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {} impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {}
pub struct WidgetArr<const LEN: usize> { pub struct WidgetArr<const LEN: usize> {
pub arr: [StrongWidget; LEN], pub arr: [WidgetHandle; LEN],
} }
impl<const LEN: usize> WidgetArr<LEN> { impl<const LEN: usize> WidgetArr<LEN> {
pub fn new(arr: [StrongWidget; LEN]) -> Self { pub fn new(arr: [WidgetHandle; LEN]) -> Self {
Self { arr } Self { arr }
} }
} }
pub trait WidgetOption<State> { pub trait WidgetOption<State> {
fn get(self, state: &mut State) -> Option<StrongWidget>; fn get(self, state: &mut State) -> Option<WidgetHandle>;
} }
impl<State> WidgetOption<State> for () { impl<State> WidgetOption<State> for () {
fn get(self, _: &mut State) -> Option<StrongWidget> { fn get(self, _: &mut State) -> Option<WidgetHandle> {
None None
} }
} }
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F { impl<State, F: FnOnce(&mut State) -> Option<WidgetHandle>> WidgetOption<State> for F {
fn get(self, state: &mut State) -> Option<StrongWidget> { fn get(self, state: &mut State) -> Option<WidgetHandle> {
self(state) self(state)
} }
} }
+38 -43
View File
@@ -1,63 +1,58 @@
use super::*; use super::*;
use crate::UiRsc; use crate::HasUi;
use std::marker::Unsize; use std::marker::Unsize;
pub struct WidgetTag; pub struct WidgetTag;
impl<Rsc: UiRsc, W: Widget> WidgetLike<Rsc, WidgetTag> for W { impl<State: HasUi + StateLike<State>, W: Widget> WidgetLike<State, WidgetTag> for W {
type Widget = W; type Widget = W;
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> { fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
let w = rsc.ui_mut().widgets.add_weak(self); state.as_state().get_mut().widgets.add_weak(self)
rsc.on_add(w);
w
} }
} }
pub struct FnTag; pub struct FnTag;
impl<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F { impl<State: HasUi + StateLike<State>, W: Widget, F: FnOnce(&mut State) -> W>
type Widget = W; WidgetLike<State, FnTag> for F
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; type Widget = W;
fn add(self, _: &mut Rsc) -> WeakWidget<W> { fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
self(state.as_state()).add(state)
}
}
pub trait WidgetFnTrait<State> {
type Widget: Widget;
fn run(self, state: &mut State) -> Self::Widget;
}
pub struct FnTraitTag;
impl<State: HasUi + StateLike<State>, T: WidgetFnTrait<State>> WidgetLike<State, FnTraitTag> for T {
type Widget = T::Widget;
#[track_caller]
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<T::Widget> {
self.run(state.as_state()).add(state)
}
}
pub struct IdTag;
impl<State: HasUi + StateLike<State>, W: ?Sized + Widget + Unsize<dyn Widget>>
WidgetLike<State, IdTag> for WidgetRef<W>
{
type Widget = W;
fn add(self, _: &mut impl StateLike<State>) -> WidgetRef<W> {
self self
} }
} }
pub struct RefFnTag; pub struct IdFnTag;
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>, F: FnOnce(&mut Rsc) -> WeakWidget<W>> impl<
WidgetLike<Rsc, RefFnTag> for F State: HasUi + StateLike<State>,
W: ?Sized + Widget + Unsize<dyn Widget>,
F: FnOnce(&mut State) -> WidgetRef<W>,
> WidgetLike<State, IdFnTag> for F
{ {
type Widget = W; type Widget = W;
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> { fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
self(rsc) self(state.as_state())
}
}
pub struct ViewTag;
impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
type Widget = V::Widget;
fn add(self, _: &mut Rsc) -> WeakWidget<Self::Widget> {
self.root()
} }
} }
-24
View File
@@ -1,24 +0,0 @@
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
}
}
+28 -59
View File
@@ -1,7 +1,7 @@
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::Sender;
use crate::{ use crate::{
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId, IdLike, Widget, WidgetData, WidgetHandle, WidgetId, WidgetRef,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
}; };
@@ -9,19 +9,16 @@ pub struct Widgets {
pub needs_redraw: HashSet<WidgetId>, pub needs_redraw: HashSet<WidgetId>,
vec: SlotVec<WidgetData>, vec: SlotVec<WidgetData>,
send: Sender<WidgetId>, send: Sender<WidgetId>,
recv: Receiver<WidgetId>,
pub(crate) waiting: HashSet<WidgetId>, pub(crate) waiting: HashSet<WidgetId>,
} }
impl Widgets { impl Widgets {
pub fn new() -> Self { pub fn new(send: Sender<WidgetId>) -> Self {
let (send, recv) = channel();
Self { Self {
needs_redraw: Default::default(), needs_redraw: Default::default(),
vec: Default::default(), vec: Default::default(),
waiting: Default::default(), waiting: Default::default(),
send, send,
recv,
} }
} }
@@ -29,18 +26,9 @@ impl Widgets {
!self.needs_redraw.is_empty() !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 /// get_dyn but dynamic borrow checking of widgets
/// lets you do recursive (tree) operations, like the painter does /// lets you do recursive (tree) operations, like the painter does
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> { pub(crate) fn get_dyn<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
// SAFETY: must guarantee no other mutable references to this widget exist // SAFETY: must guarantee no other mutable references to this widget exist
// done through the borrow variable // done through the borrow variable
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) }; let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
@@ -54,29 +42,41 @@ impl Widgets {
where where
I::Widget: Sized + Widget, I::Widget: Sized + Widget,
{ {
self.get_dyn(id.id())?.as_any().downcast_ref() self.vec
.get(id.id())?
.widget
.as_ref()
.as_any()
.downcast_ref()
} }
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget> pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
where where
I::Widget: Sized + Widget, I::Widget: Sized + Widget,
{ {
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut() let id = id.id();
self.needs_redraw.insert(id);
self.vec
.get_mut(id)?
.widget
.as_mut()
.as_any_mut()
.downcast_mut()
} }
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> { pub fn add_strong<W: Widget>(&mut self, widget: W) -> WidgetHandle<W> {
let id = self.vec.add(WidgetData::new(widget)); let id = self.vec.add(WidgetData::new(widget));
StrongWidget::new(id, self.send.clone()) WidgetHandle::new(id, self.send.clone())
} }
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> { pub fn add_weak<W: Widget>(&mut self, widget: W) -> WidgetRef<W> {
let id = self.vec.add(WidgetData::new(widget)); let id = self.vec.add(WidgetData::new(widget));
self.waiting.insert(id); self.waiting.insert(id);
WeakWidget::new(id) WidgetRef::new(id)
} }
#[track_caller] #[track_caller]
pub fn upgrade<W: ?Sized>(&mut self, rf: WeakWidget<W>) -> StrongWidget<W> { pub fn upgrade<W: ?Sized>(&mut self, rf: WidgetRef<W>) -> WidgetHandle<W> {
if !self.waiting.remove(&rf.id()) { if !self.waiting.remove(&rf.id()) {
let label = self.label(rf); let label = self.label(rf);
let id = rf.id(); let id = rf.id();
@@ -84,7 +84,7 @@ impl Widgets {
"widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two" "widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two"
) )
} }
StrongWidget::new(rf.id(), self.send.clone()) WidgetHandle::new(rf.id(), self.send.clone())
} }
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> { pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
@@ -95,19 +95,14 @@ impl Widgets {
&self.data(id.id()).unwrap().label &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> { pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
self.vec.get_mut(id.id()) self.vec.get_mut(id.id())
} }
pub fn free_next(&mut self) -> Option<WidgetId> { pub fn delete(&mut self, id: impl IdLike) {
let next = self.recv.try_recv().ok()?; self.vec.free(id.id());
self.vec.free(next); // not sure if there's any point in this
Some(next) // self.updates.remove(&id);
} }
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
@@ -116,30 +111,4 @@ impl Widgets {
} }
} }
impl Default for Widgets {
fn default() -> Self {
Self::new()
}
}
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>; 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()
}
}
+11 -12
View File
@@ -1,21 +1,20 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<State>::run(); App::<State>::run();
} }
#[derive(DefaultUiState)] #[default_ui_state]
struct State { struct State {}
ui_state: DefaultUiState,
}
impl DefaultAppState for State { impl DefaultAppState for State {
fn new( fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
mut ui_state: DefaultUiState, let mut ui = Ui::new();
rsc: &mut DefaultRsc<Self>, rect(Color::RED).set_root(&mut ui);
_: Proxy<Self::Event>, Self {
) -> Self { ui,
rect(Color::RED).set_root(rsc, &mut ui_state); ui_state,
Self { ui_state } events: EventManager::default(),
}
} }
} }
-34
View File
@@ -1,34 +0,0 @@
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
@@ -1,53 +0,0 @@
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 }
}
}
+74 -78
View File
@@ -2,8 +2,8 @@ extern crate proc_macro;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::{ use syn::{
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature, Attribute, Block, Error, Fields, FieldsNamed, GenericParam, Generics, Ident, ItemStruct,
Token, Type, Visibility, ItemTrait, Meta, Signature, Token, Visibility,
parse::{Parse, ParseStream, Result}, parse::{Parse, ParseStream, Result},
parse_macro_input, parse_quote, parse_macro_input, parse_quote,
spanned::Spanned, spanned::Spanned,
@@ -89,108 +89,104 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
quote! { quote! {
#trai #trai
impl #generics #name<Rsc, WL, Tag> for WL { impl #generics #name<State, WL, Tag> for WL {
#(#impls)* #(#impls)*
} }
} }
.into() .into()
} }
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))] #[proc_macro_derive(UiState, attributes(rsc))]
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { pub fn derive_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new(); let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input); let state: ItemStruct = parse_macro_input!(input);
let sname = state.ident;
let rscname = Ident::new(&(sname.to_string() + "Rsc"), sname.span());
let mut rsc_fields = Vec::new();
let mut found_attr = false; for field in state.fields {
let mut state_field = None; let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("rsc")) else {
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; continue;
}; };
if found_attr { let Meta::List(list) = &attr.meta else {
output.extend( output.extend(Error::new(attr.span(), "invalid attr syntax").into_compile_error());
Error::new( continue;
attr.span(), };
"cannot have more than one default_ui_state attribute", let tname: Ident = match list.parse_args::<Ident>() {
) Ok(ident) => ident,
.into_compile_error(), Err(err) => {
); output.extend(err.to_compile_error());
continue; 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 fty = &field.ty;
let fname = field.ident.as_ref().unwrap(); let fname = &field.ident.unwrap();
rsc_fields.extend(quote! {#fname: #fty,});
output.extend(quote! { output.extend(quote! {
impl iris::default::HasDefaultUiState for #sname { impl #tname for #sname {
fn default_state(&self) -> &iris::default::DefaultUiState { fn get(&self) -> &#fty {
&self.#fname &self.#fname
} }
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState { fn get_mut(&mut self) -> &mut #fty {
&mut self.#fname
}
}
impl #tname for #rscname {
fn get(&self) -> &#fty {
&self.#fname
}
fn get_mut(&mut self) -> &mut #fty {
&mut self.#fname &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; let vis = state.vis;
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! { output.extend(quote! {
impl iris::core::WidgetView for #sname { #vis struct #rscname {
type Widget = <#fty as iris::core::HasWidget>::Widget; #(#rsc_fields)*
fn root(&self) -> #fty { }
self.#fname
impl HasState for #sname {
type State = #sname;
}
impl HasState for #rscname {
type State = #sname;
}
impl StateLike<#sname> for #sname {
fn as_state(&mut self) -> &mut Self {
self
}
}
impl StateLike<#rscname> for #rscname {
fn as_state(&mut self) -> &mut Self {
self
} }
} }
}); });
output.into() output.into()
} }
#[proc_macro_attribute]
pub fn default_ui_state(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut state: ItemStruct = parse_macro_input!(input);
let Fields::Named(fields) = &mut state.fields else {
panic!("must be on named fields struct");
};
let name = &state.ident;
state.attrs.push(parse_quote! {#[derive(UiState)]});
let new: FieldsNamed = parse_quote! {{
#[rsc(HasUi)]
pub ui: Ui,
#[rsc(HasDefaultUiState)]
pub ui_state: DefaultUiState,
#[rsc(HasEvents)]
pub events: iris::prelude::EventManager<#name>,
}};
fields.named.extend(new.named);
quote! {#state}.into()
}
+2 -2
View File
@@ -4,12 +4,12 @@ My experimental attempt at a rust ui library (also my first ui library).
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not. It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
Examples are in `examples`, eg. `cargo run --example tabs`. There's a `main.rs` that runs a testing window, so you can just `cargo run` to see it working.
Goals, in general order: Goals, in general order:
1. does what I want it to (text, images, video, animations) 1. does what I want it to (text, images, video, animations)
2. very easy to use ignoring ergonomic ref counting 2. very easy to use ignoring ergonomic ref counting
3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem) 3. reasonably fast / efficient (a lot faster than electron, save battery life)
## dev details ## dev details
-3
View File
@@ -1,3 +0,0 @@
[toolchain]
channel = "nightly"
components = ["clippy", "rustfmt"]
File renamed without changes.
+57 -57
View File
@@ -1,25 +1,26 @@
use cosmic_text::Family;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent; use winit::event::WindowEvent;
use iris::prelude::*; iris::state_prelude!(ClientRsc);
type ClientRsc = DefaultRsc<Client>;
fn main() { fn main() {
DefaultApp::<Client>::run(); App::<Client>::run();
} }
#[derive(DefaultUiState)] #[default_ui_state]
pub struct Client { pub struct Client {
ui_state: DefaultUiState, info: WidgetRef<Text>,
info: WeakWidget<Text>,
} }
impl DefaultAppState for Client { impl DefaultAppState for Client {
fn new( fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
mut ui_state: DefaultUiState, let mut rsc = ClientRsc {
rsc: &mut DefaultRsc<Self>, ui: Ui::new(),
_: Proxy<Self::Event>, ui_state,
) -> Self { events: EventManager::default(),
};
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
@@ -42,7 +43,7 @@ impl DefaultAppState for Client {
.width(rest(3)), .width(rest(3)),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.add(rsc); .add(&mut rsc);
let span_test = ( let span_test = (
rrect.color(Color::GREEN).width(100), rrect.color(Color::GREEN).width(100),
@@ -53,30 +54,30 @@ impl DefaultAppState for Client {
rrect.color(Color::RED).width(100), rrect.color(Color::RED).width(100),
) )
.span(Dir::LEFT) .span(Dir::LEFT)
.add(rsc); .add(&mut rsc);
let span_add = Span::empty(Dir::RIGHT).add(rsc); let span_add = Span::empty(Dir::RIGHT).add(&mut rsc);
let add_button = rect(Color::LIME) let add_button = rect(Color::LIME)
.radius(30) .radius(30)
.on(CursorSense::click(), move |_, rsc| { .on(CursorSense::click(), move |ctx| {
let child = image(include_bytes!("assets/sungals.png")) let child = image(include_bytes!("assets/sungals.png"))
.center() .center()
.add_strong(rsc); .add_strong(ctx);
span_add(rsc).push(child); span_add(ctx).push(child);
}) })
.sized((150, 150)) .sized((150, 150))
.align(Align::BOT_RIGHT); .align(Align::BOT_RIGHT);
let del_button = rect(Color::RED) let del_button = rect(Color::RED)
.radius(30) .radius(30)
.on(CursorSense::click(), move |_, rsc| { .on(CursorSense::click(), move |ctx| {
span_add(rsc).pop(); span_add(ctx).pop();
}) })
.sized((150, 150)) .sized((150, 150))
.align(Align::BOT_LEFT); .align(Align::BOT_LEFT);
let span_add_test = (span_add, add_button, del_button).stack().add(rsc); let span_add_test = (span_add, add_button, del_button).stack().add(&mut rsc);
let btext = |content| wtext(content).size(30); let btext = |content| wtext(content).size(30);
@@ -99,18 +100,18 @@ impl DefaultAppState for Client {
wtext("pretty cool right?").size(50), wtext("pretty cool right?").size(50),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(&mut rsc);
let texts = Span::empty(Dir::DOWN).gap(10).add(rsc); let texts = Span::empty(Dir::DOWN).gap(10).add(&mut rsc);
let msg_area = texts.scrollable().masked().background(rect(Color::SKY)); let msg_area = texts.scrollable().masked().background(rect(Color::SKY));
let add_text = wtext("add") let add_text = wtext("add")
.editable(EditMode::MultiLine) .editable(EditMode::MultiLine)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.size(30) .size(30)
.attr::<Selectable>(()) .attr::<Selectable>(())
.on(Submit, move |ctx, rsc| { .on(Submit, move |ctx| {
let w = ctx.widget; let w = ctx.widget;
let content = w.edit(rsc).take(); let content = w.edit(ctx).take();
let text = wtext(content) let text = wtext(content)
.editable(EditMode::MultiLine) .editable(EditMode::MultiLine)
.size(30) .size(30)
@@ -119,11 +120,10 @@ impl DefaultAppState for Client {
.attr::<Selectable>(()); .attr::<Selectable>(());
let msg_box = text let msg_box = text
.background(rect(Color::WHITE.darker(0.5))) .background(rect(Color::WHITE.darker(0.5)))
.add_strong(rsc); .add_strong(ctx);
texts(rsc).push(msg_box); texts(ctx).push(msg_box);
}) })
.add(rsc); .add(&mut rsc);
let text_edit_scroll = ( let text_edit_scroll = (
msg_area.height(rest(1)), msg_area.height(rest(1)),
( (
@@ -131,8 +131,8 @@ impl DefaultAppState for Client {
( (
add_text.width(rest(1)), add_text.width(rest(1)),
Rect::new(Color::GREEN) Rect::new(Color::GREEN)
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| { .on(CursorSense::click(), move |ctx| {
rsc.run_event::<Submit>(add_text, (), ctx.state); ctx.state.run_event::<Submit>(add_text, &mut ());
}) })
.sized((40, 40)), .sized((40, 40)),
) )
@@ -145,39 +145,39 @@ impl DefaultAppState for Client {
.align(Align::BOT), .align(Align::BOT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.add(rsc); .add(&mut rsc);
let main = WidgetPtr::new().add(rsc); let main = WidgetPtr::new().add(&mut rsc);
let vals = Rc::new(RefCell::new((0, Vec::new()))); let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| { let mut switch_button = |color, to: WidgetRef, label| {
let to = to.upgrade(rsc); let to = to.upgrade(&mut rsc);
let vec = &mut vals.borrow_mut().1; let vec = &mut vals.borrow_mut().1;
let i = vec.len(); let i = vec.len();
if vec.is_empty() { if vec.is_empty() {
vec.push(None); vec.push(None);
main(rsc).set(to); rsc.ui[main].set(to);
} else { } else {
vec.push(Some(to)); vec.push(Some(to));
} }
let vals = vals.clone(); let vals = vals.clone();
let rect = rect(color) let rect = rect(color)
.on(CursorSense::click(), move |ctx, rsc| { .on(CursorSense::click(), move |ctx| {
let (prev, vec) = &mut *vals.borrow_mut(); let (prev, vec) = &mut *vals.borrow_mut();
if let Some(h) = vec[i].take() { if let Some(h) = vec[i].take() {
vec[*prev] = main(rsc).replace(h); vec[*prev] = main(ctx).replace(h);
*prev = i; *prev = i;
} }
ctx.widget(rsc).color = color.darker(0.3); ctx.widget().color = color.darker(0.3);
}) })
.on( .on(
CursorSense::HoverStart | CursorSense::unclick(), CursorSense::HoverStart | CursorSense::unclick(),
move |ctx, rsc| { move |ctx| {
ctx.widget(rsc).color = color.brighter(0.2); ctx.widget().color = color.brighter(0.2);
}, },
) )
.on(CursorSense::HoverEnd, move |ctx, rsc| { .on(CursorSense::HoverEnd, move |ctx| {
ctx.widget(rsc).color = color; ctx.widget().color = color;
}); });
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack() (rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
}; };
@@ -195,30 +195,30 @@ impl DefaultAppState for Client {
) )
.span(Dir::RIGHT); .span(Dir::RIGHT);
let info = wtext("").add(rsc); let info = wtext("").add(&mut rsc);
let info_sect = info.pad(10).align(Align::RIGHT); let info_sect = info.pad(10).align(Align::RIGHT);
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect) ((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
.stack() .stack()
.set_root(rsc, &mut ui_state); .set_root(&mut rsc);
Self { ui_state, info } Self {
ui: rsc.ui,
ui_state: rsc.ui_state,
events: rsc.events,
info,
}
} }
fn window_event( fn window_event(&mut self, _: WindowEvent) {
&mut self,
_: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
let new = format!( let new = format!(
"widgets: {}\nactive: {}\nviews: {}", "widgets: {}\nactive:{}\nviews: {}",
rsc.widgets().len(), self.ui.num_widgets(),
render.active_widgets(), self.ui.active_widgets(),
self.ui_state.renderer.ui.view_count(), self.ui_state.renderer.ui.view_count()
); );
if new != *rsc.widgets()[self.info].content { if new != *self.ui[self.info].content {
*rsc.widgets_mut()[self.info].content = new; *self.ui[self.info].content = new;
} }
} }
} }
-7
View File
@@ -11,13 +11,6 @@ pub trait AppState {
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop); fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop); fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
fn exit(&mut self); fn exit(&mut self);
fn run()
where
Self: Sized,
{
App::<Self>::run();
}
} }
pub struct App<State: AppState> { pub struct App<State: AppState> {
+16 -28
View File
@@ -4,45 +4,35 @@ use winit::dpi::{LogicalPosition, LogicalSize};
pub struct Selector; pub struct Selector;
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector impl<State: HasEvents, W: Widget + 'static> WidgetAttr<State, W> for Selector
where where
Rsc::State: HasDefaultUiState, State::State: HasDefaultUiState,
{ {
type Input = WeakWidget<TextEdit>; type Input = WidgetRef<TextEdit>;
fn run(rsc: &mut Rsc, container: WeakWidget<W>, id: Self::Input) { fn run(state: &mut State, container: WidgetRef<W>, id: Self::Input) {
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| { state.register_event(container, CursorSense::click_or_drag(), move |ctx| {
let region = ctx.data.render.window_region(&id).unwrap(); let region = ctx.ui().window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left; let container_pos = ctx.state.ui().window_region(&container).unwrap().top_left;
let pos = ctx.data.pos + container_pos - id_pos; let pos = ctx.data.pos + container_pos - id_pos;
let size = region.size(); let size = region.size();
select( select(ctx.state, id, pos, size, ctx.data.sense.is_dragging());
rsc,
ctx.data.render,
ctx.state,
id,
pos,
size,
ctx.data.sense.is_dragging(),
);
}); });
} }
} }
pub struct Selectable; pub struct Selectable;
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable impl<State: HasEvents> WidgetAttr<State, TextEdit> for Selectable
where where
Rsc::State: HasDefaultUiState, State::State: HasDefaultUiState,
{ {
type Input = (); type Input = ();
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) { fn run(state: &mut State, id: WidgetRef<TextEdit>, _: Self::Input) {
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| { state.register_event(id, CursorSense::click_or_drag(), move |ctx| {
select( select(
rsc,
ctx.data.render,
ctx.state, ctx.state,
id, id,
ctx.data.pos, ctx.data.pos,
@@ -54,20 +44,18 @@ where
} }
fn select( fn select(
rsc: &mut impl UiRsc,
render: &UiRenderState,
state: &mut impl HasDefaultUiState, state: &mut impl HasDefaultUiState,
id: WeakWidget<TextEdit>, id: WidgetRef<TextEdit>,
pos: Vec2, pos: Vec2,
size: Vec2, size: Vec2,
dragging: bool, dragging: bool,
) { ) {
let state = state.default_state_mut(); let (ui, state) = HasDefaultUiState::ui_with_state(state);
let now = Instant::now(); let now = Instant::now();
let recent = (now - state.last_click) < Duration::from_millis(300); let recent = (now - state.last_click) < Duration::from_millis(300);
state.last_click = now; state.last_click = now;
id.edit(rsc).select(pos, size, dragging, recent); id.edit(ui).select(pos, size, dragging, recent);
if let Some(region) = render.window_region(&id) { if let Some(region) = ui.window_region(&id) {
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()), LogicalPosition::<f32>::from(region.top_left.tuple()),
+46 -214
View File
@@ -1,10 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{ use iris_core::util::forget_mut;
marker::{PhantomData, Sized}, use std::{marker::Sized, sync::Arc, time::Instant};
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -17,8 +14,6 @@ mod event;
mod input; mod input;
mod render; mod render;
mod sense; mod sense;
mod state;
mod task;
pub use app::*; pub use app::*;
pub use attr::*; pub use attr::*;
@@ -26,33 +21,23 @@ pub use event::*;
pub use input::*; pub use input::*;
pub use render::*; pub use render::*;
pub use sense::*; pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub type Proxy<Event> = EventLoopProxy<Event>;
pub struct DefaultUiState { pub struct DefaultUiState {
pub root: Option<StrongWidget>,
pub renderer: UiRenderer, pub renderer: UiRenderer,
pub input: Input, pub input: Input,
pub focus: Option<WeakWidget<TextEdit>>, pub focus: Option<WidgetRef<TextEdit>>,
pub clipboard: Clipboard, pub clipboard: Clipboard,
pub window: Arc<Window>, pub window: Arc<Window>,
pub ime: usize, pub ime: usize,
pub last_click: Instant, pub last_click: Instant,
} }
impl HasRoot for DefaultUiState {
fn set_root(&mut self, root: StrongWidget) {
self.root = Some(root);
}
}
impl DefaultUiState { impl DefaultUiState {
pub fn new(window: impl Into<Arc<Window>>) -> Self { pub fn new(window: impl Into<Arc<Window>>) -> Self {
let window = window.into(); let window = window.into();
Self { Self {
root: None,
renderer: UiRenderer::new(window.clone()), renderer: UiRenderer::new(window.clone()),
window, window,
input: Input::default(), input: Input::default(),
@@ -64,162 +49,53 @@ impl DefaultUiState {
} }
} }
pub trait HasDefaultUiState: Sized + 'static { pub trait HasDefaultUiState: Sized + 'static + HasUi {
fn default_state(&self) -> &DefaultUiState; fn get(&self) -> &DefaultUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState; fn get_mut(&mut self) -> &mut DefaultUiState;
fn ui_state(&self) -> &DefaultUiState {
HasDefaultUiState::get(self)
}
fn ui_state_mut(&mut self) -> &mut DefaultUiState {
HasDefaultUiState::get_mut(self)
}
fn ui_with_state(&mut self) -> (&mut Ui, &mut DefaultUiState) {
// as long as you're not doing anything actually unhinged this should always work safely
(
unsafe { forget_mut(self.ui_mut()) },
HasDefaultUiState::get_mut(self),
)
}
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DefaultAppState: RunEvents + HasDefaultUiState {
type Event = (); type Event: 'static = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DefaultUiState, proxy: Proxy<Self::Event>) -> Self;
-> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(&mut self, event: Self::Event) {}
&mut self,
event: Self::Event,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {} fn exit(&mut self) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn window_event( fn window_event(&mut self, event: WindowEvent) {}
&mut self,
event: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
Default::default() Default::default()
} }
} }
pub struct DefaultRsc<State: 'static> { impl<State: DefaultAppState> AppState for State {
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; type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop let window = event_loop.create_window(Self::window_attributes()).unwrap();
.create_window(State::window_attributes()) Self::new(DefaultUiState::new(window), proxy)
.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) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render); self.event(event);
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { let events = unsafe { forget_mut(self.events_mut()) };
rsc, let ui_state = HasDefaultUiState::get_mut(self);
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 input_changed = ui_state.input.event(&event);
let cursor_state = ui_state.cursor_state().clone(); let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus; let old = ui_state.focus;
@@ -228,43 +104,43 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
if input_changed { if input_changed {
let window_size = ui_state.window_size(); let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size); self.run_sensors(&cursor_state, window_size);
} }
let ui_state = state.default_state_mut(); let (mut ui, mut ui_state) = self.ui_with_state();
if old != ui_state.focus if old != ui_state.focus
&& let Some(old) = old && let Some(old) = old
{ {
old.edit(rsc).deselect(); old.edit(ui).deselect();
} }
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
render.update(&ui_state.root, rsc); ui.update(events);
ui_state.renderer.update(&mut rsc.ui, render); ui_state.renderer.update(ui);
ui_state.renderer.draw(); ui_state.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); ui.resize((size.width, size.height));
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = ui_state.focus if let Some(sel) = ui_state.focus
&& event.state.is_pressed() && event.state.is_pressed()
{ {
let mut text = sel.edit(rsc); let mut text = sel.edit(ui);
match text.apply_event(event, &ui_state.input.modifiers) { match text.apply_event(event, &ui_state.input.modifiers) {
TextInputResult::Unfocus => { TextInputResult::Unfocus => {
ui_state.focus = None; ui_state.focus = None;
ui_state.window.set_ime_allowed(false); ui_state.window.set_ime_allowed(false);
} }
TextInputResult::Submit => { TextInputResult::Submit => {
rsc.run_event::<Submit>(sel, (), state); self.run_event::<Submit>(sel, &mut ());
} }
TextInputResult::Paste => { TextInputResult::Paste => {
if let Ok(t) = ui_state.clipboard.get_text() { if let Ok(t) = ui_state.clipboard.get_text() {
text.insert(&t); text.insert(&t);
} }
rsc.run_event::<Edited>(sel, (), state); self.run_event::<Edited>(sel, &mut ());
} }
TextInputResult::Copy(text) => { TextInputResult::Copy(text) => {
if let Err(err) = ui_state.clipboard.set_text(text) { if let Err(err) = ui_state.clipboard.set_text(text) {
@@ -272,7 +148,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
} }
TextInputResult::Used => { TextInputResult::Used => {
rsc.run_event::<Edited>(sel, (), state); self.run_event::<Edited>(sel, &mut ());
} }
TextInputResult::Unused => {} TextInputResult::Unused => {}
} }
@@ -280,7 +156,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
WindowEvent::Ime(ime) => { WindowEvent::Ime(ime) => {
if let Some(sel) = ui_state.focus { if let Some(sel) = ui_state.focus {
let mut text = sel.edit(rsc); let mut text = sel.edit(ui);
match ime { match ime {
Ime::Enabled | Ime::Disabled => (), Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => { Ime::Preedit(content, _pos) => {
@@ -296,59 +172,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} }
_ => (), _ => (),
} }
state.window_event(event, rsc, render); self.window_event(event);
let ui_state = self.state.default_state_mut(); (ui, ui_state) = self.ui_with_state();
if render.needs_redraw(&ui_state.root, rsc.widgets()) { if ui.needs_redraw() {
ui_state.renderer.window().request_redraw(); ui_state.renderer.window().request_redraw();
} }
ui_state.input.end_frame(); ui_state.input.end_frame();
} }
fn exit(&mut self) { fn exit(&mut self) {
self.state.exit(&mut self.rsc, &mut self.render); self.exit();
}
}
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)
} }
} }
+13 -9
View File
@@ -1,7 +1,7 @@
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState}; use iris_core::{Ui, UiLimits, UiRenderNode};
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use wgpu::*; use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window}; use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK; pub const CLEAR_COLOR: Color = Color::BLACK;
@@ -13,12 +13,13 @@ pub struct UiRenderer {
queue: Queue, queue: Queue,
config: SurfaceConfiguration, config: SurfaceConfiguration,
encoder: CommandEncoder, encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderNode, pub ui: UiRenderNode,
} }
impl UiRenderer { impl UiRenderer {
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) { pub fn update(&mut self, ui: &mut Ui) {
self.ui.update(&self.device, &self.queue, ui, render); self.ui.update(&self.device, &self.queue, ui);
} }
pub fn draw(&mut self) { pub fn draw(&mut self) {
@@ -45,15 +46,16 @@ impl UiRenderer {
} }
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
self.window.pre_present_notify(); self.staging_belt.finish();
output.present(); output.present();
self.staging_belt.recall();
} }
pub fn resize(&mut self, size: &PhysicalSize<u32>) { pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width; self.config.width = size.width;
self.config.height = size.height; self.config.height = size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
self.ui.resize((size.width, size.height), &self.queue); self.ui.resize(size, &self.queue);
} }
fn create_encoder(device: &Device) -> CommandEncoder { fn create_encoder(device: &Device) -> CommandEncoder {
@@ -116,7 +118,7 @@ impl UiRenderer {
format: surface_format, format: surface_format,
width: size.width, width: size.width,
height: size.height, height: size.height,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoNoVsync,
alpha_mode: surface_caps.alpha_modes[0], alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2, desired_maximum_frame_latency: 2,
view_formats: vec![], view_formats: vec![],
@@ -124,9 +126,10 @@ impl UiRenderer {
surface.configure(&device, &config); surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device); let encoder = Self::create_encoder(&device);
let ui = UiRenderNode::new(&device, &queue, &config, ui_limits); let shape_pipeline = UiRenderNode::new(&device, &queue, &config, ui_limits);
Self { Self {
surface, surface,
@@ -134,7 +137,8 @@ impl UiRenderer {
queue, queue,
config, config,
encoder, encoder,
ui, staging_belt,
ui: shape_pipeline,
window, window,
} }
} }
+18 -38
View File
@@ -28,13 +28,12 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses { impl Event for CursorSenses {
type Data<'a> = CursorData<'a>; type Data<'a> = CursorData<'a>;
type State = SensorState; type State = SensorState;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
if let Some(sense) = should_run(self, &data.cursor, data.hover) { if let Some(sense) = should_run(self, data.cursor, data.hover) {
let mut data = data.clone();
data.sense = sense; data.sense = sense;
Some(data) true
} else { } else {
None false
} }
} }
} }
@@ -116,7 +115,7 @@ pub enum ActivationState {
/// or basically have some way to have custom senses /// or basically have some way to have custom senses
/// that depend on active widget positions /// that depend on active widget positions
/// but I'm not sure how or if worth it /// but I'm not sure how or if worth it
pub struct Sensor<Ctx: HasEvents, Data> { pub struct Sensor<Ctx, Data> {
pub senses: CursorSenses, pub senses: CursorSenses,
pub f: Rc<dyn EventFn<Ctx, Data>>, pub f: Rc<dyn EventFn<Ctx, Data>>,
} }
@@ -128,46 +127,29 @@ pub struct SensorState {
pub hover: ActivationState, pub hover: ActivationState,
} }
#[derive(Clone)]
pub struct CursorData<'a> { pub struct CursorData<'a> {
/// where this widget was hit /// where this widget was hit
pub pos: Vec2, pub pos: Vec2,
pub size: Vec2, pub size: Vec2,
pub scroll_delta: Vec2, pub scroll_delta: Vec2,
pub hover: ActivationState, pub hover: ActivationState,
pub cursor: CursorState, pub cursor: &'a CursorState,
/// the first sense that triggered this /// the first sense that triggered this
pub sense: CursorSense, pub sense: CursorSense,
pub render: &'a UiRenderState,
} }
pub trait SensorUi { pub trait SensorUi<State> {
fn run_sensors<Rsc: HasEvents>( fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2);
&self,
rsc: &mut Rsc,
state: &mut Rsc::State,
cursor: CursorState,
window_size: Vec2,
);
} }
impl SensorUi for UiRenderState { impl<State: RunEvents> SensorUi<State> for State {
fn run_sensors<Rsc: HasEvents>( fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) {
&self, let layers = std::mem::take(&mut self.ui_mut().layers);
rsc: &mut Rsc, let mut active = std::mem::take(&mut self.events_mut().get_type::<CursorSense>().active);
state: &mut Rsc::State, for layer in layers.indices().rev() {
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; let mut sensed = false;
for (id, sensor) in active.get_mut(&layer).into_flat_iter() { for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let shape = self.active.get(id).unwrap().region; let shape = self.ui().active.get(id).unwrap().region;
let region = shape.to_px(window_size); let region = shape.to_px(window_size);
let in_shape = cursor.exists && region.contains(cursor.pos); let in_shape = cursor.exists && region.contains(cursor.pos);
sensor.hover.update(in_shape); sensor.hover.update(in_shape);
@@ -176,9 +158,7 @@ impl SensorUi for UiRenderState {
} }
sensed = true; sensed = true;
let cursor = cursor.clone(); let mut data = CursorData {
let data = CursorData {
pos: cursor.pos - region.top_left, pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left, size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
@@ -187,15 +167,15 @@ impl SensorUi for UiRenderState {
// this does not have any meaning; // this does not have any meaning;
// might wanna set up Event to have a prepare stage // might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering, sense: CursorSense::Hovering,
render: self,
}; };
rsc.run_event::<CursorSense>(*id, data, state); self.run_event::<CursorSense>(*id, &mut data);
} }
if sensed { if sensed {
break; break;
} }
} }
rsc.events_mut().get_type::<CursorSense>().active = active; self.events_mut().get_type::<CursorSense>().active = active;
self.ui_mut().layers = layers;
} }
} }
-75
View File
@@ -1,75 +0,0 @@
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
@@ -1,82 +0,0 @@
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);
}
}
+11 -73
View File
@@ -1,87 +1,25 @@
use iris_core::*; use crate::prelude::*;
use iris_macro::*;
use std::sync::Arc;
use crate::default::{TaskCtx, TaskUpdate, Tasks}; pub mod eventable {
use super::*;
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> { widget_trait! {
pub trait Eventable<State: HasEvents + StateLike<State> + 'static>;
fn on<E: EventLike>( fn on<E: EventLike>(
self, self,
event: E, event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, Self::Widget>, f: impl for<'a> WidgetEventFn<State::State, <E::Event as Event>::Data<'a>, WL::Widget>,
) -> impl WidgetIdFn<Rsc, Self::Widget> { ) -> impl WidgetIdFn<State, WL::Widget> {
move |rsc| { move |state| {
let id = self.add(rsc); let id = self.add(state);
rsc.register_event(id, event.into_event(), move |ctx, rsc| { state.register_event(id, event.into_event(), move |ctx| {
f( f(&mut EventIdCtx {
EventIdCtx {
widget: id, widget: id,
state: ctx.state, state: ctx.state,
data: ctx.data, 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 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);
} }
} }
+16 -2
View File
@@ -1,17 +1,31 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(gen_blocks)]
#![feature(associated_type_defaults)] #![feature(associated_type_defaults)]
#![feature(unsize)] #![feature(unsize)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#![feature(async_fn_traits)]
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod typed;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
pub use iris_macro as macros; pub use iris_macro as macros;
#[macro_export]
macro_rules! state_prelude {
($vis:vis $state:ty) => {
iris::event_state!($vis $state);
$vis use iris::{
default::*,
core::{len_fns::*, util::Vec2, *},
macros::*,
widget::*,
};
};
}
pub mod prelude { pub mod prelude {
use super::*; use super::*;
pub use default::*; pub use default::*;
@@ -21,5 +35,5 @@ pub mod prelude {
pub use widget::*; pub use widget::*;
pub use iris_core::util::Vec2; pub use iris_core::util::Vec2;
pub use len_fns::*; pub use typed::*;
} }
+37
View File
@@ -0,0 +1,37 @@
#[macro_export]
macro_rules! event_state {
($vis:vis $state:ty) => {
mod local_event_trait {
use super::*;
#[allow(unused_imports)]
use $crate::prelude::*;
pub trait EventableCtx<WL: WidgetLike<$state, Tag>, Tag> {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<
<$state as HasState>::State,
<E::Event as Event>::Data<'a>,
WL::Widget
>,
) -> impl WidgetIdFn<$state, WL::Widget>;
}
impl<WL: WidgetLike<$state, Tag>, Tag> EventableCtx<WL, Tag> for WL {
fn on<E: EventLike>(
self,
event: E,
f: impl for<'a> WidgetEventFn<
<$state as HasState>::State,
<E::Event as Event>::Data<'a>,
WL::Widget
>,
) -> impl WidgetIdFn<$state, WL::Widget> {
eventable::Eventable::on(self, event, f)
}
}
}
$vis type EventManager = $crate::prelude::EventManager<<$state as HasState>::State>;
$vis use local_event_trait::*;
};
}
pub use event_state;
+2 -2
View File
@@ -19,10 +19,10 @@ impl Widget for Image {
} }
} }
pub fn image<State: UiRsc>(image: impl LoadableImage) -> impl WidgetFn<State, Image> { pub fn image<State: HasUi>(image: impl LoadableImage) -> impl WidgetFn<State, Image> {
let image = image.get_image().expect("Failed to load image"); let image = image.get_image().expect("Failed to load image");
move |state| Image { move |state| Image {
handle: state.ui_mut().textures.add(image), handle: state.get_mut().add_texture(image),
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Masked { pub struct Masked {
pub inner: StrongWidget, pub inner: WidgetHandle,
} }
impl Widget for Masked { impl Widget for Masked {
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Aligned { pub struct Aligned {
pub inner: StrongWidget, pub inner: WidgetHandle,
pub align: Align, pub align: Align,
} }
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct LayerOffset { pub struct LayerOffset {
pub inner: StrongWidget, pub inner: WidgetHandle,
pub offset: usize, pub offset: usize,
} }
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct MaxSize { pub struct MaxSize {
pub inner: StrongWidget, pub inner: WidgetHandle,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Offset { pub struct Offset {
pub inner: StrongWidget, pub inner: WidgetHandle,
pub amt: UiVec2, pub amt: UiVec2,
} }
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::prelude::*;
pub struct Pad { pub struct Pad {
pub padding: Padding, pub padding: Padding,
pub inner: StrongWidget, pub inner: WidgetHandle,
} }
impl Widget for Pad { impl Widget for Pad {
+2 -2
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Scroll { pub struct Scroll {
inner: StrongWidget, inner: WidgetHandle,
axis: Axis, axis: Axis,
amt: f32, amt: f32,
snap_end: bool, snap_end: bool,
@@ -41,7 +41,7 @@ impl Widget for Scroll {
} }
impl Scroll { impl Scroll {
pub fn new(inner: StrongWidget, axis: Axis) -> Self { pub fn new(inner: WidgetHandle, axis: Axis) -> Self {
Self { Self {
inner, inner,
axis, axis,
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
pub struct Sized { pub struct Sized {
pub inner: StrongWidget, pub inner: WidgetHandle,
pub x: Option<Len>, pub x: Option<Len>,
pub y: Option<Len>, pub y: Option<Len>,
} }
+10 -39
View File
@@ -2,18 +2,19 @@ use crate::prelude::*;
use std::marker::PhantomData; use std::marker::PhantomData;
pub struct Span { pub struct Span {
pub children: Vec<StrongWidget>, pub children: Vec<WidgetHandle>,
pub dir: Dir, pub dir: Dir,
pub gap: f32, pub gap: f32,
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) { fn draw(&mut self, painter: &mut Painter) {
let total = self.len_sum(&mut painter.size_ctx()); let total = self.len_sum();
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
for child in &self.children { for child in &self.children {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
painter.widget(id);
let len = painter.len_axis(child, self.dir.axis); let len = painter.len_axis(child, self.dir.axis);
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs); let offset = UiScalar::new(total.rel, total.abs);
@@ -32,20 +33,6 @@ impl Widget for Span {
start.abs += self.gap; start.abs += self.gap;
} }
} }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_len(ctx),
Axis::Y => self.desired_ortho(ctx),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_ortho(ctx),
Axis::Y => self.desired_len(ctx),
}
}
} }
impl Span { impl Span {
@@ -62,30 +49,14 @@ impl Span {
self self
} }
pub fn push(&mut self, w: StrongWidget) { pub fn push(&mut self, w: WidgetHandle) {
self.children.push(w); self.children.push(w);
} }
pub fn pop(&mut self) -> Option<StrongWidget> { pub fn pop(&mut self) -> Option<WidgetHandle> {
self.children.pop() self.children.pop()
} }
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
self.children.iter().fold(Len::abs(gap), |mut s, id| {
// it's tempting to subtract the abs & rel from the ctx outer,
// but that would create inconsistent sizing if you put
// a rest first vs last & only speed up in one direction.
// I think this is only solvable by restricting how you can
// compute size, bc currently you need child to define parent's
// sectioning and you need parent's sectioning to define child.
// Fortunately, that doesn't matter in most cases
let len = ctx.len_axis(id, self.dir.axis);
s += len;
s
})
}
fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len { fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len {
let len = self.len_sum(ctx); let len = self.len_sum(ctx);
if len.rest == 0.0 && len.rel == 0.0 { if len.rest == 0.0 && len.rel == 0.0 {
@@ -159,15 +130,15 @@ pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Ta
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(State, Tag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
for SpanBuilder<Rsc, LEN, Wa, Tag> WidgetFnTrait<State> for SpanBuilder<State, LEN, Wa, Tag>
{ {
type Widget = Span; type Widget = Span;
#[track_caller] #[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget { fn run(self, state: &mut State) -> Self::Widget {
Span { Span {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.add(state).arr.into_iter().collect(),
dir: self.dir, dir: self.dir,
gap: self.gap, gap: self.gap,
} }
@@ -193,7 +164,7 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
} }
impl std::ops::Deref for Span { impl std::ops::Deref for Span {
type Target = Vec<StrongWidget>; type Target = Vec<WidgetHandle>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.children &self.children
+6 -7
View File
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
use crate::prelude::*; use crate::prelude::*;
pub struct Stack { pub struct Stack {
pub children: Vec<StrongWidget>, pub children: Vec<WidgetHandle>,
pub size: StackSize, pub size: StackSize,
} }
@@ -48,15 +48,14 @@ pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, T
_pd: PhantomData<(State, Tag)>, _pd: PhantomData<(State, Tag)>,
} }
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc> impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> FnOnce<(&mut State,)>
for StackBuilder<Rsc, LEN, Wa, Tag> for StackBuilder<State, LEN, Wa, Tag>
{ {
type Widget = Stack; type Output = Stack;
#[track_caller] extern "rust-call" fn call_once(self, args: (&mut State,)) -> Self::Output {
fn run(self, rsc: &mut Rsc) -> Self::Widget {
Stack { Stack {
children: self.children.add(rsc).arr.into_iter().collect(), children: self.children.add(args.0).arr.into_iter().collect(),
size: self.size, size: self.size,
} }
} }
+4 -4
View File
@@ -2,7 +2,7 @@ use crate::prelude::*;
use std::marker::{Sized, Unsize}; use std::marker::{Sized, Unsize};
pub struct WidgetPtr { pub struct WidgetPtr {
pub inner: Option<StrongWidget>, pub inner: Option<WidgetHandle>,
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
@@ -38,14 +38,14 @@ impl WidgetPtr {
inner: Default::default(), inner: Default::default(),
} }
} }
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) { pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: WidgetHandle<W>) {
self.inner = Some(to) self.inner = Some(to)
} }
pub fn replace<W: ?Sized + Unsize<dyn Widget>>( pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
&mut self, &mut self,
to: StrongWidget<W>, to: WidgetHandle<W>,
) -> Option<StrongWidget> { ) -> Option<WidgetHandle> {
self.inner.replace(to) self.inner.replace(to)
} }
} }
+29 -14
View File
@@ -1,4 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use cosmic_text::{Attrs, Family, Metrics};
use std::marker::{PhantomData, Sized}; use std::marker::{PhantomData, Sized};
pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> { pub struct TextBuilder<State, O = TextOutput, H: WidgetOption<State> = ()> {
@@ -19,7 +20,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
self.attrs.color = color; self.attrs.color = color;
self self
} }
pub fn family(mut self, family: Family) -> Self { pub fn family(mut self, family: Family<'static>) -> Self {
self.attrs.family = family; self.attrs.family = family;
self self
} }
@@ -50,15 +51,15 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
} }
} }
impl<Rsc: UiRsc, O> TextBuilder<Rsc, O> { impl<State: HasUi + StateLike<State>, O> TextBuilder<State, O> {
pub fn hint<W: WidgetLike<Rsc, Tag>, Tag>( pub fn hint<W: WidgetLike<State, Tag>, Tag>(
self, self,
hint: W, hint: W,
) -> TextBuilder<Rsc, O, impl WidgetOption<Rsc>> { ) -> TextBuilder<State, O, impl WidgetOption<State>> {
TextBuilder { TextBuilder {
content: self.content, content: self.content,
attrs: self.attrs, attrs: self.attrs,
hint: move |rsc: &mut Rsc| Some(hint.add_strong(rsc).any()), hint: move |ui: &mut State| Some(hint.add_strong(ui).any()),
output: self.output, output: self.output,
state: PhantomData, state: PhantomData,
} }
@@ -74,20 +75,26 @@ pub trait TextBuilderOutput<State>: Sized {
} }
pub struct TextOutput; pub struct TextOutput;
impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput { impl<State: HasUi> TextBuilderOutput<State> for TextOutput {
type Output = Text; type Output = Text;
fn run<H: WidgetOption<Rsc>>( fn run<H: WidgetOption<State>>(
state: &mut Rsc, state: &mut State,
builder: TextBuilder<Rsc, Self, H>, builder: TextBuilder<State, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let mut buf = TextBuffer::new_empty(Metrics::new(
builder.attrs.font_size,
builder.attrs.line_height,
));
let hint = builder.hint.get(state); let hint = builder.hint.get(state);
let font_system = &mut state.get_mut().text.font_system;
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
let mut text = Text { let mut text = Text {
content: builder.content.into(), content: builder.content.into(),
view: TextView::new(buf, builder.attrs, hint), view: TextView::new(buf, builder.attrs, hint),
}; };
text.content.changed = false; text.content.changed = false;
builder.attrs.apply(font_system, &mut text.view.buf, None);
text text
} }
} }
@@ -96,18 +103,26 @@ pub struct TextEditOutput {
mode: EditMode, mode: EditMode,
} }
impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput { impl<State: HasUi> TextBuilderOutput<State> for TextEditOutput {
type Output = TextEdit; type Output = TextEdit;
fn run<H: WidgetOption<State>>( fn run<H: WidgetOption<State>>(
state: &mut State, state: &mut State,
builder: TextBuilder<State, Self, H>, builder: TextBuilder<State, Self, H>,
) -> Self::Output { ) -> Self::Output {
let buf = TextBuffer::new(&builder.content); let buf = TextBuffer::new_empty(Metrics::new(
TextEdit::new( builder.attrs.font_size,
builder.attrs.line_height,
));
let mut text = TextEdit::new(
TextView::new(buf, builder.attrs, builder.hint.get(state)), TextView::new(buf, builder.attrs, builder.hint.get(state)),
builder.output.mode, builder.output.mode,
) );
let font_system = &mut state.get_mut().text.font_system;
text.buf
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
builder.attrs.apply(font_system, &mut text.buf, None);
text
} }
} }
+386 -248
View File
@@ -1,30 +1,17 @@
use crate::prelude::*; use crate::prelude::*;
use iris_core::{TextData, UiColor}; use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
use parley::{Affinity, Layout, Selection};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use unicode_segmentation::UnicodeSegmentation;
use winit::{ use winit::{
event::KeyEvent, event::KeyEvent,
keyboard::{Key, NamedKey}, keyboard::{Key, NamedKey},
}; };
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Motion {
Left,
Right,
LeftWord,
RightWord,
Up,
Down,
LineStart,
LineEnd,
}
pub struct TextEdit { pub struct TextEdit {
view: TextView, view: TextView,
/// `None` represents unfocused, which Parley's `Selection` cannot express. selection: TextSelection,
selection: Option<Selection>, history: Vec<(String, TextSelection)>,
history: Vec<(String, Option<Selection>)>, double_hit: Option<Cursor>,
double_hit: Option<usize>,
pub mode: EditMode, pub mode: EditMode,
} }
@@ -38,19 +25,27 @@ impl TextEdit {
pub fn new(view: TextView, mode: EditMode) -> Self { pub fn new(view: TextView, mode: EditMode) -> Self {
Self { Self {
view, view,
selection: None, selection: Default::default(),
history: Default::default(), history: Default::default(),
double_hit: None, double_hit: None,
mode, mode,
} }
} }
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
pub fn selected_text(&self) -> Option<String> { let (start, end) = sort_cursors(start, end);
let sel = self.selection?; let mut iter = self.buf.lines.iter().skip(start.line);
if sel.is_collapsed() { let first = iter.next().unwrap();
return None; if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
} }
Some(self.buf.text()[sel.text_range()].to_string())
} }
} }
@@ -62,30 +57,40 @@ impl Widget for TextEdit {
painter.layer = base; painter.layer = base;
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let size = vec2(1, self.attrs.line_height);
return; match self.selection {
}; TextSelection::None => (),
let layout = self.view.buf.layout(); TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
// parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) {
let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within(
RectPrimitive::color(Color::SKY),
size.align(Align::TOP_LEFT).offset(top_left).within(&region),
);
}
let caret = selection.focus().geometry(layout, CARET_WIDTH);
let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(top_left).within(&region), 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 { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_width(ctx) self.view.desired_width(ctx)
@@ -96,60 +101,154 @@ impl Widget for TextEdit {
} }
} }
const CARET_WIDTH: f32 = 1.0; /// provides top left + width
fn iter_layout_lines(
start: Cursor,
end: Cursor,
buf: &TextBuffer,
) -> impl Iterator<Item = (usize, f32, f32)> {
gen move {
let mut iter = buf.layout_runs().enumerate();
for (i, line) in iter.by_ref() {
if line.line_i == start.line
&& let Some(start_x) = index_x(&line, start.index)
{
if start.line == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, start_x, end_x - start_x);
return;
}
yield (i, start_x, line.line_w - start_x);
break;
}
}
for (i, line) in iter {
if line.line_i > end.line {
return;
}
if line.line_i == end.line
&& let Some(end_x) = index_x(&line, end.index)
{
yield (i, 0.0, end_x);
return;
}
yield (i, 0.0, line.line_w);
}
}
}
/// copied & modified from fn found in Editor in cosmic_text
/// returns x pos of a (non layout) index within an layout run
fn index_x(run: &LayoutRun, index: usize) -> Option<f32> {
for glyph in run.glyphs.iter() {
if index == glyph.start {
return Some(glyph.x);
} else if index > glyph.start && index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < index {
before += 1;
}
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset);
}
}
None
}
/// returns top of line segment where cursor should visually select
fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some(vec2(run.line_w, run.line_top));
if let Some(pos) = index_x(&run, cursor.index) {
return Some(vec2(pos, run.line_top));
}
}
prev
}
pub struct TextEditCtx<'a> { pub struct TextEditCtx<'a> {
pub text: &'a mut TextEdit, pub text: &'a mut TextEdit,
pub data: &'a mut TextData, pub font_system: &'a mut FontSystem,
} }
impl<'a> TextEditCtx<'a> { impl<'a> TextEditCtx<'a> {
/// Returns a layout synchronized with the current text.
fn layout(&mut self) -> &Layout<UiColor> {
let attrs = self.text.view.attrs.clone();
let width = self.text.view.wrap_width();
self.text.view.buf.shape(self.data, &attrs, width);
self.text.view.buf.layout()
}
/// Keep the selection valid after the text underneath it changed.
fn refresh(&mut self) {
if let Some(sel) = self.text.selection {
let layout = self.layout();
self.text.selection = Some(sel.refresh(layout));
}
}
pub fn take(&mut self) -> String { pub fn take(&mut self) -> String {
let text = self.text.view.buf.text().to_string(); let text = self
self.set(""); .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 text
} }
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.text
self.text.view.buf.changed = true; .buf
self.text.selection = None; .set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
} }
pub fn motion(&mut self, motion: Motion, select: bool) { pub fn motion(&mut self, motion: Motion, select: bool) {
let Some(sel) = self.text.selection else { if let TextSelection::Pos(cursor) = self.text.selection
return; && let Some(new) = self.buf_motion(cursor, motion)
{
if select {
self.text.selection = TextSelection::Span {
start: cursor,
end: new,
}; };
let layout = self.layout(); } else {
let sel = apply_motion(sel, layout, motion, select); self.text.selection = TextSelection::Pos(new);
self.text.selection = Some(sel); }
} else if let TextSelection::Span { start, end } = self.text.selection {
if select {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Span { start, end: cursor };
}
} else {
let (start, end) = sort_cursors(start, end);
let sel = &mut self.text.selection;
match motion {
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
_ => {
if let Some(cursor) = self.buf_motion(end, motion) {
self.text.selection = TextSelection::Pos(cursor);
}
}
}
}
}
} }
/// Replace the `len` characters before the caret. This is the IME's
/// preedit path: it re-sends the whole composition each time.
pub fn replace(&mut self, len: usize, text: &str) { pub fn replace(&mut self, len: usize, text: &str) {
let text = self.string(text); let text = self.string(text);
for _ in 0..len { for _ in 0..len {
self.backspace(false); self.delete(false);
} }
self.insert_str(&text); self.insert_inner(&text, false);
} }
fn string(&self, text: &str) -> String { fn string(&self, text: &str) -> String {
@@ -162,184 +261,202 @@ impl<'a> TextEditCtx<'a> {
pub fn insert(&mut self, text: &str) { pub fn insert(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.insert_str(&text); let mut lines = text.split('\n');
} let Some(first) = lines.next() else {
fn insert_str(&mut self, text: &str) {
if text.is_empty() {
return; return;
}
self.clear_span();
let at = match self.text.selection {
Some(sel) => sel.focus().index(),
None => return,
}; };
let at = at.min(self.text.view.buf.text().len()); self.insert_inner(first, true);
self.text.view.buf.edit().insert_str(at, text); for line in lines {
self.text.view.buf.changed = true; self.newline();
self.set_caret(at + text.len()); self.insert_inner(line, true);
}
} }
/// True when there was a span to remove.
pub fn clear_span(&mut self) -> bool { pub fn clear_span(&mut self) -> bool {
let Some(sel) = self.text.selection else { if let TextSelection::Span { start, end } = self.text.selection {
return false; self.delete_between(start, end);
}; let (start, _) = sort_cursors(start, end);
if sel.is_collapsed() { self.text.selection = TextSelection::Pos(start);
return false;
}
let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start);
true true
} else {
false
}
} }
fn set_caret(&mut self, index: usize) { pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
let index = index.min(self.text.view.buf.text().len()); let lines = &mut self.text.view.buf.lines;
let layout = self.layout(); let (start, end) = sort_cursors(start, end);
self.text.selection = Some(Selection::from_byte_index( if start.line == end.line {
layout, let line = &mut lines[start.line];
index, let text = line.text();
Affinity::default(), 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) { pub fn newline(&mut self) {
if self.text.mode == EditMode::MultiLine { if self.text.mode == EditMode::SingleLine {
self.insert_str("\n"); 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) { pub fn backspace(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
self.delete(word);
} }
let Some(sel) = self.text.selection else {
return;
};
let end = sel.focus().index();
if end == 0 {
return;
}
let start = {
let layout = self.layout();
if word {
sel.focus().previous_logical_word(layout).index()
} else {
let Some(cluster) = sel.focus().logical_clusters(layout)[0] else {
return;
};
let range = cluster.text_range();
if cluster.is_hard_line_break() || cluster.is_emoji() {
range.start
} else {
self.text.view.buf.text()[..range.end]
.char_indices()
.next_back()
.map_or(range.start, |(start, _)| start)
}
}
};
self.delete_range(start, end);
} }
pub fn delete(&mut self, word: bool) { pub fn delete(&mut self, word: bool) {
if self.clear_span() { if !self.clear_span()
return; && let TextSelection::Pos(cursor) = &mut self.text.selection
} {
let Some(sel) = self.text.selection else {
return;
};
let start = sel.focus().index();
if start >= self.text.view.buf.text().len() {
return;
}
let end = {
let layout = self.layout();
if word { if word {
sel.focus().next_logical_word(layout).index() let start = *cursor;
if let Some(end) = self.buf_motion(start, Motion::RightWord) {
self.delete_between(start, end);
}
} else { } else {
let clusters = sel.focus().logical_clusters(layout); let lines = &mut self.text.view.buf.lines;
let Some(cluster) = clusters[1].as_ref() else { let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
if cursor.line == lines.len() - 1 {
return; return;
};
cluster.text_range().end
} }
}; let add = lines.remove(cursor.line + 1).into_text();
self.delete_range(start, end); 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 delete_range(&mut self, start: usize, end: usize) { fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
let len = self.text.view.buf.text().len(); self.text
let (start, end) = (start.min(end).min(len), start.max(end).min(len)); .buf
if start == end { .cursor_motion(self.font_system, cursor, None, motion)
return; .map(|r| r.0)
}
self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start);
} }
pub fn select_all(&mut self) { pub fn select_word_at(&mut self, cursor: Cursor) {
let len = self.text.view.buf.text().len(); if let (Some(start), Some(end)) = (
if len == 0 { self.buf_motion(cursor, Motion::LeftWord),
return; self.buf_motion(cursor, Motion::RightWord),
) {
self.text.selection = TextSelection::Span { start, end };
}
}
pub fn select_line_at(&mut self, cursor: Cursor) {
let end = self.text.buf.lines[cursor.line].text().len();
self.text.selection = TextSelection::Span {
start: Cursor::new(cursor.line, 0),
end: Cursor::new(cursor.line, end),
} }
let layout = self.layout();
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
self.text.selection = Some(Selection::new(anchor, focus));
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { 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 pos = pos - self.text.region().top_left().to_abs(size);
let prev_sel = self.text.selection; let hit = self.text.buf.hit(pos.x, pos.y);
let prev_hit = self.text.double_hit; let sel = &mut self.text.selection;
match sel {
let outcome = { TextSelection::None => {
let layout = self.layout(); if !drag && let Some(hit) = hit {
if drag { *sel = TextSelection::Pos(hit)
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit))
} else {
let hit = Selection::from_point(layout, pos.x, pos.y);
let index = hit.focus().index();
// Successive clicks at one index select the word, then the line.
Some(if recent && prev_hit == Some(index) {
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
(
Some(Selection::word_from_point(layout, pos.x, pos.y)),
Some(index),
)
} else {
(Some(hit), None)
})
} }
}; }
TextSelection::Pos(pos) => match (hit, drag) {
if let Some((selection, double_hit)) = outcome { (None, false) => *sel = TextSelection::None,
self.text.selection = selection; (None, true) => (),
self.text.double_hit = double_hit; (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) { pub fn deselect(&mut self) {
self.text.selection = None; self.text.selection = TextSelection::None;
self.text.double_hit = None;
} }
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.view.buf.text().to_string(), self.text.selection); let old = (self.text.content(), self.text.selection);
let mut undo = false; let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo); let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo { if undo && let Some((old, selection)) = self.text.history.pop() {
if let Some((old, selection)) = self.text.history.pop() {
self.set(&old); self.set(&old);
self.text.selection = selection; self.text.selection = selection;
self.refresh(); } else if self.text.content() != old.0 {
}
} else if self.text.view.buf.text() != old.0 {
self.text.history.push(old); self.text.history.push(old);
} }
res res
@@ -364,25 +481,21 @@ impl<'a> TextEditCtx<'a> {
} }
} }
NamedKey::ArrowRight => { NamedKey::ArrowRight => {
let motion = if modifiers.control { if modifiers.control {
Motion::RightWord self.motion(Motion::RightWord, modifiers.shift)
} else { } else {
Motion::Right self.motion(Motion::Right, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowLeft => { NamedKey::ArrowLeft => {
let motion = if modifiers.control { if modifiers.control {
Motion::LeftWord self.motion(Motion::LeftWord, modifiers.shift)
} else { } else {
Motion::Left self.motion(Motion::Left, modifiers.shift)
}; }
self.motion(motion, modifiers.shift);
} }
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
NamedKey::Escape => { NamedKey::Escape => {
self.deselect(); self.deselect();
return TextInputResult::Unfocus; return TextInputResult::Unfocus;
@@ -394,18 +507,34 @@ impl<'a> TextEditCtx<'a> {
match text.as_str() { match text.as_str() {
"v" => return TextInputResult::Paste, "v" => return TextInputResult::Paste,
"c" => { "c" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"x" => { "x" => {
if let Some(content) = self.text.selected_text() { if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
self.clear_span(); self.clear_span();
return TextInputResult::Copy(content); return TextInputResult::Copy(content);
} }
} }
"a" => self.select_all(), "a" => {
"z" => *undo = true, 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), _ => self.insert(text),
} }
} else { } else {
@@ -418,24 +547,6 @@ impl<'a> TextEditCtx<'a> {
} }
} }
fn apply_motion(
sel: Selection,
layout: &Layout<UiColor>,
motion: Motion,
extend: bool,
) -> Selection {
match motion {
Motion::Left => sel.previous_visual(layout, extend),
Motion::Right => sel.next_visual(layout, extend),
Motion::LeftWord => sel.previous_visual_word(layout, extend),
Motion::RightWord => sel.next_visual_word(layout, extend),
Motion::Up => sel.previous_line(layout, extend),
Motion::Down => sel.next_line(layout, extend),
Motion::LineStart => sel.line_start(layout, extend),
Motion::LineEnd => sel.line_end(layout, extend),
}
}
#[derive(Default)] #[derive(Default)]
pub struct Modifiers { pub struct Modifiers {
pub shift: bool, pub shift: bool,
@@ -458,6 +569,33 @@ pub enum TextInputResult {
Paste, 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 { impl TextInputResult {
pub fn unfocus(&self) -> bool { pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus) matches!(self, TextInputResult::Unfocus)
@@ -479,15 +617,15 @@ impl DerefMut for TextEdit {
} }
pub trait TextEditable { pub trait TextEditable {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a>; fn edit<'a>(&self, ui: &'a mut impl HasUi) -> TextEditCtx<'a>;
} }
impl<I: IdLike<Widget = TextEdit>> TextEditable for I { impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> { fn edit<'a>(&self, ui: &'a mut impl HasUi) -> TextEditCtx<'a> {
let ui = ui.ui_mut(); let ui = ui.ui_mut();
TextEditCtx { TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(), text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text, font_system: &mut ui.text.font_system,
} }
} }
} }
+57 -29
View File
@@ -6,8 +6,11 @@ pub use edit::*;
use iris_core::util::MutDetect; use iris_core::util::MutDetect;
use crate::prelude::*; use crate::prelude::*;
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
pub const SHAPING: Shaping = Shaping::Advanced;
pub struct Text { pub struct Text {
pub content: MutDetect<String>, pub content: MutDetect<String>,
view: TextView, view: TextView,
@@ -19,22 +22,11 @@ pub struct TextView {
// cache // cache
tex: Option<RenderedText>, tex: Option<RenderedText>,
width: Option<f32>, width: Option<f32>,
pub hint: Option<StrongWidget>, pub hint: Option<WidgetHandle>,
} }
impl TextView { impl TextView {
fn is_blank(&self) -> bool { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<WidgetHandle>) -> Self {
self.buf.is_empty()
}
/// The width used by the cached layout.
pub fn wrap_width(&self) -> Option<f32> {
self.width
}
}
impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self { Self {
attrs: attrs.into(), attrs: attrs.into(),
buf: buf.into(), buf: buf.into(),
@@ -53,6 +45,15 @@ impl TextView {
.align(self.align) .align(self.align)
} }
fn tex_region(&self, tex: &RenderedText) -> UiRegion {
let region = tex.size.align(self.align);
let dims = tex.handle.size();
let mut region = region.offset(tex.top_left_offset);
region.x.end = region.x.start + UiScalar::abs(dims.x);
region.y.end = region.y.start + UiScalar::abs(dims.y);
region
}
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText { fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
let width = if self.attrs.wrap { let width = if self.attrs.wrap {
Some(ctx.px_size().x) Some(ctx.px_size().x)
@@ -67,7 +68,10 @@ impl TextView {
return tex.clone(); return tex.clone();
} }
self.width = width; self.width = width;
let tex = ctx.draw_text(&mut self.buf, &self.attrs, width); let font_system = &mut ctx.text.font_system;
self.attrs.apply(font_system, &mut self.buf, width);
self.buf.shape_until_scroll(font_system, false);
let tex = ctx.draw_text(&mut self.buf, &self.attrs);
self.tex = Some(tex.clone()); self.tex = Some(tex.clone());
self.attrs.changed = false; self.attrs.changed = false;
self.buf.changed = false; self.buf.changed = false;
@@ -77,8 +81,9 @@ impl TextView {
self.tex.as_ref() self.tex.as_ref()
} }
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
ctx.width(hint) ctx.width(hint)
} else { } else {
@@ -86,8 +91,9 @@ impl TextView {
} }
} }
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
ctx.height(hint) ctx.height(hint)
} else { } else {
@@ -96,35 +102,47 @@ impl TextView {
} }
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion { pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let tex = self.render(&mut painter.size_ctx()); let tex = self.render(&mut painter.size_ctx());
let region = tex.size.align(self.align); let region = self.tex_region(&tex);
if self.is_blank() if let Some(hint) = &self.hint
&& let Some(hint) = &self.hint && let [line] = &self.buf.lines[..]
&& line.text().is_empty()
{ {
painter.widget(hint); painter.widget(hint);
} else { } else {
let within = region.within(&painter.region()); painter.texture_within(&tex.handle, region);
painter.glyphs(&tex, within);
} }
region region
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
self.buf.text().to_string() self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
} }
} }
impl Text { impl Text {
pub fn new(content: impl Into<String>) -> Self { pub fn new(content: impl Into<String>) -> Self {
let content: String = content.into(); let attrs = TextAttrs::default();
let buf = TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height));
Self { Self {
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None), content: content.into().into(),
content: content.into(), view: TextView::new(buf, attrs, None),
} }
} }
fn update_buf(&mut self, _ctx: &mut SizeCtx) { fn update_buf(&mut self, ctx: &mut SizeCtx) {
if self.content.changed { if self.content.changed {
self.content.changed = false; self.content.changed = false;
self.view.buf.set_text(self.content.as_str()); self.view.buf.set_text(
&mut ctx.text.font_system,
&self.content,
&Attrs::new().family(self.view.attrs.family),
SHAPING,
None,
);
} }
} }
} }
@@ -146,6 +164,16 @@ impl Widget for Text {
} }
} }
pub fn sort_cursors(a: Cursor, b: Cursor) -> (Cursor, Cursor) {
let start = a.min(b);
let end = a.max(b);
(start, end)
}
pub fn edit_line(line: &mut BufferLine, text: String) {
line.set_text(text, line.ending(), line.attrs_list().clone());
}
impl Deref for Text { impl Deref for Text {
type Target = TextAttrs; type Target = TextAttrs;
+26 -25
View File
@@ -3,35 +3,35 @@ use crate::prelude::*;
// these methods should "not require any context" (require unit) because they're in core // these methods should "not require any context" (require unit) because they're in core
widget_trait! { widget_trait! {
pub trait CoreWidget<Rsc: UiRsc + 'static>; pub trait CoreWidget<State: HasUi + StateLike<State> + 'static>;
fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<Rsc, Pad> { fn pad(self, padding: impl Into<Padding>) -> impl WidgetFn<State, Pad> {
|state| Pad { |state| Pad {
padding: padding.into(), padding: padding.into(),
inner: self.add_strong(state), inner: self.add_strong(state),
} }
} }
fn align(self, align: impl Into<Align>) -> impl WidgetFn<Rsc, Aligned> { fn align(self, align: impl Into<Align>) -> impl WidgetFn<State, Aligned> {
move |state| Aligned { move |state| Aligned {
inner: self.add_strong(state), inner: self.add_strong(state),
align: align.into(), align: align.into(),
} }
} }
fn center(self) -> impl WidgetFn<Rsc, Aligned> { fn center(self) -> impl WidgetFn<State, Aligned> {
self.align(Align::CENTER) self.align(Align::CENTER)
} }
fn label(self, label: impl Into<String>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn label(self, label: impl Into<String>) -> impl WidgetIdFn<State, WL::Widget> {
|state| { |state| {
let id = self.add(state); let id = self.add(state);
state.ui_mut().widgets.set_label(id, label.into()); state.get_mut().set_label(id, label.into());
id id
} }
} }
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, Sized> { fn sized(self, size: impl Into<Size>) -> impl WidgetFn<State, Sized> {
let size = size.into(); let size = size.into();
move |state| Sized { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
@@ -40,7 +40,7 @@ widget_trait! {
} }
} }
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> { fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<State, MaxSize> {
let len = len.into(); let len = len.into();
move |state| MaxSize { move |state| MaxSize {
inner: self.add_strong(state), inner: self.add_strong(state),
@@ -49,7 +49,7 @@ widget_trait! {
} }
} }
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> { fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<State, MaxSize> {
let len = len.into(); let len = len.into();
move |state| MaxSize { move |state| MaxSize {
inner: self.add_strong(state), inner: self.add_strong(state),
@@ -58,7 +58,7 @@ widget_trait! {
} }
} }
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> { fn width(self, len: impl Into<Len>) -> impl WidgetFn<State, Sized> {
let len = len.into(); let len = len.into();
move |state| Sized { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
@@ -67,7 +67,7 @@ widget_trait! {
} }
} }
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> { fn height(self, len: impl Into<Len>) -> impl WidgetFn<State, Sized> {
let len = len.into(); let len = len.into();
move |state| Sized { move |state| Sized {
inner: self.add_strong(state), inner: self.add_strong(state),
@@ -76,64 +76,65 @@ widget_trait! {
} }
} }
fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<Rsc, Offset> { fn offset(self, amt: impl Into<UiVec2>) -> impl WidgetFn<State, Offset> {
move |state| Offset { move |state| Offset {
inner: self.add_strong(state), inner: self.add_strong(state),
amt: amt.into(), amt: amt.into(),
} }
} }
fn scrollable(self) -> impl WidgetIdFn<Rsc, Scroll> where Rsc: HasEvents { fn scrollable(self) -> impl WidgetIdFn<State, Scroll> where State: HasEvents {
use eventable::*;
move |state| { move |state| {
Scroll::new(self.add_strong(state), Axis::Y) Scroll::new(self.add_strong(state), Axis::Y)
.on(CursorSense::Scroll, |ctx, rsc| { .on(CursorSense::Scroll, |ctx: &mut EventIdCtx<'_, State::State, CursorData<'_>, Scroll>| {
let delta = ctx.data.scroll_delta.y * 50.0; let delta = ctx.data.scroll_delta.y * 50.0;
ctx.widget(rsc).scroll(delta); ctx.widget().scroll(delta);
}) })
.add(state) .add(state)
} }
} }
fn masked(self) -> impl WidgetFn<Rsc, Masked> { fn masked(self) -> impl WidgetFn<State, Masked> {
move |state| Masked { move |state| Masked {
inner: self.add_strong(state), inner: self.add_strong(state),
} }
} }
fn background<T>(self, w: impl WidgetLike<Rsc, T>) -> impl WidgetFn<Rsc, Stack> { fn background<T>(self, w: impl WidgetLike<State, T>) -> impl WidgetFn<State, Stack> {
move |state| Stack { move |state| Stack {
children: vec![w.add_strong(state), self.add_strong(state)], children: vec![w.add_strong(state), self.add_strong(state)],
size: StackSize::Child(1), size: StackSize::Child(1),
} }
} }
fn foreground<T>(self, w: impl WidgetLike<Rsc, T>) -> impl WidgetFn<Rsc, Stack> { fn foreground<T>(self, w: impl WidgetLike<State, T>) -> impl WidgetFn<State, Stack> {
move |state| Stack { move |state| Stack {
children: vec![self.add_strong(state), w.add_strong(state)], children: vec![self.add_strong(state), w.add_strong(state)],
size: StackSize::Child(0), size: StackSize::Child(0),
} }
} }
fn layer_offset(self, offset: usize) -> impl WidgetFn<Rsc, LayerOffset> { fn layer_offset(self, offset: usize) -> impl WidgetFn<State, LayerOffset> {
move |state| LayerOffset { move |state| LayerOffset {
inner: self.add_strong(state), inner: self.add_strong(state),
offset, offset,
} }
} }
fn to_any(self) -> impl WidgetIdFn<Rsc> { fn to_any(self) -> impl WidgetIdFn<State> {
|state| self.add(state) |state| self.add(state)
} }
fn set_ptr(self, ptr: WeakWidget<WidgetPtr>, state: &mut Rsc) { fn set_ptr(self, ptr: WidgetRef<WidgetPtr>, state: &mut State) {
let id = self.add_strong(state); let id = self.add_strong(state);
state.ui_mut().widgets[ptr].inner = Some(id); state.get_mut()[ptr].inner = Some(id);
} }
} }
pub trait CoreWidgetArr<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> { pub trait CoreWidgetArr<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
fn span(self, dir: Dir) -> SpanBuilder<Rsc, LEN, Wa, Tag>; fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag>;
fn stack(self) -> StackBuilder<Rsc, LEN, Wa, Tag>; fn stack(self) -> StackBuilder<State, LEN, Wa, Tag>;
} }
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
-67
View File
@@ -1,67 +0,0 @@
use iris::prelude::*;
fn editor(text: &str, mode: EditMode) -> (TextEdit, TextData) {
let view = TextView::new(TextBuffer::new(text), TextAttrs::default(), None);
(TextEdit::new(view, mode), TextData::default())
}
fn press(edit: &mut TextEditCtx<'_>, x: f32) {
edit.select(vec2(x, 10.0), vec2(400.0, 200.0), false, false);
}
#[test]
fn pressing_an_empty_field_places_input() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, 40.0);
edit.insert("hello");
assert_eq!(edit.text.content(), "hello");
}
#[test]
fn preedit_replaces_the_previous_composition() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, 0.0);
edit.replace(0, "");
edit.replace(1, "日本");
assert_eq!(edit.text.content(), "日本");
}
#[test]
fn backspace_respects_utf8_boundaries() {
let (mut text, mut data) = editor("", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
press(&mut edit, f32::MAX);
edit.backspace(false);
assert_eq!(edit.text.content(), "a");
}
#[test]
fn typing_replaces_the_selection() {
let (mut text, mut data) = editor("hello", EditMode::SingleLine);
let mut edit = TextEditCtx {
text: &mut text,
data: &mut data,
};
edit.select_all();
edit.insert("goodbye");
assert_eq!(edit.text.content(), "goodbye");
}