Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e4a7f8cf5 | ||
|
|
93291badc1 |
No files matched your search
Generated
+466
-743
File diff suppressed because it is too large.
Load diff
+6
-9
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "iris"
|
||||
default-run = "test"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -8,16 +9,13 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
iris-core = { workspace = true }
|
||||
iris-macro = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
cosmic-text = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
winit = { workspace = true }
|
||||
arboard = { workspace = true, features = ["wayland-data-control"] }
|
||||
pollster = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
image = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||
|
||||
[workspace]
|
||||
members = ["core", "macro"]
|
||||
@@ -29,13 +27,12 @@ edition = "2024"
|
||||
[workspace.dependencies]
|
||||
pollster = "0.4.0"
|
||||
winit = "0.30.12"
|
||||
wgpu = "28.0.0"
|
||||
wgpu = "27.0.1"
|
||||
bytemuck = "1.23.1"
|
||||
image = "0.25.6"
|
||||
parley = "0.11.1"
|
||||
swash = "0.2.10"
|
||||
cosmic-text = "0.15.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
fxhash = "0.2.1"
|
||||
arboard = "3.6.1"
|
||||
iris-core = { path = "core" }
|
||||
iris-macro = { path = "macro" }
|
||||
tokio = "1.49.0"
|
||||
+2
-2
@@ -4,9 +4,9 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
winit = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
cosmic-text = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
+13
-11
@@ -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;
|
||||
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> {
|
||||
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>;
|
||||
pub trait Attrable<State, W: ?Sized, Tag> {
|
||||
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 {
|
||||
fn attr<A: WidgetAttr<Rsc, WL::Widget>>(
|
||||
impl<State: HasUi + StateLike<State>, WL: WidgetLike<State, Tag>, Tag>
|
||||
Attrable<State, WL::Widget, Tag> for WL
|
||||
{
|
||||
fn attr<A: WidgetAttr<State, WL::Widget>>(
|
||||
self,
|
||||
input: A::Input,
|
||||
) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
|rsc| {
|
||||
let id = self.add(rsc);
|
||||
A::run(rsc, id, input);
|
||||
) -> impl WidgetIdFn<State, WL::Widget> {
|
||||
|state| {
|
||||
let id = self.add(state);
|
||||
A::run(state, id, input);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
+109
-11
@@ -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 state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
pub struct EventCtx<'a, State, Data> {
|
||||
pub state: &'a mut State,
|
||||
pub data: &'a mut Data,
|
||||
}
|
||||
|
||||
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> {
|
||||
pub widget: WeakWidget<W>,
|
||||
pub state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
pub struct EventIdCtx<'a, State, Data, W: ?Sized> {
|
||||
pub widget: WidgetRef<W>,
|
||||
pub state: &'a mut State,
|
||||
pub data: &'a mut Data,
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents, Data, W: Widget> EventIdCtx<'_, Rsc, Data, W> {
|
||||
pub fn widget<'a>(&self, rsc: &'a mut Rsc) -> &'a mut W {
|
||||
&mut rsc.ui_mut().widgets[self.widget]
|
||||
impl<State: HasUi, Data, W: ?Sized> Deref for EventIdCtx<'_, State, Data, W> {
|
||||
type Target = State;
|
||||
|
||||
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);
|
||||
// });
|
||||
// }
|
||||
+36
-42
@@ -1,16 +1,16 @@
|
||||
use crate::{
|
||||
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
|
||||
WeakWidget, WidgetEventFn, WidgetId,
|
||||
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, IdLike, LayerId, Widget,
|
||||
WidgetEventFn, WidgetId, WidgetRef,
|
||||
util::{HashMap, HashSet, TypeMap},
|
||||
};
|
||||
use std::{any::TypeId, rc::Rc};
|
||||
|
||||
pub struct EventManager<Rsc> {
|
||||
pub struct EventManager<State> {
|
||||
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 {
|
||||
Self {
|
||||
widget_to_types: Default::default(),
|
||||
@@ -19,27 +19,26 @@ impl<Rsc> Default for EventManager<Rsc> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static> EventManager<Rsc> {
|
||||
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<Rsc, E::Event> {
|
||||
impl<State: 'static> EventManager<State> {
|
||||
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<State, E::Event> {
|
||||
self.types.type_or_default()
|
||||
}
|
||||
|
||||
pub fn register<I: IdLike + 'static, E: EventLike>(
|
||||
pub fn register<W: Widget + ?Sized, E: EventLike>(
|
||||
&mut self,
|
||||
id: I,
|
||||
id: WidgetRef<W>,
|
||||
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.widget_to_types
|
||||
.entry(i)
|
||||
.entry(id.id())
|
||||
.or_default()
|
||||
.insert(Self::type_key::<E>());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
|
||||
impl<State: 'static> EventsLike for EventManager<State> {
|
||||
fn remove(&mut self, id: WidgetId) {
|
||||
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().remove(id);
|
||||
@@ -75,14 +74,14 @@ pub trait EventManagerLike<State> {
|
||||
fn undraw(&mut self, data: &ActiveData);
|
||||
}
|
||||
|
||||
type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>>);
|
||||
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
||||
type EventData<State, E> = (E, Rc<dyn for<'a> EventFn<State, <E as Event>::Data<'a>>>);
|
||||
pub struct TypeEventManager<State, E: Event> {
|
||||
// TODO: reduce visiblity!!
|
||||
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) {
|
||||
self.map.remove(&id);
|
||||
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 {
|
||||
Self {
|
||||
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> {
|
||||
fn register<I: IdLike + 'static>(
|
||||
impl<State: 'static, E: Event> TypeEventManager<State, E> {
|
||||
fn register<W: Widget + ?Sized>(
|
||||
&mut self,
|
||||
widget: I,
|
||||
widget: WidgetRef<W>,
|
||||
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();
|
||||
self.map.entry(widget.id()).or_default().push((
|
||||
event,
|
||||
Rc::new(move |ctx, rsc| {
|
||||
f(
|
||||
EventIdCtx {
|
||||
widget: WeakWidget::new(widget.id()),
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
Rc::new(move |ctx| {
|
||||
let mut test = EventIdCtx {
|
||||
widget,
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
};
|
||||
f(&mut test);
|
||||
}),
|
||||
));
|
||||
}
|
||||
@@ -138,18 +135,15 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
||||
pub fn run_fn<'a>(
|
||||
&mut self,
|
||||
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();
|
||||
move |ctx, rsc| {
|
||||
move |ctx| {
|
||||
for (e, f) in fs {
|
||||
if let Some(data) = e.should_run(&ctx.data) {
|
||||
f(
|
||||
EventCtx {
|
||||
state: ctx.state,
|
||||
data,
|
||||
},
|
||||
rsc,
|
||||
)
|
||||
if e.should_run(ctx.data) {
|
||||
f(&mut EventCtx {
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-12
@@ -7,11 +7,11 @@ pub use manager::*;
|
||||
pub use rsc::*;
|
||||
|
||||
pub trait Event: Sized + 'static + Clone {
|
||||
type Data<'a>: Clone = ();
|
||||
type Data<'a> = ();
|
||||
type State: Default = ();
|
||||
#[allow(unused_variables)]
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
Some(data.clone())
|
||||
fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,17 +28,14 @@ impl<E: Event> EventLike for E {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventFn<Rsc: HasEvents, Data>: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static {}
|
||||
impl<Rsc: HasEvents, F: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static, Data> EventFn<Rsc, Data>
|
||||
for F
|
||||
{
|
||||
}
|
||||
pub trait EventFn<State, Data>: Fn(&mut EventCtx<State, Data>) + 'static {}
|
||||
impl<State, F: Fn(&mut EventCtx<State, Data>) + 'static, Data> EventFn<State, Data> for F {}
|
||||
|
||||
pub trait WidgetEventFn<Rsc: HasEvents, Data, W: ?Sized>:
|
||||
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static
|
||||
pub trait WidgetEventFn<State, Data, W: ?Sized>:
|
||||
Fn(&mut EventIdCtx<State, Data, W>) + 'static
|
||||
{
|
||||
}
|
||||
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized>
|
||||
WidgetEventFn<Rsc, Data, W> for F
|
||||
impl<State, F: Fn(&mut EventIdCtx<State, Data, W>) + 'static, Data, W: ?Sized>
|
||||
WidgetEventFn<State, Data, W> for F
|
||||
{
|
||||
}
|
||||
+21
-14
@@ -1,34 +1,41 @@
|
||||
use crate::{
|
||||
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||
Event, EventCtx, EventLike, EventManager, HasUi, IdLike, Widget, WidgetEventFn, WidgetRef,
|
||||
};
|
||||
|
||||
pub trait HasState: 'static {
|
||||
type State;
|
||||
pub trait HasState {
|
||||
type State: HasUi;
|
||||
}
|
||||
|
||||
pub trait HasEvents: Sized + UiRsc + HasState {
|
||||
fn events(&self) -> &EventManager<Self>;
|
||||
fn events_mut(&mut self) -> &mut EventManager<Self>;
|
||||
pub trait HasEvents: Sized + HasState + HasUi {
|
||||
fn get(&self) -> &EventManager<Self::State>;
|
||||
fn get_mut(&mut self) -> &mut EventManager<Self::State>;
|
||||
fn events(&self) -> &EventManager<Self::State> {
|
||||
HasEvents::get(self)
|
||||
}
|
||||
fn events_mut(&mut self) -> &mut EventManager<Self::State> {
|
||||
HasEvents::get_mut(self)
|
||||
}
|
||||
|
||||
fn register_event<W: Widget + ?Sized, E: EventLike>(
|
||||
&mut self,
|
||||
id: WeakWidget<W>,
|
||||
id: WidgetRef<W>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunEvents: HasEvents {
|
||||
pub trait RunEvents: HasEvents + HasState<State = Self> + 'static {
|
||||
fn run_event<E: EventLike>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
data: <E::Event as Event>::Data<'_>,
|
||||
state: &mut Self::State,
|
||||
data: &mut <E::Event as Event>::Data<'_>,
|
||||
) {
|
||||
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 {}
|
||||
@@ -2,9 +2,12 @@
|
||||
#![feature(const_ops)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(const_convert)]
|
||||
#![feature(map_try_insert)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_cmp)]
|
||||
#![feature(const_destruct)]
|
||||
#![feature(portable_simd)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
|
||||
+5
-5
@@ -5,19 +5,19 @@ pub const trait UiNum {
|
||||
fn to_f32(self) -> f32;
|
||||
}
|
||||
|
||||
const impl UiNum for f32 {
|
||||
impl const UiNum for f32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for u32 {
|
||||
impl const UiNum for u32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for i32 {
|
||||
impl const UiNum for i32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
|
||||
Vec2::new(x.to_f32(), y.to_f32())
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
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
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::vec2;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
@@ -24,13 +24,26 @@ impl Align {
|
||||
pub const TOP: CardinalAlign = CardinalAlign::TOP;
|
||||
pub const V_CENTER: CardinalAlign = CardinalAlign::V_CENTER;
|
||||
pub const BOT: CardinalAlign = CardinalAlign::BOT;
|
||||
pub const NONE: Align = Align { x: None, y: None };
|
||||
|
||||
pub fn tuple(&self) -> (Option<AxisAlign>, Option<AxisAlign>) {
|
||||
(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 {
|
||||
Neg,
|
||||
Center,
|
||||
@@ -106,6 +119,7 @@ impl UiVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
/// aligns this as a size within a region
|
||||
pub fn align(&self, align: RegionAlign) -> UiRegion {
|
||||
UiRegion {
|
||||
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 {
|
||||
Self::rel(align.rel())
|
||||
}
|
||||
|
||||
@@ -74,14 +74,14 @@ pub const trait AxisT {
|
||||
}
|
||||
|
||||
pub struct XAxis;
|
||||
const impl AxisT for XAxis {
|
||||
impl const AxisT for XAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::X
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YAxis;
|
||||
const impl AxisT for YAxis {
|
||||
impl const AxisT for YAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::Y
|
||||
}
|
||||
|
||||
@@ -124,13 +124,13 @@ impl Display for UiVec2 {
|
||||
impl_op!(UiVec2 Add add; 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 {
|
||||
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
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
@@ -140,12 +140,22 @@ where
|
||||
}
|
||||
|
||||
#[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 rel: 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 Hash for UiScalar {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
|
||||
@@ -10,12 +10,6 @@ pub struct Color<T> {
|
||||
pub a: T,
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Default for Color<T> {
|
||||
fn default() -> Self {
|
||||
Self::BLACK
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ColorNum> Color<T> {
|
||||
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
||||
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
||||
@@ -150,7 +144,7 @@ impl ColorNum for f32 {
|
||||
|
||||
unsafe impl bytemuck::Pod for Color<u8> {}
|
||||
|
||||
const impl F32Conversion for f32 {
|
||||
impl const F32Conversion for f32 {
|
||||
fn to(self) -> f32 {
|
||||
self
|
||||
}
|
||||
@@ -159,7 +153,7 @@ const impl F32Conversion for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
const impl F32Conversion for u8 {
|
||||
impl const F32Conversion for u8 {
|
||||
fn to(self) -> f32 {
|
||||
self as f32 / 255.0
|
||||
}
|
||||
|
||||
+138
-201
@@ -1,63 +1,60 @@
|
||||
use crate::{Align, GlyphAtlas, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, util::Vec2};
|
||||
use parley::{
|
||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
};
|
||||
use swash::{
|
||||
FontRef,
|
||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||
zeno::{Format, Vector},
|
||||
use crate::{Align, RegionAlign, TextureHandle, Textures, UiColor, util::Vec2};
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, Buffer, CacheKey, Color, Family, FontSystem, Metrics, Placement, SwashCache,
|
||||
SwashContent,
|
||||
};
|
||||
use image::{GenericImageView, RgbaImage};
|
||||
use std::simd::{Simd, num::SimdUint};
|
||||
|
||||
/// TODO: properly wrap this
|
||||
pub mod text_lib {
|
||||
pub use cosmic_text::*;
|
||||
}
|
||||
|
||||
pub struct TextData {
|
||||
pub font_cx: FontContext,
|
||||
pub layout_cx: LayoutContext<UiColor>,
|
||||
scale_cx: ScaleContext,
|
||||
pub atlas: GlyphAtlas,
|
||||
pub font_system: FontSystem,
|
||||
pub swash_cache: SwashCache,
|
||||
glyph_cache: Vec<(Placement, CacheKey, Color)>,
|
||||
}
|
||||
|
||||
impl Default for TextData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_cx: FontContext::new(),
|
||||
layout_cx: LayoutContext::new(),
|
||||
scale_cx: ScaleContext::new(),
|
||||
atlas: GlyphAtlas::default(),
|
||||
font_system: FontSystem::new(),
|
||||
swash_cache: SwashCache::new(),
|
||||
glyph_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum Family {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Family {
|
||||
fn family(&self) -> FontFamily<'_> {
|
||||
let name = match self {
|
||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
||||
};
|
||||
FontFamily::Single(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct TextAttrs {
|
||||
pub color: UiColor,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub family: Family,
|
||||
pub family: Family<'static>,
|
||||
pub wrap: bool,
|
||||
/// inner alignment of text region (within where it's drawn)
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
@@ -73,178 +70,118 @@ impl Default for TextAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps text and its corresponding layout from getting out of sync.
|
||||
pub struct TextBuffer {
|
||||
text: String,
|
||||
layout: Layout<UiColor>,
|
||||
shaped: Option<(TextAttrs, Option<f32>)>,
|
||||
}
|
||||
|
||||
impl TextBuffer {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
layout: Layout::new(),
|
||||
shaped: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new("")
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> &Layout<UiColor> {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: impl Into<String>) {
|
||||
let text = text.into();
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
self.shaped = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidates the layout and returns the underlying string for editing.
|
||||
pub fn edit(&mut self) -> &mut String {
|
||||
self.shaped = None;
|
||||
&mut self.text
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.layout.width(), self.layout.height())
|
||||
}
|
||||
|
||||
pub fn shape(&mut self, data: &mut TextData, attrs: &TextAttrs, width: Option<f32>) {
|
||||
if self.shaped.as_ref() == Some(&(attrs.clone(), width)) {
|
||||
return;
|
||||
}
|
||||
let mut builder = data
|
||||
.layout_cx
|
||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||
builder.push_default(StyleProperty::FontFamily(attrs.family.family()));
|
||||
builder.push_default(StyleProperty::FontSize(attrs.font_size));
|
||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||
attrs.line_height,
|
||||
)));
|
||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||
builder.build_into(&mut self.layout, &self.text);
|
||||
self.layout.break_all_lines(width);
|
||||
self.layout
|
||||
.align(Alignment::Start, AlignmentOptions::default());
|
||||
self.shaped = Some((attrs.clone(), width));
|
||||
}
|
||||
}
|
||||
pub const LINE_HEIGHT_MULT: f32 = 1.1;
|
||||
|
||||
impl TextData {
|
||||
pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec<PlacedGlyph> {
|
||||
let mut placed = Vec::new();
|
||||
for line in buffer.layout.lines() {
|
||||
for item in line.items() {
|
||||
let PositionedLayoutItem::GlyphRun(run) = item else {
|
||||
continue;
|
||||
};
|
||||
let font = run.run().font();
|
||||
let font_size = run.run().font_size();
|
||||
let coords = run.run().normalized_coords();
|
||||
let Some(font_ref) = FontRef::from_index(font.data.as_ref(), font.index as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let coords_hash = hash_coords(coords);
|
||||
// `font.data.id()` rather than the pointer, so the same font
|
||||
// loaded twice is still one set of entries.
|
||||
let font_id = font.data.id();
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
// TODO: either this or the layout stuff (or both) is super slow,
|
||||
// should probably do texture packing and things if possible.
|
||||
// very visible if you add just a couple of wrapping texts and resize window
|
||||
// should also be timed to figure out exactly what points need to be sped up
|
||||
// let mut pixels = HashMap::<_, [u8; 4]>::default();
|
||||
let mut min_x = 0;
|
||||
let mut min_y = 0;
|
||||
let mut max_x = 0;
|
||||
let mut max_y = 0;
|
||||
let text_color = {
|
||||
let c = attrs.color;
|
||||
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
|
||||
};
|
||||
let mut max_width = 0.0f32;
|
||||
let mut height = 0.0;
|
||||
|
||||
for 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,
|
||||
),
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
let max_dim = 8192;
|
||||
if image.width() > max_dim || image.height() > max_dim {
|
||||
let width = image.width().min(max_dim);
|
||||
let height = image.height().min(max_dim);
|
||||
eprintln!(
|
||||
"WARNING: image of size {:?} cropped to {:?} (texture too big)",
|
||||
image.dimensions(),
|
||||
(width, height)
|
||||
);
|
||||
image = image.view(0, 0, width, height).to_image();
|
||||
}
|
||||
|
||||
RenderedText {
|
||||
handle: textures.add(image),
|
||||
top_left_offset: Vec2::new(min_x as f32, min_y as f32),
|
||||
size: Vec2::new(max_width, height),
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderedText {
|
||||
pub glyphs: std::sync::Arc<Vec<PlacedGlyph>>,
|
||||
pub handle: TextureHandle,
|
||||
pub top_left_offset: Vec2,
|
||||
pub size: Vec2,
|
||||
pub color: UiColor,
|
||||
}
|
||||
|
||||
impl TextData {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
textures: &mut Textures,
|
||||
) -> RenderedText {
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer, textures);
|
||||
RenderedText {
|
||||
glyphs: std::sync::Arc::new(glyphs),
|
||||
size: buffer.size(),
|
||||
color: attrs.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,24 +29,14 @@ pub struct Textures {
|
||||
pub enum TextureUpdate<'a> {
|
||||
Push(&'a DynamicImage),
|
||||
Set(u32, &'a DynamicImage),
|
||||
Patch(u32, PatchRect, &'a DynamicImage),
|
||||
Free(u32),
|
||||
PushFree,
|
||||
SetFree,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatchRect {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(u32),
|
||||
Set(u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
|
||||
@@ -91,18 +81,6 @@ impl Textures {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
||||
self.images[handle.inner.view_idx as usize]
|
||||
.as_mut()
|
||||
.expect("texture was freed while still held")
|
||||
}
|
||||
|
||||
/// Queue an upload of just `rect`, after writing it with `image_mut`.
|
||||
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
||||
self.updates
|
||||
.push(Update::Patch(handle.inner.view_idx, rect));
|
||||
}
|
||||
|
||||
pub fn free(&mut self) {
|
||||
for idx in self.recv.try_iter() {
|
||||
self.images[idx as usize] = None;
|
||||
@@ -121,10 +99,6 @@ impl Textures {
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Patch(i, rect, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Free(i) => TextureUpdate::Free(i),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
use crate::{
|
||||
PatchRect, TextureHandle, Textures,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
const PAGE: u32 = 1024;
|
||||
|
||||
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||
/// pick up its neighbour along a shared edge.
|
||||
const PAD: u32 = 1;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphKey {
|
||||
pub font: u64,
|
||||
pub glyph: u32,
|
||||
/// Font size in 1/16 px, so sizes that round to the same pixels share a
|
||||
/// raster instead of filling the atlas with near-duplicates.
|
||||
pub size: u32,
|
||||
/// Horizontal subpixel phase, in 1/4 px.
|
||||
pub subpixel: u8,
|
||||
/// Hash of the variation coordinates; a variable font at two weights is two
|
||||
/// different sets of pixels from one glyph id.
|
||||
pub coords: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GlyphEntry {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
/// Offset from the glyph's pen position to the top-left of its pixels.
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_color: bool,
|
||||
pub view_idx: u32,
|
||||
pub sampler_idx: u32,
|
||||
}
|
||||
|
||||
struct Page {
|
||||
handle: TextureHandle,
|
||||
x: u32,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
pages: Vec<Page>,
|
||||
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||
/// too, so it is not re-rasterised on every layout.
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.entries.get(key).copied()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
key: GlyphKey,
|
||||
image: &Image,
|
||||
textures: &mut Textures,
|
||||
) -> Option<GlyphEntry> {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
||||
self.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (page_idx, x, y) = self.allocate(w, h, textures);
|
||||
let page = &self.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
write_glyph(rgba, image, x, y);
|
||||
|
||||
let handle = page.handle.clone();
|
||||
let rect = PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
textures.patch(&handle, rect);
|
||||
|
||||
let page = &self.pages[page_idx];
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
||||
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
|
||||
left: image.placement.left,
|
||||
top: image.placement.top,
|
||||
width: w,
|
||||
height: h,
|
||||
is_color: matches!(image.content, Content::Color),
|
||||
view_idx: page.handle.primitive().view_idx,
|
||||
sampler_idx: page.handle.primitive().sampler_idx,
|
||||
};
|
||||
self.entries.insert(key, Some(entry));
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
||||
let need_w = w + PAD;
|
||||
let need_h = h + PAD;
|
||||
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
|
||||
let page = &mut self.pages[i];
|
||||
if page.x + need_w > PAGE {
|
||||
page.y += page.shelf_height;
|
||||
page.x = PAD;
|
||||
page.shelf_height = 0;
|
||||
}
|
||||
let (x, y) = (page.x, page.y);
|
||||
page.x += need_w;
|
||||
page.shelf_height = page.shelf_height.max(need_h);
|
||||
return (i, x, y);
|
||||
}
|
||||
|
||||
let handle = textures.add(RgbaImage::new(PAGE, PAGE));
|
||||
self.pages.push(Page {
|
||||
handle,
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
(self.pages.len() - 1, PAD, PAD)
|
||||
}
|
||||
|
||||
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||
self.entries.insert(key, None);
|
||||
}
|
||||
|
||||
pub fn page_count(&self) -> usize {
|
||||
self.pages.len()
|
||||
}
|
||||
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
||||
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|
||||
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
|
||||
}
|
||||
|
||||
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
|
||||
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
match image.content {
|
||||
Content::Mask => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let a = image.data[(row * w + col) as usize];
|
||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::Color => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let i = ((row * w + col) * 4) as usize;
|
||||
let px = [
|
||||
image.data[i],
|
||||
image.data[i + 1],
|
||||
image.data[i + 2],
|
||||
image.data[i + 3],
|
||||
];
|
||||
page.put_pixel(x + col, y + row, image::Rgba(px));
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::SubpixelMask => {
|
||||
// Preserve readable output if the rasterizer returns a subpixel mask.
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let i = ((row * w + col) * 4) as usize;
|
||||
let a = image.data[i + 1];
|
||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PlacedGlyph {
|
||||
pub entry: GlyphEntry,
|
||||
pub offset: Vec2,
|
||||
}
|
||||
+12
-23
@@ -1,23 +1,22 @@
|
||||
use std::num::NonZero;
|
||||
|
||||
use crate::{
|
||||
UiData, UiRenderState,
|
||||
Ui,
|
||||
render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf},
|
||||
util::{HashMap, Vec2},
|
||||
util::HashMap,
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod primitive;
|
||||
mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx};
|
||||
pub use primitive::*;
|
||||
|
||||
@@ -60,18 +59,12 @@ impl UiRenderNode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
ui: &mut UiData,
|
||||
ui_render: &mut UiRenderState,
|
||||
) {
|
||||
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) {
|
||||
self.active.clear();
|
||||
for (i, primitives) in ui_render.layers.iter_mut() {
|
||||
for (i, primitives) in ui.layers.iter_mut() {
|
||||
self.active.push(i);
|
||||
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 {
|
||||
if h.layer == i && h.inst_idx == change.old {
|
||||
h.inst_idx = change.new;
|
||||
@@ -119,11 +112,10 @@ impl UiRenderNode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||
let size = size.into();
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>, queue: &Queue) {
|
||||
let slice = &[WindowUniform {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
width: size.width as f32,
|
||||
height: size.height as f32,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
@@ -139,10 +131,7 @@ impl UiRenderNode {
|
||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
||||
});
|
||||
|
||||
let window_uniform = WindowUniform {
|
||||
width: config.width as f32,
|
||||
height: config.height as f32,
|
||||
};
|
||||
let window_uniform = WindowUniform::default();
|
||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||
label: Some("window"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
@@ -194,7 +183,7 @@ impl UiRenderNode {
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout],
|
||||
immediate_size: 0,
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
@@ -230,7 +219,7 @@ impl UiRenderNode {
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: None,
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ macro_rules! primitives {
|
||||
}
|
||||
)*
|
||||
};
|
||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
|
||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t),+) };
|
||||
(@count $t:tt) => { 1 };
|
||||
}
|
||||
|
||||
@@ -201,7 +201,6 @@ impl PrimitiveHandle {
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
textures: TexturePrimitive => 1,
|
||||
glyphs: GlyphPrimitive => 2,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
@@ -231,21 +230,6 @@ pub struct TexturePrimitive {
|
||||
pub sampler_idx: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
pub view_idx: u32,
|
||||
pub sampler_idx: u32,
|
||||
pub color: Color<u8>,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
impl GlyphPrimitive {
|
||||
pub const IS_COLOR: u32 = 1;
|
||||
}
|
||||
|
||||
pub struct PrimitiveVec<T> {
|
||||
vec: Vec<T>,
|
||||
free: Vec<usize>,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const RECT: u32 = 0u;
|
||||
const TEXTURE: u32 = 1u;
|
||||
const GLYPH: u32 = 2u;
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> window: WindowUniform;
|
||||
@@ -8,8 +7,6 @@ var<uniform> window: WindowUniform;
|
||||
var<storage> rects: array<Rect>;
|
||||
@group(1) @binding(TEXTURE)
|
||||
var<storage> textures: array<TextureInfo>;
|
||||
@group(1) @binding(GLYPH)
|
||||
var<storage> glyphs: array<GlyphInfo>;
|
||||
|
||||
struct Rect {
|
||||
color: u32,
|
||||
@@ -23,15 +20,6 @@ struct TextureInfo {
|
||||
sampler_idx: u32,
|
||||
}
|
||||
|
||||
struct GlyphInfo {
|
||||
uv_min: vec2<f32>,
|
||||
uv_max: vec2<f32>,
|
||||
view_idx: u32,
|
||||
sampler_idx: u32,
|
||||
color: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
struct Mask {
|
||||
x: UiSpan,
|
||||
y: UiSpan,
|
||||
@@ -137,9 +125,6 @@ fn fs_main(
|
||||
case TEXTURE: {
|
||||
color = draw_texture(region, textures[i]);
|
||||
}
|
||||
case GLYPH: {
|
||||
color = draw_glyph(region, glyphs[i]);
|
||||
}
|
||||
default: {
|
||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
||||
let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv);
|
||||
if (g.flags & 1u) != 0u {
|
||||
return texel;
|
||||
}
|
||||
var color = unpack4x8unorm(g.color);
|
||||
color.a *= texel.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
||||
var color = unpack4x8unorm(rect.color);
|
||||
|
||||
|
||||
+13
-75
@@ -1,13 +1,11 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{PatchRect, TextureUpdate, Textures};
|
||||
use crate::{TextureUpdate, Textures};
|
||||
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
/// Parallel to `views`; patches require textures rather than views.
|
||||
textures: Vec<Option<Texture>>,
|
||||
views: Vec<TextureView>,
|
||||
view_count: usize,
|
||||
samplers: Vec<Sampler>,
|
||||
@@ -19,95 +17,37 @@ impl GpuTextures {
|
||||
pub fn update(&mut self, textures: &mut Textures) -> bool {
|
||||
let mut changed = false;
|
||||
for update in textures.updates() {
|
||||
changed = true;
|
||||
match update {
|
||||
TextureUpdate::Push(image) => {
|
||||
self.push(image);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Set(i, image) => {
|
||||
self.set(i, image);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Patch(i, rect, image) => {
|
||||
// Patching contents leaves the binding array unchanged.
|
||||
self.patch(i, rect, image);
|
||||
}
|
||||
TextureUpdate::SetFree => {
|
||||
self.view_count += 1;
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Free(i) => {
|
||||
self.free(i);
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::PushFree => {
|
||||
self.push_free();
|
||||
changed = true;
|
||||
}
|
||||
TextureUpdate::Push(image) => self.push(image),
|
||||
TextureUpdate::Set(i, image) => self.set(i, image),
|
||||
TextureUpdate::SetFree => self.view_count += 1,
|
||||
TextureUpdate::Free(i) => self.free(i),
|
||||
TextureUpdate::PushFree => self.push_free(),
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
fn set(&mut self, i: u32, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let (texture, view) = self.create(image);
|
||||
self.textures[i as usize] = Some(texture);
|
||||
let view = self.create_view(image);
|
||||
self.views[i as usize] = view;
|
||||
}
|
||||
fn free(&mut self, i: u32) {
|
||||
self.view_count -= 1;
|
||||
self.textures[i as usize] = None;
|
||||
self.views[i as usize] = self.null_view.clone();
|
||||
}
|
||||
fn push(&mut self, image: &DynamicImage) {
|
||||
self.view_count += 1;
|
||||
let (texture, view) = self.create(image);
|
||||
self.textures.push(Some(texture));
|
||||
let view = self.create_view(image);
|
||||
self.views.push(view);
|
||||
}
|
||||
fn push_free(&mut self) {
|
||||
self.view_count += 1;
|
||||
self.textures.push(None);
|
||||
self.views.push(self.null_view.clone());
|
||||
}
|
||||
|
||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||
let Some(texture) = &self.textures[i as usize] else {
|
||||
return;
|
||||
};
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
// `write_texture` requires tightly packed rows, unlike the atlas image.
|
||||
let sub = image
|
||||
.view(rect.x, rect.y, rect.width, rect.height)
|
||||
.to_image();
|
||||
self.queue.write_texture(
|
||||
TexelCopyTextureInfo {
|
||||
texture,
|
||||
mip_level: 0,
|
||||
origin: Origin3d {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
z: 0,
|
||||
},
|
||||
aspect: TextureAspect::All,
|
||||
},
|
||||
sub.as_bytes(),
|
||||
TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(rect.width * 4),
|
||||
rows_per_image: Some(rect.height),
|
||||
},
|
||||
Extent3d {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn create(&self, image: &DynamicImage) -> (Texture, TextureView) {
|
||||
fn create_view(&self, image: &DynamicImage) -> TextureView {
|
||||
let image = image.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let texture = self.device.create_texture_with_data(
|
||||
@@ -123,14 +63,13 @@ impl GpuTextures {
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8Unorm,
|
||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
image.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
(texture, view)
|
||||
texture.create_view(&TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||
@@ -138,7 +77,6 @@ impl GpuTextures {
|
||||
Self {
|
||||
device: device.clone(),
|
||||
queue: queue.clone(),
|
||||
textures: Vec::new(),
|
||||
views: Vec::new(),
|
||||
samplers: vec![default_sampler(device)],
|
||||
no_views: vec![null_view.clone()],
|
||||
|
||||
@@ -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
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
pub region: UiRegion,
|
||||
pub region_used: UiRegion,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
pub children: Vec<WidgetId>,
|
||||
pub mask: MaskIdx,
|
||||
pub layer: LayerId,
|
||||
pub align: Align,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+193
-29
@@ -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 cache;
|
||||
mod draw_state;
|
||||
mod painter;
|
||||
mod render_state;
|
||||
mod size;
|
||||
mod state;
|
||||
|
||||
pub use active::*;
|
||||
pub use painter::Painter;
|
||||
pub use render_state::*;
|
||||
pub use size::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub struct Ui {
|
||||
// TODO: edit visibilities
|
||||
pub widgets: Widgets,
|
||||
|
||||
// retained painter state
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
pub layers: PrimitiveLayers,
|
||||
pub textures: Textures,
|
||||
pub text: TextData,
|
||||
output_size: Vec2,
|
||||
pub masks: TrackedArena<Mask, u32>,
|
||||
|
||||
pub root: Option<WidgetHandle>,
|
||||
old_root: Option<WidgetId>,
|
||||
recv: Receiver<WidgetId>,
|
||||
resized: bool,
|
||||
}
|
||||
|
||||
pub trait UiRsc {
|
||||
fn ui(&self) -> &UiData;
|
||||
fn ui_mut(&mut self) -> &mut UiData;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn on_add(&mut self, id: WeakWidget) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_remove(&mut self, id: WidgetId) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_draw(&mut self, active: &ActiveData) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_undraw(&mut self, active: &ActiveData) {}
|
||||
|
||||
fn widgets(&self) -> &Widgets {
|
||||
&self.ui().widgets
|
||||
pub trait HasUi: Sized {
|
||||
fn get(&self) -> &Ui;
|
||||
fn get_mut(&mut self) -> &mut Ui;
|
||||
fn ui(&self) -> &Ui {
|
||||
self.get()
|
||||
}
|
||||
fn widgets_mut(&mut self) -> &mut Widgets {
|
||||
&mut self.ui_mut().widgets
|
||||
fn ui_mut(&mut self) -> &mut Ui {
|
||||
self.get_mut()
|
||||
}
|
||||
fn free(&mut self) {
|
||||
while let Some(id) = self.widgets_mut().free_next() {
|
||||
self.on_remove(id);
|
||||
}
|
||||
|
||||
impl HasUi for Ui {
|
||||
fn get(&self) -> &Ui {
|
||||
self
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
self.ui_mut().textures.free();
|
||||
}
|
||||
}
|
||||
+26
-80
@@ -1,15 +1,15 @@
|
||||
use crate::{
|
||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||
render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
RenderedText, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, WidgetHandle, WidgetId,
|
||||
render::{Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||
ui::draw_state::DrawState,
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
/// makes your surfaces look pretty
|
||||
pub struct Painter<'a> {
|
||||
pub(super) state: &'a mut UiRenderState,
|
||||
pub(super) rsc: &'a mut dyn UiRsc,
|
||||
pub struct Painter<'a, 'b> {
|
||||
pub(super) state: &'a mut DrawState<'b>,
|
||||
|
||||
pub region_used: UiRegion,
|
||||
pub(super) region: UiRegion,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
@@ -19,7 +19,7 @@ pub struct Painter<'a> {
|
||||
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) {
|
||||
let h = self.state.layers.write(
|
||||
self.layer,
|
||||
@@ -32,7 +32,7 @@ impl<'a> Painter<'a> {
|
||||
);
|
||||
if self.mask != MaskIdx::NONE {
|
||||
// TODO: I have no clue if this works at all :joy:
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
self.state.masks.push_ref(self.mask);
|
||||
}
|
||||
self.primitives.push(h);
|
||||
}
|
||||
@@ -48,31 +48,28 @@ impl<'a> Painter<'a> {
|
||||
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn rest_hint(&self, id: &WidgetHandle) -> f32 {
|
||||
self.state.widgets.data(id).unwrap().align
|
||||
}
|
||||
|
||||
/// Draws a widget somewhere within this one.
|
||||
/// Useful for drawing child widgets in select areas.
|
||||
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||
pub fn widget_within<W: ?Sized>(&mut self, id: &WidgetHandle<W>, region: UiRegion) {
|
||||
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.state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
self.mask,
|
||||
None,
|
||||
self.rsc,
|
||||
);
|
||||
self.state
|
||||
.draw_inner(self.layer, id.id(), region, Some(self.id), self.mask);
|
||||
}
|
||||
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
@@ -85,65 +82,18 @@ impl<'a> Painter<'a> {
|
||||
self.primitive(handle.primitive());
|
||||
}
|
||||
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.textures.push(handle.clone());
|
||||
self.primitive_at(handle.primitive(), region);
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
&mut self,
|
||||
buffer: &mut TextBuffer,
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
let ui = self.rsc.ui_mut();
|
||||
ui.text.render(buffer, attrs, width, &mut ui.textures)
|
||||
}
|
||||
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
let flags_for = |is_color| {
|
||||
if is_color {
|
||||
GlyphPrimitive::IS_COLOR
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
region.y.end = region.y.start;
|
||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||
self.primitive_at(
|
||||
GlyphPrimitive {
|
||||
uv_min: glyph.entry.uv_min,
|
||||
uv_max: glyph.entry.uv_max,
|
||||
view_idx: glyph.entry.view_idx,
|
||||
sampler_idx: glyph.entry.sampler_idx,
|
||||
color: text.color,
|
||||
flags: flags_for(glyph.entry.is_color),
|
||||
},
|
||||
region,
|
||||
);
|
||||
}
|
||||
/// returns (handle, offset from top left)
|
||||
pub fn render_text(&mut self, buffer: &mut TextBuffer, attrs: &TextAttrs) -> RenderedText {
|
||||
self.state
|
||||
.ui
|
||||
.text
|
||||
.draw(buffer, attrs, &mut self.state.ui.textures)
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
self.region
|
||||
}
|
||||
|
||||
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||
self.size_ctx().size(id)
|
||||
}
|
||||
|
||||
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
|
||||
match axis {
|
||||
Axis::X => self.size_ctx().width(id),
|
||||
Axis::Y => self.size_ctx().height(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
self.state.output_size
|
||||
}
|
||||
@@ -153,7 +103,7 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
&mut self.state.text
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
@@ -165,14 +115,10 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
|
||||
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 {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
|
||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// pub struct DynState {
|
||||
//
|
||||
// }
|
||||
@@ -9,16 +9,15 @@ pub const trait DivOr {
|
||||
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 {
|
||||
let res = self / rhs;
|
||||
if res.is_nan() { other } else { res }
|
||||
}
|
||||
}
|
||||
|
||||
const impl<
|
||||
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
|
||||
> LerpUtil for T
|
||||
impl<T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy> const
|
||||
LerpUtil for T
|
||||
{
|
||||
/// linear interpolation
|
||||
/// from * (1.0 - self) + to * self
|
||||
@@ -38,7 +37,7 @@ macro_rules! impl_op {
|
||||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use std::ops::*;
|
||||
const impl $op for $T {
|
||||
impl const $op for $T {
|
||||
type Output = Self;
|
||||
|
||||
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) {
|
||||
*self = self.$fn(rhs);
|
||||
}
|
||||
}
|
||||
const impl $op<f32> for $T {
|
||||
impl const $op<f32> for $T {
|
||||
type Output = Self;
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
*self = self.$fn(rhs);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ pub use id::*;
|
||||
pub use math::*;
|
||||
pub use refcount::*;
|
||||
pub use slot::*;
|
||||
pub(crate) use trust::*;
|
||||
pub use trust::*;
|
||||
pub use typemap::*;
|
||||
pub use vec2::*;
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||
pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||
unsafe { std::mem::transmute::<&T, &T>(x) }
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||
pub unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
|
||||
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
pub unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
std::mem::transmute::<&T, &mut T>(x)
|
||||
|
||||
@@ -67,7 +67,7 @@ impl_op!(Vec2 Sub sub; x y);
|
||||
impl_op!(Vec2 Mul mul; x y);
|
||||
impl_op!(Vec2 Div div; x y);
|
||||
|
||||
const impl DivOr for Vec2 {
|
||||
impl const DivOr for Vec2 {
|
||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x.div_or(rhs.x, other.x),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use crate::Widget;
|
||||
use crate::{RegionAlign, Widget};
|
||||
|
||||
pub struct WidgetData {
|
||||
pub widget: Box<dyn Widget>,
|
||||
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
|
||||
pub borrowed: bool,
|
||||
}
|
||||
@@ -15,7 +19,9 @@ impl WidgetData {
|
||||
}
|
||||
Self {
|
||||
widget: Box::new(widget),
|
||||
align: RegionAlign::CENTER,
|
||||
label,
|
||||
rest_len: None,
|
||||
borrowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
+36
-29
@@ -1,7 +1,7 @@
|
||||
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
||||
|
||||
use crate::{
|
||||
UiRsc, Widget,
|
||||
HasUi, Widget,
|
||||
util::{RefCounter, SlotId},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ pub type WidgetId = SlotId;
|
||||
/// a signal is sent to the owning UI to clean up the resources.
|
||||
///
|
||||
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
|
||||
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||
pub struct WidgetHandle<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
counter: RefCounter,
|
||||
send: Sender<WidgetId>,
|
||||
@@ -21,19 +21,24 @@ pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||
|
||||
/// A weak handle to a widget.
|
||||
/// Will not keep it alive, but can still be used for indexing like WidgetHandle.
|
||||
pub struct WeakWidget<W: ?Sized = dyn Widget> {
|
||||
pub struct WidgetRef<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
#[allow(unused)]
|
||||
ty: *const W,
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> {
|
||||
pub fn any(self) -> StrongWidget<dyn Widget> {
|
||||
pub struct WidgetHandles<W: ?Sized = dyn Widget> {
|
||||
pub h: WidgetHandle<W>,
|
||||
pub r: WidgetRef<W>,
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized + Unsize<dyn Widget>> WidgetHandle<W> {
|
||||
pub fn any(self) -> WidgetHandle<dyn Widget> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> StrongWidget<W> {
|
||||
impl<W: ?Sized> WidgetHandle<W> {
|
||||
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
@@ -51,13 +56,18 @@ impl<W: ?Sized> StrongWidget<W> {
|
||||
self.counter.refs()
|
||||
}
|
||||
|
||||
pub fn weak(&self) -> WeakWidget<W> {
|
||||
pub fn weak(&self) -> WidgetRef<W> {
|
||||
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 {
|
||||
Self { id, ty: null_ptr() }
|
||||
}
|
||||
@@ -67,12 +77,12 @@ impl<W: ?Sized> WeakWidget<W> {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> {
|
||||
ui.widgets_mut().upgrade(self)
|
||||
pub fn upgrade(self, ui: &mut impl HasUi) -> WidgetHandle<W> {
|
||||
ui.ui_mut().widgets.upgrade(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> Drop for StrongWidget<W> {
|
||||
impl<W: ?Sized> Drop for WidgetHandle<W> {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
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> {}
|
||||
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
|
||||
pub trait WidgetIdFn<State, W: ?Sized = dyn Widget>: FnOnce(&mut State) -> WidgetRef<W> {}
|
||||
impl<State, W: ?Sized, F: FnOnce(&mut State) -> WidgetRef<W>> WidgetIdFn<State, W> for F {}
|
||||
|
||||
pub trait IdLike {
|
||||
type Widget: ?Sized;
|
||||
fn id(&self) -> WidgetId;
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for &StrongWidget<W> {
|
||||
impl<W: ?Sized> IdLike for &WidgetHandle<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for StrongWidget<W> {
|
||||
impl<W: ?Sized> IdLike for WidgetHandle<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for WeakWidget<W> {
|
||||
impl<W: ?Sized> IdLike for WidgetRef<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
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<WeakWidget<U>> for WeakWidget<T> {}
|
||||
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetHandle<U>> for WidgetHandle<T> {}
|
||||
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WidgetRef<U>> for WidgetRef<T> {}
|
||||
|
||||
impl<W: ?Sized> Clone for WeakWidget<W> {
|
||||
impl<W: ?Sized> Clone for WidgetRef<W> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl<W: ?Sized> Copy for WeakWidget<W> {}
|
||||
impl<W: ?Sized> PartialEq for WeakWidget<W> {
|
||||
impl<W: ?Sized> Copy for WidgetRef<W> {}
|
||||
impl<W: ?Sized> PartialEq for WidgetRef<W> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> PartialEq for StrongWidget<W> {
|
||||
impl<W> PartialEq for WidgetHandle<W> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
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 {
|
||||
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;
|
||||
|
||||
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 impl<W: ?Sized> Send for WeakWidget<W> {}
|
||||
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
|
||||
+32
-21
@@ -1,44 +1,55 @@
|
||||
use crate::UiRsc;
|
||||
use crate::{HasUi, Ui};
|
||||
|
||||
use super::*;
|
||||
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>;
|
||||
|
||||
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> {
|
||||
self.add(rsc).upgrade(rsc)
|
||||
fn add_strong(self, state: &mut impl StateLike<State>) -> WidgetHandle<Self::Widget> {
|
||||
self.add(state).upgrade(state.as_state().ui_mut())
|
||||
}
|
||||
|
||||
fn with_id<W2>(
|
||||
self,
|
||||
f: impl FnOnce(&mut Rsc, WeakWidget<Self::Widget>) -> WeakWidget<W2>,
|
||||
) -> impl WidgetIdFn<Rsc, W2> {
|
||||
f: impl FnOnce(&mut State, WidgetRef<Self::Widget>) -> WidgetRef<W2>,
|
||||
) -> impl WidgetIdFn<State, W2> {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
f(state, id)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) {
|
||||
let id = self.add_strong(rsc);
|
||||
root.set_root(id);
|
||||
fn set_root(self, state: &mut impl StateLike<State>) {
|
||||
let id = self.add(state);
|
||||
let ui = state.as_state().ui_mut();
|
||||
ui.root = Some(id.upgrade(ui));
|
||||
}
|
||||
|
||||
fn handles(self, state: &mut impl StateLike<State>) -> WidgetHandles<Self::Widget> {
|
||||
self.add(state).upgrade(state.as_state().ui_mut()).handles()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasRoot {
|
||||
fn set_root(&mut self, root: StrongWidget);
|
||||
}
|
||||
|
||||
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
||||
pub trait WidgetArrLike<State, const LEN: usize, Tag> {
|
||||
#[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> {
|
||||
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> {
|
||||
impl<State, const LEN: usize> WidgetArrLike<State, LEN, ArrTag> for WidgetArr<LEN> {
|
||||
fn add(self, _: &mut impl StateLike<State>) -> WidgetArr<LEN> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -49,12 +60,12 @@ macro_rules! impl_widget_arr {
|
||||
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
|
||||
};
|
||||
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
|
||||
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> WidgetArrLike<Rsc, $n, ($($Tag,)*)> for ($($W,)*) {
|
||||
fn add(self, rsc: &mut Rsc) -> WidgetArr<$n> {
|
||||
impl<State: HasUi + StateLike<State>, $($W: WidgetLike<State, $Tag>,$Tag,)*> WidgetArrLike<State, $n, ($($Tag,)*)> for ($($W,)*) {
|
||||
fn add(self, state: &mut impl StateLike<State>) -> WidgetArr<$n> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($W,)*) = self;
|
||||
WidgetArr::new(
|
||||
[$($W.add(rsc).upgrade(rsc),)*],
|
||||
[$($W.add(state).upgrade(state.as_state().ui_mut()),)*],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-30
@@ -1,47 +1,24 @@
|
||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
||||
use crate::Painter;
|
||||
use std::any::Any;
|
||||
|
||||
mod data;
|
||||
mod handle;
|
||||
mod like;
|
||||
mod tag;
|
||||
mod view;
|
||||
mod widgets;
|
||||
|
||||
pub use data::*;
|
||||
pub use handle::*;
|
||||
pub use like::*;
|
||||
pub use tag::*;
|
||||
pub use view::*;
|
||||
pub use widgets::*;
|
||||
|
||||
pub trait Widget: Any {
|
||||
fn draw(&mut self, painter: &mut Painter);
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
}
|
||||
|
||||
pub trait WidgetAxisFns {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
match A::get() {
|
||||
Axis::X => self.desired_width(ctx),
|
||||
Axis::Y => self.desired_height(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for () {
|
||||
fn draw(&mut self, _: &mut Painter) {}
|
||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
}
|
||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||
Len::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Widget {
|
||||
@@ -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 {}
|
||||
|
||||
pub struct WidgetArr<const LEN: usize> {
|
||||
pub arr: [StrongWidget; LEN],
|
||||
pub arr: [WidgetHandle; LEN],
|
||||
}
|
||||
|
||||
impl<const LEN: usize> WidgetArr<LEN> {
|
||||
pub fn new(arr: [StrongWidget; LEN]) -> Self {
|
||||
pub fn new(arr: [WidgetHandle; LEN]) -> Self {
|
||||
Self { arr }
|
||||
}
|
||||
}
|
||||
|
||||
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 () {
|
||||
fn get(self, _: &mut State) -> Option<StrongWidget> {
|
||||
fn get(self, _: &mut State) -> Option<WidgetHandle> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F {
|
||||
fn get(self, state: &mut State) -> Option<StrongWidget> {
|
||||
impl<State, F: FnOnce(&mut State) -> Option<WidgetHandle>> WidgetOption<State> for F {
|
||||
fn get(self, state: &mut State) -> Option<WidgetHandle> {
|
||||
self(state)
|
||||
}
|
||||
}
|
||||
+38
-43
@@ -1,63 +1,58 @@
|
||||
use super::*;
|
||||
use crate::UiRsc;
|
||||
use crate::HasUi;
|
||||
use std::marker::Unsize;
|
||||
|
||||
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;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
let w = rsc.ui_mut().widgets.add_weak(self);
|
||||
rsc.on_add(w);
|
||||
w
|
||||
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
|
||||
state.as_state().get_mut().widgets.add_weak(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FnTag;
|
||||
impl<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F {
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
self(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetFnTrait<Rsc> {
|
||||
type Widget: Widget;
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget;
|
||||
}
|
||||
pub struct FnTraitTag;
|
||||
impl<Rsc: UiRsc, T: WidgetFnTrait<Rsc>> WidgetLike<Rsc, FnTraitTag> for T {
|
||||
type Widget = T::Widget;
|
||||
#[track_caller]
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<T::Widget> {
|
||||
self.run(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefTag;
|
||||
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>> WidgetLike<Rsc, RefTag>
|
||||
for WeakWidget<W>
|
||||
impl<State: HasUi + StateLike<State>, W: Widget, F: FnOnce(&mut State) -> W>
|
||||
WidgetLike<State, FnTag> for F
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefFnTag;
|
||||
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>, F: FnOnce(&mut Rsc) -> WeakWidget<W>>
|
||||
WidgetLike<Rsc, RefFnTag> for F
|
||||
pub struct IdFnTag;
|
||||
impl<
|
||||
State: HasUi + StateLike<State>,
|
||||
W: ?Sized + Widget + Unsize<dyn Widget>,
|
||||
F: FnOnce(&mut State) -> WidgetRef<W>,
|
||||
> WidgetLike<State, IdFnTag> for F
|
||||
{
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
self(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ViewTag;
|
||||
impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
|
||||
type Widget = V::Widget;
|
||||
fn add(self, _: &mut Rsc) -> WeakWidget<Self::Widget> {
|
||||
self.root()
|
||||
fn add(self, state: &mut impl StateLike<State>) -> WidgetRef<W> {
|
||||
self(state.as_state())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::{
|
||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||
IdLike, Widget, WidgetData, WidgetHandle, WidgetId, WidgetRef,
|
||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||
};
|
||||
|
||||
@@ -9,19 +9,16 @@ pub struct Widgets {
|
||||
pub needs_redraw: HashSet<WidgetId>,
|
||||
vec: SlotVec<WidgetData>,
|
||||
send: Sender<WidgetId>,
|
||||
recv: Receiver<WidgetId>,
|
||||
pub(crate) waiting: HashSet<WidgetId>,
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
pub fn new() -> Self {
|
||||
let (send, recv) = channel();
|
||||
pub fn new(send: Sender<WidgetId>) -> Self {
|
||||
Self {
|
||||
needs_redraw: Default::default(),
|
||||
vec: Default::default(),
|
||||
waiting: Default::default(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,18 +26,9 @@ impl Widgets {
|
||||
!self.needs_redraw.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||
Some(self.vec.get(id)?.widget.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_dyn_mut(&mut self, id: WidgetId) -> Option<&mut dyn Widget> {
|
||||
self.needs_redraw.insert(id);
|
||||
Some(self.vec.get_mut(id)?.widget.as_mut())
|
||||
}
|
||||
|
||||
/// get_dyn but dynamic borrow checking of widgets
|
||||
/// lets you do recursive (tree) operations, like the painter does
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
pub(crate) fn get_dyn<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||
// done through the borrow variable
|
||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||
@@ -54,29 +42,41 @@ impl Widgets {
|
||||
where
|
||||
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>
|
||||
where
|
||||
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));
|
||||
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));
|
||||
self.waiting.insert(id);
|
||||
WeakWidget::new(id)
|
||||
WidgetRef::new(id)
|
||||
}
|
||||
|
||||
#[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()) {
|
||||
let label = self.label(rf);
|
||||
let id = rf.id();
|
||||
@@ -84,7 +84,7 @@ impl Widgets {
|
||||
"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> {
|
||||
@@ -95,19 +95,14 @@ impl Widgets {
|
||||
&self.data(id.id()).unwrap().label
|
||||
}
|
||||
|
||||
/// useful for debugging
|
||||
pub fn set_label(&mut self, id: impl IdLike, label: String) {
|
||||
self.data_mut(id.id()).unwrap().label = label;
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
||||
self.vec.get_mut(id.id())
|
||||
}
|
||||
|
||||
pub fn free_next(&mut self) -> Option<WidgetId> {
|
||||
let next = self.recv.try_recv().ok()?;
|
||||
self.vec.free(next);
|
||||
Some(next)
|
||||
pub fn delete(&mut self, id: impl IdLike) {
|
||||
self.vec.free(id.id());
|
||||
// not sure if there's any point in this
|
||||
// self.updates.remove(&id);
|
||||
}
|
||||
|
||||
#[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>;
|
||||
|
||||
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
@@ -1,21 +1,20 @@
|
||||
use iris::prelude::*;
|
||||
|
||||
fn main() {
|
||||
DefaultApp::<State>::run();
|
||||
App::<State>::run();
|
||||
}
|
||||
|
||||
#[derive(DefaultUiState)]
|
||||
struct State {
|
||||
ui_state: DefaultUiState,
|
||||
}
|
||||
#[default_ui_state]
|
||||
struct State {}
|
||||
|
||||
impl DefaultAppState for State {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||
Self { ui_state }
|
||||
fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
|
||||
let mut ui = Ui::new();
|
||||
rect(Color::RED).set_root(&mut ui);
|
||||
Self {
|
||||
ui,
|
||||
ui_state,
|
||||
events: EventManager::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
+81
-85
@@ -2,8 +2,8 @@ extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
|
||||
Token, Type, Visibility,
|
||||
Attribute, Block, Error, Fields, FieldsNamed, GenericParam, Generics, Ident, ItemStruct,
|
||||
ItemTrait, Meta, Signature, Token, Visibility,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input, parse_quote,
|
||||
spanned::Spanned,
|
||||
@@ -89,108 +89,104 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
||||
quote! {
|
||||
#trai
|
||||
|
||||
impl #generics #name<Rsc, WL, Tag> for WL {
|
||||
impl #generics #name<State, WL, Tag> for WL {
|
||||
#(#impls)*
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))]
|
||||
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
|
||||
#[proc_macro_derive(UiState, attributes(rsc))]
|
||||
pub fn derive_ui_state(input: TokenStream) -> TokenStream {
|
||||
let mut output = proc_macro2::TokenStream::new();
|
||||
|
||||
let state: ItemStruct = parse_macro_input!(input);
|
||||
let sname = state.ident;
|
||||
let rscname = Ident::new(&(sname.to_string() + "Rsc"), sname.span());
|
||||
let mut rsc_fields = Vec::new();
|
||||
|
||||
let mut found_attr = false;
|
||||
let mut state_field = None;
|
||||
for field in &state.fields {
|
||||
if !found_attr
|
||||
&& let Type::Path(path) = &field.ty
|
||||
&& path.path.is_ident("DefaultUiState")
|
||||
{
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(attr) = field
|
||||
.attrs
|
||||
.iter()
|
||||
.find(|a| a.path().is_ident("default_ui_state"))
|
||||
else {
|
||||
for field in state.fields {
|
||||
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("rsc")) else {
|
||||
continue;
|
||||
};
|
||||
if found_attr {
|
||||
output.extend(
|
||||
Error::new(
|
||||
attr.span(),
|
||||
"cannot have more than one default_ui_state attribute",
|
||||
)
|
||||
.into_compile_error(),
|
||||
);
|
||||
let Meta::List(list) = &attr.meta else {
|
||||
output.extend(Error::new(attr.span(), "invalid attr syntax").into_compile_error());
|
||||
continue;
|
||||
}
|
||||
found_attr = true;
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(field) = state_field else {
|
||||
output.extend(
|
||||
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(),
|
||||
);
|
||||
return output.into();
|
||||
};
|
||||
let sname = &state.ident;
|
||||
let fname = field.ident.as_ref().unwrap();
|
||||
output.extend(quote! {
|
||||
impl iris::default::HasDefaultUiState for #sname {
|
||||
fn default_state(&self) -> &iris::default::DefaultUiState {
|
||||
&self.#fname
|
||||
};
|
||||
let tname: Ident = match list.parse_args::<Ident>() {
|
||||
Ok(ident) => ident,
|
||||
Err(err) => {
|
||||
output.extend(err.to_compile_error());
|
||||
continue;
|
||||
}
|
||||
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState {
|
||||
&mut self.#fname
|
||||
};
|
||||
let fty = &field.ty;
|
||||
let fname = &field.ident.unwrap();
|
||||
rsc_fields.extend(quote! {#fname: #fty,});
|
||||
output.extend(quote! {
|
||||
impl #tname for #sname {
|
||||
fn get(&self) -> &#fty {
|
||||
&self.#fname
|
||||
}
|
||||
fn get_mut(&mut self) -> &mut #fty {
|
||||
&mut self.#fname
|
||||
}
|
||||
}
|
||||
impl #tname for #rscname {
|
||||
fn get(&self) -> &#fty {
|
||||
&self.#fname
|
||||
}
|
||||
fn get_mut(&mut self) -> &mut #fty {
|
||||
&mut self.#fname
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let vis = state.vis;
|
||||
output.extend(quote! {
|
||||
#vis struct #rscname {
|
||||
#(#rsc_fields)*
|
||||
}
|
||||
|
||||
impl HasState for #sname {
|
||||
type State = #sname;
|
||||
}
|
||||
|
||||
impl HasState for #rscname {
|
||||
type State = #sname;
|
||||
}
|
||||
|
||||
impl StateLike<#sname> for #sname {
|
||||
fn as_state(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StateLike<#rscname> for #rscname {
|
||||
fn as_state(&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
});
|
||||
output.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(WidgetView, attributes(root))]
|
||||
pub fn derive_widget_view(input: TokenStream) -> TokenStream {
|
||||
let mut output = proc_macro2::TokenStream::new();
|
||||
|
||||
let state: ItemStruct = parse_macro_input!(input);
|
||||
|
||||
let mut found_attr = false;
|
||||
let mut state_field = None;
|
||||
for field in &state.fields {
|
||||
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("root")) else {
|
||||
continue;
|
||||
};
|
||||
if found_attr {
|
||||
output.extend(
|
||||
Error::new(attr.span(), "cannot have more than one root widget")
|
||||
.into_compile_error(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
found_attr = true;
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(field) = state_field else {
|
||||
output.extend(
|
||||
Error::new(state.ident.span(), "no root widget field found (#[root])")
|
||||
.into_compile_error(),
|
||||
);
|
||||
return output.into();
|
||||
#[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 sname = &state.ident;
|
||||
let fname = field.ident.as_ref().unwrap();
|
||||
let fty = &field.ty;
|
||||
output.extend(quote! {
|
||||
impl iris::core::WidgetView for #sname {
|
||||
type Widget = <#fty as iris::core::HasWidget>::Widget;
|
||||
fn root(&self) -> #fty {
|
||||
self.#fname
|
||||
}
|
||||
}
|
||||
});
|
||||
output.into()
|
||||
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()
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
1. does what I want it to (text, images, video, animations)
|
||||
2. very easy to use ignoring ergonomic ref counting
|
||||
3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem)
|
||||
3. reasonably fast / efficient (a lot faster than electron, save battery life)
|
||||
|
||||
## dev details
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["clippy", "rustfmt"]
|
||||
File renamed without changes.
@@ -1,25 +1,26 @@
|
||||
use cosmic_text::Family;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
use iris::prelude::*;
|
||||
type ClientRsc = DefaultRsc<Client>;
|
||||
iris::state_prelude!(ClientRsc);
|
||||
|
||||
fn main() {
|
||||
DefaultApp::<Client>::run();
|
||||
App::<Client>::run();
|
||||
}
|
||||
|
||||
#[derive(DefaultUiState)]
|
||||
#[default_ui_state]
|
||||
pub struct Client {
|
||||
ui_state: DefaultUiState,
|
||||
info: WeakWidget<Text>,
|
||||
info: WidgetRef<Text>,
|
||||
}
|
||||
|
||||
impl DefaultAppState for Client {
|
||||
fn new(
|
||||
mut ui_state: DefaultUiState,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_: Proxy<Self::Event>,
|
||||
) -> Self {
|
||||
fn new(ui_state: DefaultUiState, _proxy: Proxy<Self::Event>) -> Self {
|
||||
let mut rsc = ClientRsc {
|
||||
ui: Ui::new(),
|
||||
ui_state,
|
||||
events: EventManager::default(),
|
||||
};
|
||||
|
||||
let rrect = rect(Color::WHITE).radius(20);
|
||||
let pad_test = (
|
||||
rrect.color(Color::BLUE),
|
||||
@@ -42,7 +43,7 @@ impl DefaultAppState for Client {
|
||||
.width(rest(3)),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.add(rsc);
|
||||
.add(&mut rsc);
|
||||
|
||||
let span_test = (
|
||||
rrect.color(Color::GREEN).width(100),
|
||||
@@ -53,30 +54,30 @@ impl DefaultAppState for Client {
|
||||
rrect.color(Color::RED).width(100),
|
||||
)
|
||||
.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)
|
||||
.radius(30)
|
||||
.on(CursorSense::click(), move |_, rsc| {
|
||||
.on(CursorSense::click(), move |ctx| {
|
||||
let child = image(include_bytes!("assets/sungals.png"))
|
||||
.center()
|
||||
.add_strong(rsc);
|
||||
span_add(rsc).push(child);
|
||||
.add_strong(ctx);
|
||||
span_add(ctx).push(child);
|
||||
})
|
||||
.sized((150, 150))
|
||||
.align(Align::BOT_RIGHT);
|
||||
|
||||
let del_button = rect(Color::RED)
|
||||
.radius(30)
|
||||
.on(CursorSense::click(), move |_, rsc| {
|
||||
span_add(rsc).pop();
|
||||
.on(CursorSense::click(), move |ctx| {
|
||||
span_add(ctx).pop();
|
||||
})
|
||||
.sized((150, 150))
|
||||
.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);
|
||||
|
||||
@@ -99,18 +100,18 @@ impl DefaultAppState for Client {
|
||||
wtext("pretty cool right?").size(50),
|
||||
)
|
||||
.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 add_text = wtext("add")
|
||||
.editable(EditMode::MultiLine)
|
||||
.text_align(Align::LEFT)
|
||||
.size(30)
|
||||
.attr::<Selectable>(())
|
||||
.on(Submit, move |ctx, rsc| {
|
||||
.on(Submit, move |ctx| {
|
||||
let w = ctx.widget;
|
||||
let content = w.edit(rsc).take();
|
||||
let content = w.edit(ctx).take();
|
||||
let text = wtext(content)
|
||||
.editable(EditMode::MultiLine)
|
||||
.size(30)
|
||||
@@ -119,11 +120,10 @@ impl DefaultAppState for Client {
|
||||
.attr::<Selectable>(());
|
||||
let msg_box = text
|
||||
.background(rect(Color::WHITE.darker(0.5)))
|
||||
.add_strong(rsc);
|
||||
texts(rsc).push(msg_box);
|
||||
.add_strong(ctx);
|
||||
texts(ctx).push(msg_box);
|
||||
})
|
||||
.add(rsc);
|
||||
|
||||
.add(&mut rsc);
|
||||
let text_edit_scroll = (
|
||||
msg_area.height(rest(1)),
|
||||
(
|
||||
@@ -131,8 +131,8 @@ impl DefaultAppState for Client {
|
||||
(
|
||||
add_text.width(rest(1)),
|
||||
Rect::new(Color::GREEN)
|
||||
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||
.on(CursorSense::click(), move |ctx| {
|
||||
ctx.state.run_event::<Submit>(add_text, &mut ());
|
||||
})
|
||||
.sized((40, 40)),
|
||||
)
|
||||
@@ -145,39 +145,39 @@ impl DefaultAppState for Client {
|
||||
.align(Align::BOT),
|
||||
)
|
||||
.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 mut switch_button = |color, to: WeakWidget, label| {
|
||||
let to = to.upgrade(rsc);
|
||||
let mut switch_button = |color, to: WidgetRef, label| {
|
||||
let to = to.upgrade(&mut rsc);
|
||||
let vec = &mut vals.borrow_mut().1;
|
||||
let i = vec.len();
|
||||
if vec.is_empty() {
|
||||
vec.push(None);
|
||||
main(rsc).set(to);
|
||||
rsc.ui[main].set(to);
|
||||
} else {
|
||||
vec.push(Some(to));
|
||||
}
|
||||
let vals = vals.clone();
|
||||
let rect = rect(color)
|
||||
.on(CursorSense::click(), move |ctx, rsc| {
|
||||
.on(CursorSense::click(), move |ctx| {
|
||||
let (prev, vec) = &mut *vals.borrow_mut();
|
||||
if let Some(h) = vec[i].take() {
|
||||
vec[*prev] = main(rsc).replace(h);
|
||||
vec[*prev] = main(ctx).replace(h);
|
||||
*prev = i;
|
||||
}
|
||||
ctx.widget(rsc).color = color.darker(0.3);
|
||||
ctx.widget().color = color.darker(0.3);
|
||||
})
|
||||
.on(
|
||||
CursorSense::HoverStart | CursorSense::unclick(),
|
||||
move |ctx, rsc| {
|
||||
ctx.widget(rsc).color = color.brighter(0.2);
|
||||
move |ctx| {
|
||||
ctx.widget().color = color.brighter(0.2);
|
||||
},
|
||||
)
|
||||
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
||||
ctx.widget(rsc).color = color;
|
||||
.on(CursorSense::HoverEnd, move |ctx| {
|
||||
ctx.widget().color = color;
|
||||
});
|
||||
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
||||
};
|
||||
@@ -195,30 +195,30 @@ impl DefaultAppState for Client {
|
||||
)
|
||||
.span(Dir::RIGHT);
|
||||
|
||||
let info = wtext("").add(rsc);
|
||||
let info = wtext("").add(&mut rsc);
|
||||
let info_sect = info.pad(10).align(Align::RIGHT);
|
||||
|
||||
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
|
||||
.stack()
|
||||
.set_root(rsc, &mut ui_state);
|
||||
.set_root(&mut rsc);
|
||||
|
||||
Self { ui_state, info }
|
||||
Self {
|
||||
ui: rsc.ui,
|
||||
ui_state: rsc.ui_state,
|
||||
events: rsc.events,
|
||||
info,
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_: WindowEvent,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
fn window_event(&mut self, _: WindowEvent) {
|
||||
let new = format!(
|
||||
"widgets: {}\nactive: {}\nviews: {}",
|
||||
rsc.widgets().len(),
|
||||
render.active_widgets(),
|
||||
self.ui_state.renderer.ui.view_count(),
|
||||
"widgets: {}\nactive:{}\nviews: {}",
|
||||
self.ui.num_widgets(),
|
||||
self.ui.active_widgets(),
|
||||
self.ui_state.renderer.ui.view_count()
|
||||
);
|
||||
if new != *rsc.widgets()[self.info].content {
|
||||
*rsc.widgets_mut()[self.info].content = new;
|
||||
if new != *self.ui[self.info].content {
|
||||
*self.ui[self.info].content = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,6 @@ pub trait AppState {
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
|
||||
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
|
||||
fn exit(&mut self);
|
||||
|
||||
fn run()
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
App::<Self>::run();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App<State: AppState> {
|
||||
|
||||
+16
-28
@@ -4,45 +4,35 @@ use winit::dpi::{LogicalPosition, LogicalSize};
|
||||
|
||||
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
|
||||
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) {
|
||||
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| {
|
||||
let region = ctx.data.render.window_region(&id).unwrap();
|
||||
fn run(state: &mut State, container: WidgetRef<W>, id: Self::Input) {
|
||||
state.register_event(container, CursorSense::click_or_drag(), move |ctx| {
|
||||
let region = ctx.ui().window_region(&id).unwrap();
|
||||
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 size = region.size();
|
||||
select(
|
||||
rsc,
|
||||
ctx.data.render,
|
||||
ctx.state,
|
||||
id,
|
||||
pos,
|
||||
size,
|
||||
ctx.data.sense.is_dragging(),
|
||||
);
|
||||
select(ctx.state, id, pos, size, ctx.data.sense.is_dragging());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Selectable;
|
||||
|
||||
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
|
||||
impl<State: HasEvents> WidgetAttr<State, TextEdit> for Selectable
|
||||
where
|
||||
Rsc::State: HasDefaultUiState,
|
||||
State::State: HasDefaultUiState,
|
||||
{
|
||||
type Input = ();
|
||||
|
||||
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) {
|
||||
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| {
|
||||
fn run(state: &mut State, id: WidgetRef<TextEdit>, _: Self::Input) {
|
||||
state.register_event(id, CursorSense::click_or_drag(), move |ctx| {
|
||||
select(
|
||||
rsc,
|
||||
ctx.data.render,
|
||||
ctx.state,
|
||||
id,
|
||||
ctx.data.pos,
|
||||
@@ -54,20 +44,18 @@ where
|
||||
}
|
||||
|
||||
fn select(
|
||||
rsc: &mut impl UiRsc,
|
||||
render: &UiRenderState,
|
||||
state: &mut impl HasDefaultUiState,
|
||||
id: WeakWidget<TextEdit>,
|
||||
id: WidgetRef<TextEdit>,
|
||||
pos: Vec2,
|
||||
size: Vec2,
|
||||
dragging: bool,
|
||||
) {
|
||||
let state = state.default_state_mut();
|
||||
let (ui, state) = HasDefaultUiState::ui_with_state(state);
|
||||
let now = Instant::now();
|
||||
let recent = (now - state.last_click) < Duration::from_millis(300);
|
||||
state.last_click = now;
|
||||
id.edit(rsc).select(pos, size, dragging, recent);
|
||||
if let Some(region) = render.window_region(&id) {
|
||||
id.edit(ui).select(pos, size, dragging, recent);
|
||||
if let Some(region) = ui.window_region(&id) {
|
||||
state.window.set_ime_allowed(true);
|
||||
state.window.set_ime_cursor_area(
|
||||
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
||||
|
||||
+46
-214
@@ -1,10 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use arboard::Clipboard;
|
||||
use std::{
|
||||
marker::{PhantomData, Sized},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use iris_core::util::forget_mut;
|
||||
use std::{marker::Sized, sync::Arc, time::Instant};
|
||||
use winit::{
|
||||
event::{Ime, WindowEvent},
|
||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||
@@ -17,8 +14,6 @@ mod event;
|
||||
mod input;
|
||||
mod render;
|
||||
mod sense;
|
||||
mod state;
|
||||
mod task;
|
||||
|
||||
pub use app::*;
|
||||
pub use attr::*;
|
||||
@@ -26,33 +21,23 @@ pub use event::*;
|
||||
pub use input::*;
|
||||
pub use render::*;
|
||||
pub use sense::*;
|
||||
pub use state::*;
|
||||
pub use task::*;
|
||||
|
||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
||||
|
||||
pub struct DefaultUiState {
|
||||
pub root: Option<StrongWidget>,
|
||||
pub renderer: UiRenderer,
|
||||
pub input: Input,
|
||||
pub focus: Option<WeakWidget<TextEdit>>,
|
||||
pub focus: Option<WidgetRef<TextEdit>>,
|
||||
pub clipboard: Clipboard,
|
||||
pub window: Arc<Window>,
|
||||
pub ime: usize,
|
||||
pub last_click: Instant,
|
||||
}
|
||||
|
||||
impl HasRoot for DefaultUiState {
|
||||
fn set_root(&mut self, root: StrongWidget) {
|
||||
self.root = Some(root);
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultUiState {
|
||||
pub fn new(window: impl Into<Arc<Window>>) -> Self {
|
||||
let window = window.into();
|
||||
Self {
|
||||
root: None,
|
||||
renderer: UiRenderer::new(window.clone()),
|
||||
window,
|
||||
input: Input::default(),
|
||||
@@ -64,162 +49,53 @@ impl DefaultUiState {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasDefaultUiState: Sized + 'static {
|
||||
fn default_state(&self) -> &DefaultUiState;
|
||||
fn default_state_mut(&mut self) -> &mut DefaultUiState;
|
||||
pub trait HasDefaultUiState: Sized + 'static + HasUi {
|
||||
fn get(&self) -> &DefaultUiState;
|
||||
fn get_mut(&mut self) -> &mut DefaultUiState;
|
||||
fn ui_state(&self) -> &DefaultUiState {
|
||||
HasDefaultUiState::get(self)
|
||||
}
|
||||
fn ui_state_mut(&mut self) -> &mut DefaultUiState {
|
||||
HasDefaultUiState::get_mut(self)
|
||||
}
|
||||
fn ui_with_state(&mut self) -> (&mut Ui, &mut DefaultUiState) {
|
||||
// as long as you're not doing anything actually unhinged this should always work safely
|
||||
(
|
||||
unsafe { forget_mut(self.ui_mut()) },
|
||||
HasDefaultUiState::get_mut(self),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DefaultAppState: HasDefaultUiState {
|
||||
type Event = ();
|
||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
|
||||
-> Self;
|
||||
pub trait DefaultAppState: RunEvents + HasDefaultUiState {
|
||||
type Event: 'static = ();
|
||||
fn new(ui_state: DefaultUiState, proxy: Proxy<Self::Event>) -> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
}
|
||||
fn event(&mut self, event: Self::Event) {}
|
||||
#[allow(unused_variables)]
|
||||
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {}
|
||||
fn exit(&mut self) {}
|
||||
#[allow(unused_variables)]
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event: WindowEvent,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
}
|
||||
fn window_event(&mut self, event: WindowEvent) {}
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultRsc<State: 'static> {
|
||||
pub ui: UiData,
|
||||
pub events: EventManager<Self>,
|
||||
pub tasks: Tasks<Self>,
|
||||
pub state: WidgetState,
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
impl<State> DefaultRsc<State> {
|
||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, recv) = Tasks::init(window);
|
||||
(
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
events: Default::default(),
|
||||
tasks,
|
||||
state: Default::default(),
|
||||
_state: Default::default(),
|
||||
},
|
||||
recv,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
||||
self.state.add(id.id(), data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<State> UiRsc for DefaultRsc<State> {
|
||||
fn ui(&self) -> &UiData {
|
||||
&self.ui
|
||||
}
|
||||
|
||||
fn ui_mut(&mut self) -> &mut UiData {
|
||||
&mut self.ui
|
||||
}
|
||||
|
||||
fn on_draw(&mut self, active: &ActiveData) {
|
||||
self.events.draw(active);
|
||||
}
|
||||
|
||||
fn on_undraw(&mut self, active: &ActiveData) {
|
||||
self.events.undraw(active);
|
||||
}
|
||||
|
||||
fn on_remove(&mut self, id: WidgetId) {
|
||||
self.events.remove(id);
|
||||
self.state.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasState for DefaultRsc<State> {
|
||||
type State = State;
|
||||
}
|
||||
|
||||
impl<State: 'static> HasEvents for DefaultRsc<State> {
|
||||
fn events(&self) -> &EventManager<Self> {
|
||||
&self.events
|
||||
}
|
||||
|
||||
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||
&mut self.events
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasTasks for DefaultRsc<State> {
|
||||
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
|
||||
&mut self.tasks
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
|
||||
fn widget_state(&self) -> &WidgetState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
fn widget_state_mut(&mut self) -> &mut WidgetState {
|
||||
&mut self.state
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DefaultApp<State: DefaultAppState> {
|
||||
rsc: DefaultRsc<State>,
|
||||
render: UiRenderState,
|
||||
state: State,
|
||||
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
|
||||
}
|
||||
|
||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
impl<State: DefaultAppState> AppState for State {
|
||||
type Event = State::Event;
|
||||
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||
let window = event_loop
|
||||
.create_window(State::window_attributes())
|
||||
.unwrap();
|
||||
let default_state = DefaultUiState::new(window);
|
||||
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
|
||||
let state = State::new(default_state, &mut rsc, proxy);
|
||||
let render = UiRenderState::new();
|
||||
Self {
|
||||
rsc,
|
||||
state,
|
||||
render,
|
||||
task_recv,
|
||||
}
|
||||
let window = event_loop.create_window(Self::window_attributes()).unwrap();
|
||||
Self::new(DefaultUiState::new(window), proxy)
|
||||
}
|
||||
|
||||
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) {
|
||||
let Self {
|
||||
rsc,
|
||||
render,
|
||||
state,
|
||||
task_recv,
|
||||
} = self;
|
||||
|
||||
for update in task_recv.try_iter() {
|
||||
update(state, rsc);
|
||||
}
|
||||
|
||||
let ui_state = state.default_state_mut();
|
||||
let events = unsafe { forget_mut(self.events_mut()) };
|
||||
let ui_state = HasDefaultUiState::get_mut(self);
|
||||
let input_changed = ui_state.input.event(&event);
|
||||
let cursor_state = ui_state.cursor_state().clone();
|
||||
let old = ui_state.focus;
|
||||
@@ -228,43 +104,43 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
if input_changed {
|
||||
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
|
||||
&& let Some(old) = old
|
||||
{
|
||||
old.edit(rsc).deselect();
|
||||
old.edit(ui).deselect();
|
||||
}
|
||||
match &event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
WindowEvent::RedrawRequested => {
|
||||
render.update(&ui_state.root, rsc);
|
||||
ui_state.renderer.update(&mut rsc.ui, render);
|
||||
ui.update(events);
|
||||
ui_state.renderer.update(ui);
|
||||
ui_state.renderer.draw();
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
render.resize((size.width, size.height));
|
||||
ui.resize((size.width, size.height));
|
||||
ui_state.renderer.resize(size)
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let Some(sel) = ui_state.focus
|
||||
&& event.state.is_pressed()
|
||||
{
|
||||
let mut text = sel.edit(rsc);
|
||||
let mut text = sel.edit(ui);
|
||||
match text.apply_event(event, &ui_state.input.modifiers) {
|
||||
TextInputResult::Unfocus => {
|
||||
ui_state.focus = None;
|
||||
ui_state.window.set_ime_allowed(false);
|
||||
}
|
||||
TextInputResult::Submit => {
|
||||
rsc.run_event::<Submit>(sel, (), state);
|
||||
self.run_event::<Submit>(sel, &mut ());
|
||||
}
|
||||
TextInputResult::Paste => {
|
||||
if let Ok(t) = ui_state.clipboard.get_text() {
|
||||
text.insert(&t);
|
||||
}
|
||||
rsc.run_event::<Edited>(sel, (), state);
|
||||
self.run_event::<Edited>(sel, &mut ());
|
||||
}
|
||||
TextInputResult::Copy(text) => {
|
||||
if let Err(err) = ui_state.clipboard.set_text(text) {
|
||||
@@ -272,7 +148,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
}
|
||||
TextInputResult::Used => {
|
||||
rsc.run_event::<Edited>(sel, (), state);
|
||||
self.run_event::<Edited>(sel, &mut ());
|
||||
}
|
||||
TextInputResult::Unused => {}
|
||||
}
|
||||
@@ -280,7 +156,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
WindowEvent::Ime(ime) => {
|
||||
if let Some(sel) = ui_state.focus {
|
||||
let mut text = sel.edit(rsc);
|
||||
let mut text = sel.edit(ui);
|
||||
match ime {
|
||||
Ime::Enabled | Ime::Disabled => (),
|
||||
Ime::Preedit(content, _pos) => {
|
||||
@@ -296,59 +172,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
state.window_event(event, rsc, render);
|
||||
let ui_state = self.state.default_state_mut();
|
||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
||||
self.window_event(event);
|
||||
(ui, ui_state) = self.ui_with_state();
|
||||
if ui.needs_redraw() {
|
||||
ui_state.renderer.window().request_redraw();
|
||||
}
|
||||
ui_state.input.end_frame();
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.state.exit(&mut self.rsc, &mut self.render);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RscIdx<Rsc> {
|
||||
type Output;
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
|
||||
}
|
||||
|
||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
|
||||
type Output = I::Output;
|
||||
|
||||
fn index(&self, index: I) -> &Self::Output {
|
||||
index.get(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
|
||||
fn index_mut(&mut self, index: I) -> &mut Self::Output {
|
||||
index.get_mut(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
|
||||
type Output = W;
|
||||
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output {
|
||||
&rsc.ui().widgets[self]
|
||||
}
|
||||
|
||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
||||
&mut rsc.ui_mut().widgets[self]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
|
||||
type Output = T;
|
||||
|
||||
fn get(self, rsc: &Rsc) -> &Self::Output {
|
||||
rsc.widget_state().get(self)
|
||||
}
|
||||
|
||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
||||
rsc.widget_state_mut().get_mut(self)
|
||||
self.exit();
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -1,7 +1,7 @@
|
||||
use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState};
|
||||
use iris_core::{Ui, UiLimits, UiRenderNode};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use wgpu::*;
|
||||
use wgpu::{util::StagingBelt, *};
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
@@ -13,12 +13,13 @@ pub struct UiRenderer {
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
encoder: CommandEncoder,
|
||||
staging_belt: StagingBelt,
|
||||
pub ui: UiRenderNode,
|
||||
}
|
||||
|
||||
impl UiRenderer {
|
||||
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) {
|
||||
self.ui.update(&self.device, &self.queue, ui, render);
|
||||
pub fn update(&mut self, ui: &mut Ui) {
|
||||
self.ui.update(&self.device, &self.queue, ui);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
@@ -45,15 +46,16 @@ impl UiRenderer {
|
||||
}
|
||||
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.staging_belt.finish();
|
||||
output.present();
|
||||
self.staging_belt.recall();
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
self.config.width = size.width;
|
||||
self.config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.ui.resize((size.width, size.height), &self.queue);
|
||||
self.ui.resize(size, &self.queue);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
@@ -116,7 +118,7 @@ impl UiRenderer {
|
||||
format: surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
present_mode: PresentMode::AutoNoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
@@ -124,9 +126,10 @@ impl UiRenderer {
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let staging_belt = StagingBelt::new(4096 * 4);
|
||||
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 {
|
||||
surface,
|
||||
@@ -134,7 +137,8 @@ impl UiRenderer {
|
||||
queue,
|
||||
config,
|
||||
encoder,
|
||||
ui,
|
||||
staging_belt,
|
||||
ui: shape_pipeline,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
+18
-38
@@ -28,13 +28,12 @@ pub struct CursorSenses(Vec<CursorSense>);
|
||||
impl Event for CursorSenses {
|
||||
type Data<'a> = CursorData<'a>;
|
||||
type State = SensorState;
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
if let Some(sense) = should_run(self, &data.cursor, data.hover) {
|
||||
let mut data = data.clone();
|
||||
fn should_run(&self, data: &mut Self::Data<'_>) -> bool {
|
||||
if let Some(sense) = should_run(self, data.cursor, data.hover) {
|
||||
data.sense = sense;
|
||||
Some(data)
|
||||
true
|
||||
} else {
|
||||
None
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +115,7 @@ pub enum ActivationState {
|
||||
/// or basically have some way to have custom senses
|
||||
/// that depend on active widget positions
|
||||
/// but I'm not sure how or if worth it
|
||||
pub struct Sensor<Ctx: HasEvents, Data> {
|
||||
pub struct Sensor<Ctx, Data> {
|
||||
pub senses: CursorSenses,
|
||||
pub f: Rc<dyn EventFn<Ctx, Data>>,
|
||||
}
|
||||
@@ -128,46 +127,29 @@ pub struct SensorState {
|
||||
pub hover: ActivationState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CursorData<'a> {
|
||||
/// where this widget was hit
|
||||
pub pos: Vec2,
|
||||
pub size: Vec2,
|
||||
pub scroll_delta: Vec2,
|
||||
pub hover: ActivationState,
|
||||
pub cursor: CursorState,
|
||||
pub cursor: &'a CursorState,
|
||||
/// the first sense that triggered this
|
||||
pub sense: CursorSense,
|
||||
pub render: &'a UiRenderState,
|
||||
}
|
||||
|
||||
pub trait SensorUi {
|
||||
fn run_sensors<Rsc: HasEvents>(
|
||||
&self,
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
window_size: Vec2,
|
||||
);
|
||||
pub trait SensorUi<State> {
|
||||
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2);
|
||||
}
|
||||
|
||||
impl SensorUi for UiRenderState {
|
||||
fn run_sensors<Rsc: HasEvents>(
|
||||
&self,
|
||||
rsc: &mut Rsc,
|
||||
state: &mut Rsc::State,
|
||||
cursor: CursorState,
|
||||
window_size: Vec2,
|
||||
) {
|
||||
// in order to remove this take, need to store active list in UiRenderState somehow
|
||||
// this would probably be done through a generic parameter that adds yet another rsc /
|
||||
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
||||
// update it there?
|
||||
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||
for layer in self.layers.indices().rev() {
|
||||
impl<State: RunEvents> SensorUi<State> for State {
|
||||
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) {
|
||||
let layers = std::mem::take(&mut self.ui_mut().layers);
|
||||
let mut active = std::mem::take(&mut self.events_mut().get_type::<CursorSense>().active);
|
||||
for layer in layers.indices().rev() {
|
||||
let mut sensed = false;
|
||||
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 in_shape = cursor.exists && region.contains(cursor.pos);
|
||||
sensor.hover.update(in_shape);
|
||||
@@ -176,9 +158,7 @@ impl SensorUi for UiRenderState {
|
||||
}
|
||||
sensed = true;
|
||||
|
||||
let cursor = cursor.clone();
|
||||
|
||||
let data = CursorData {
|
||||
let mut data = CursorData {
|
||||
pos: cursor.pos - region.top_left,
|
||||
size: region.bot_right - region.top_left,
|
||||
scroll_delta: cursor.scroll_delta,
|
||||
@@ -187,15 +167,15 @@ impl SensorUi for UiRenderState {
|
||||
// this does not have any meaning;
|
||||
// might wanna set up Event to have a prepare stage
|
||||
sense: CursorSense::Hovering,
|
||||
render: self,
|
||||
};
|
||||
rsc.run_event::<CursorSense>(*id, data, state);
|
||||
self.run_event::<CursorSense>(*id, &mut data);
|
||||
}
|
||||
if sensed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rsc.events_mut().get_type::<CursorSense>().active = active;
|
||||
self.events_mut().get_type::<CursorSense>().active = active;
|
||||
self.ui_mut().layers = layers;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
-79
@@ -1,87 +1,25 @@
|
||||
use iris_core::*;
|
||||
use iris_macro::*;
|
||||
use std::sync::Arc;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::default::{TaskCtx, TaskUpdate, Tasks};
|
||||
|
||||
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
||||
fn on<E: EventLike>(
|
||||
self,
|
||||
event: E,
|
||||
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, Self::Widget>,
|
||||
) -> impl WidgetIdFn<Rsc, Self::Widget> {
|
||||
move |rsc| {
|
||||
let id = self.add(rsc);
|
||||
rsc.register_event(id, event.into_event(), move |ctx, rsc| {
|
||||
f(
|
||||
EventIdCtx {
|
||||
pub mod eventable {
|
||||
use super::*;
|
||||
widget_trait! {
|
||||
pub trait Eventable<State: HasEvents + StateLike<State> + 'static>;
|
||||
fn on<E: EventLike>(
|
||||
self,
|
||||
event: E,
|
||||
f: impl for<'a> WidgetEventFn<State::State, <E::Event as Event>::Data<'a>, WL::Widget>,
|
||||
) -> impl WidgetIdFn<State, WL::Widget> {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
state.register_event(id, event.into_event(), move |ctx| {
|
||||
f(&mut EventIdCtx {
|
||||
widget: id,
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
});
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
|
||||
|
||||
widget_trait! {
|
||||
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
||||
fn task_on<'a, E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
||||
self,
|
||||
event: E,
|
||||
f: F,
|
||||
) -> impl WidgetIdFn<Rsc, WL::Widget>
|
||||
where <E::Event as Event>::Data<'a>: Send,
|
||||
for<'b> F::CallRefFuture<'b>: Send,
|
||||
{
|
||||
let f = Arc::new(f);
|
||||
move |rsc| {
|
||||
let id = self.add(rsc);
|
||||
rsc.register_event(id, event.into_event(), move |_, rsc| {
|
||||
let f = f.clone();
|
||||
rsc.tasks_mut().spawn(async move |task| {
|
||||
f(AsyncEventIdCtx {
|
||||
widget: id,
|
||||
task,
|
||||
}).await;
|
||||
});
|
||||
});
|
||||
});
|
||||
id
|
||||
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
@@ -1,17 +1,31 @@
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(gen_blocks)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
#![feature(async_fn_traits)]
|
||||
|
||||
pub mod default;
|
||||
pub mod event;
|
||||
pub mod typed;
|
||||
pub mod widget;
|
||||
|
||||
pub use iris_core as core;
|
||||
pub use iris_macro as macros;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! state_prelude {
|
||||
($vis:vis $state:ty) => {
|
||||
iris::event_state!($vis $state);
|
||||
$vis use iris::{
|
||||
default::*,
|
||||
core::{len_fns::*, util::Vec2, *},
|
||||
macros::*,
|
||||
widget::*,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
pub use default::*;
|
||||
@@ -21,5 +35,5 @@ pub mod prelude {
|
||||
pub use widget::*;
|
||||
|
||||
pub use iris_core::util::Vec2;
|
||||
pub use len_fns::*;
|
||||
pub use typed::*;
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
move |state| Image {
|
||||
handle: state.ui_mut().textures.add(image),
|
||||
handle: state.get_mut().add_texture(image),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Masked {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
}
|
||||
|
||||
impl Widget for Masked {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Aligned {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
pub align: Align,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct LayerOffset {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct MaxSize {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
pub x: Option<Len>,
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Offset {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
pub amt: UiVec2,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||
|
||||
pub struct Pad {
|
||||
pub padding: Padding,
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
}
|
||||
|
||||
impl Widget for Pad {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Scroll {
|
||||
inner: StrongWidget,
|
||||
inner: WidgetHandle,
|
||||
axis: Axis,
|
||||
amt: f32,
|
||||
snap_end: bool,
|
||||
@@ -41,7 +41,7 @@ impl Widget for Scroll {
|
||||
}
|
||||
|
||||
impl Scroll {
|
||||
pub fn new(inner: StrongWidget, axis: Axis) -> Self {
|
||||
pub fn new(inner: WidgetHandle, axis: Axis) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
axis,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Sized {
|
||||
pub inner: StrongWidget,
|
||||
pub inner: WidgetHandle,
|
||||
pub x: Option<Len>,
|
||||
pub y: Option<Len>,
|
||||
}
|
||||
|
||||
+10
-39
@@ -2,18 +2,19 @@ use crate::prelude::*;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct Span {
|
||||
pub children: Vec<StrongWidget>,
|
||||
pub children: Vec<WidgetHandle>,
|
||||
pub dir: Dir,
|
||||
pub gap: f32,
|
||||
}
|
||||
|
||||
impl Widget for Span {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let total = self.len_sum(&mut painter.size_ctx());
|
||||
let total = self.len_sum();
|
||||
let mut start = UiScalar::rel_min();
|
||||
for child in &self.children {
|
||||
let mut span = UiSpan::FULL;
|
||||
span.start = start;
|
||||
painter.widget(id);
|
||||
let len = painter.len_axis(child, self.dir.axis);
|
||||
if len.rest > 0.0 {
|
||||
let offset = UiScalar::new(total.rel, total.abs);
|
||||
@@ -32,20 +33,6 @@ impl Widget for Span {
|
||||
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 {
|
||||
@@ -62,30 +49,14 @@ impl Span {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push(&mut self, w: StrongWidget) {
|
||||
pub fn push(&mut self, w: WidgetHandle) {
|
||||
self.children.push(w);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<StrongWidget> {
|
||||
pub fn pop(&mut self) -> Option<WidgetHandle> {
|
||||
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 {
|
||||
let len = self.len_sum(ctx);
|
||||
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)>,
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
|
||||
for SpanBuilder<Rsc, LEN, Wa, Tag>
|
||||
impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
WidgetFnTrait<State> for SpanBuilder<State, LEN, Wa, Tag>
|
||||
{
|
||||
type Widget = Span;
|
||||
|
||||
#[track_caller]
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget {
|
||||
fn run(self, state: &mut State) -> Self::Widget {
|
||||
Span {
|
||||
children: self.children.add(rsc).arr.into_iter().collect(),
|
||||
children: self.children.add(state).arr.into_iter().collect(),
|
||||
dir: self.dir,
|
||||
gap: self.gap,
|
||||
}
|
||||
@@ -193,7 +164,7 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Span {
|
||||
type Target = Vec<StrongWidget>;
|
||||
type Target = Vec<WidgetHandle>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.children
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Stack {
|
||||
pub children: Vec<StrongWidget>,
|
||||
pub children: Vec<WidgetHandle>,
|
||||
pub size: StackSize,
|
||||
}
|
||||
|
||||
@@ -48,15 +48,14 @@ pub struct StackBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, T
|
||||
_pd: PhantomData<(State, Tag)>,
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
|
||||
for StackBuilder<Rsc, LEN, Wa, Tag>
|
||||
impl<State: StateLike<State>, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> FnOnce<(&mut State,)>
|
||||
for StackBuilder<State, LEN, Wa, Tag>
|
||||
{
|
||||
type Widget = Stack;
|
||||
type Output = Stack;
|
||||
|
||||
#[track_caller]
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget {
|
||||
extern "rust-call" fn call_once(self, args: (&mut State,)) -> Self::Output {
|
||||
Stack {
|
||||
children: self.children.add(rsc).arr.into_iter().collect(),
|
||||
children: self.children.add(args.0).arr.into_iter().collect(),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||
use std::marker::{Sized, Unsize};
|
||||
|
||||
pub struct WidgetPtr {
|
||||
pub inner: Option<StrongWidget>,
|
||||
pub inner: Option<WidgetHandle>,
|
||||
}
|
||||
|
||||
impl Widget for WidgetPtr {
|
||||
@@ -38,14 +38,14 @@ impl WidgetPtr {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn replace<W: ?Sized + Unsize<dyn Widget>>(
|
||||
&mut self,
|
||||
to: StrongWidget<W>,
|
||||
) -> Option<StrongWidget> {
|
||||
to: WidgetHandle<W>,
|
||||
) -> Option<WidgetHandle> {
|
||||
self.inner.replace(to)
|
||||
}
|
||||
}
|
||||
|
||||
+29
-14
@@ -1,4 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
use cosmic_text::{Attrs, Family, Metrics};
|
||||
use std::marker::{PhantomData, Sized};
|
||||
|
||||
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
|
||||
}
|
||||
pub fn family(mut self, family: Family) -> Self {
|
||||
pub fn family(mut self, family: Family<'static>) -> Self {
|
||||
self.attrs.family = family;
|
||||
self
|
||||
}
|
||||
@@ -50,15 +51,15 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc, O> TextBuilder<Rsc, O> {
|
||||
pub fn hint<W: WidgetLike<Rsc, Tag>, Tag>(
|
||||
impl<State: HasUi + StateLike<State>, O> TextBuilder<State, O> {
|
||||
pub fn hint<W: WidgetLike<State, Tag>, Tag>(
|
||||
self,
|
||||
hint: W,
|
||||
) -> TextBuilder<Rsc, O, impl WidgetOption<Rsc>> {
|
||||
) -> TextBuilder<State, O, impl WidgetOption<State>> {
|
||||
TextBuilder {
|
||||
content: self.content,
|
||||
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,
|
||||
state: PhantomData,
|
||||
}
|
||||
@@ -74,20 +75,26 @@ pub trait TextBuilderOutput<State>: Sized {
|
||||
}
|
||||
|
||||
pub struct TextOutput;
|
||||
impl<Rsc: UiRsc> TextBuilderOutput<Rsc> for TextOutput {
|
||||
impl<State: HasUi> TextBuilderOutput<State> for TextOutput {
|
||||
type Output = Text;
|
||||
|
||||
fn run<H: WidgetOption<Rsc>>(
|
||||
state: &mut Rsc,
|
||||
builder: TextBuilder<Rsc, Self, H>,
|
||||
fn run<H: WidgetOption<State>>(
|
||||
state: &mut State,
|
||||
builder: TextBuilder<State, Self, H>,
|
||||
) -> 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 font_system = &mut state.get_mut().text.font_system;
|
||||
buf.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
|
||||
let mut text = Text {
|
||||
content: builder.content.into(),
|
||||
view: TextView::new(buf, builder.attrs, hint),
|
||||
};
|
||||
text.content.changed = false;
|
||||
builder.attrs.apply(font_system, &mut text.view.buf, None);
|
||||
text
|
||||
}
|
||||
}
|
||||
@@ -96,18 +103,26 @@ pub struct TextEditOutput {
|
||||
mode: EditMode,
|
||||
}
|
||||
|
||||
impl<State: UiRsc> TextBuilderOutput<State> for TextEditOutput {
|
||||
impl<State: HasUi> TextBuilderOutput<State> for TextEditOutput {
|
||||
type Output = TextEdit;
|
||||
|
||||
fn run<H: WidgetOption<State>>(
|
||||
state: &mut State,
|
||||
builder: TextBuilder<State, Self, H>,
|
||||
) -> Self::Output {
|
||||
let buf = TextBuffer::new(&builder.content);
|
||||
TextEdit::new(
|
||||
let buf = TextBuffer::new_empty(Metrics::new(
|
||||
builder.attrs.font_size,
|
||||
builder.attrs.line_height,
|
||||
));
|
||||
let mut text = TextEdit::new(
|
||||
TextView::new(buf, builder.attrs, builder.hint.get(state)),
|
||||
builder.output.mode,
|
||||
)
|
||||
);
|
||||
let font_system = &mut state.get_mut().text.font_system;
|
||||
text.buf
|
||||
.set_text(font_system, &builder.content, &Attrs::new(), SHAPING, None);
|
||||
builder.attrs.apply(font_system, &mut text.buf, None);
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+392
-251
@@ -1,30 +1,17 @@
|
||||
use crate::prelude::*;
|
||||
use iris_core::{TextData, UiColor};
|
||||
use parley::{Affinity, Layout, Selection};
|
||||
use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use winit::{
|
||||
event::KeyEvent,
|
||||
keyboard::{Key, NamedKey},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Motion {
|
||||
Left,
|
||||
Right,
|
||||
LeftWord,
|
||||
RightWord,
|
||||
Up,
|
||||
Down,
|
||||
LineStart,
|
||||
LineEnd,
|
||||
}
|
||||
|
||||
pub struct TextEdit {
|
||||
view: TextView,
|
||||
/// `None` represents unfocused, which Parley's `Selection` cannot express.
|
||||
selection: Option<Selection>,
|
||||
history: Vec<(String, Option<Selection>)>,
|
||||
double_hit: Option<usize>,
|
||||
selection: TextSelection,
|
||||
history: Vec<(String, TextSelection)>,
|
||||
double_hit: Option<Cursor>,
|
||||
pub mode: EditMode,
|
||||
}
|
||||
|
||||
@@ -38,19 +25,27 @@ impl TextEdit {
|
||||
pub fn new(view: TextView, mode: EditMode) -> Self {
|
||||
Self {
|
||||
view,
|
||||
selection: None,
|
||||
selection: Default::default(),
|
||||
history: Default::default(),
|
||||
double_hit: None,
|
||||
mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
let sel = self.selection?;
|
||||
if sel.is_collapsed() {
|
||||
return None;
|
||||
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
let mut iter = self.buf.lines.iter().skip(start.line);
|
||||
let first = iter.next().unwrap();
|
||||
if start.line == end.line {
|
||||
first.text()[start.index..end.index].to_string()
|
||||
} else {
|
||||
let mut str = first.text()[start.index..].to_string();
|
||||
for _ in (start.line + 1)..end.line {
|
||||
str = str + "\n" + iter.next().unwrap().text();
|
||||
}
|
||||
let last = iter.next().unwrap();
|
||||
str = str + "\n" + &last.text()[..end.index];
|
||||
str
|
||||
}
|
||||
Some(self.buf.text()[sel.text_range()].to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,29 +57,39 @@ impl Widget for TextEdit {
|
||||
painter.layer = base;
|
||||
let region = self.region();
|
||||
|
||||
let Some(selection) = self.selection else {
|
||||
return;
|
||||
};
|
||||
let layout = self.view.buf.layout();
|
||||
|
||||
// parley reports selection as boxes in layout space, so bidi and
|
||||
// wrapped lines come out right without this code knowing about either.
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::SKY),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
let size = vec2(1, self.attrs.line_height);
|
||||
match self.selection {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
if let Some(offset) = cursor_pos(cursor, &self.buf) {
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(offset).within(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
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(®ion),
|
||||
);
|
||||
}
|
||||
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(®ion),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
|
||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
@@ -96,58 +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 text: &'a mut TextEdit,
|
||||
pub data: &'a mut TextData,
|
||||
pub font_system: &'a mut FontSystem,
|
||||
}
|
||||
|
||||
impl<'a> TextEditCtx<'a> {
|
||||
fn layout(&mut self) -> &Layout<UiColor> {
|
||||
let attrs = self.text.view.attrs.clone();
|
||||
let width = self.text.view.wrap_width();
|
||||
self.text.view.buf.shape(self.data, &attrs, width);
|
||||
self.text.view.buf.layout()
|
||||
}
|
||||
|
||||
fn 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 {
|
||||
let text = self.text.view.buf.text().to_string();
|
||||
self.set("");
|
||||
let text = self
|
||||
.text
|
||||
.buf
|
||||
.lines
|
||||
.drain(..)
|
||||
.map(|l| l.into_text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.text
|
||||
.buf
|
||||
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
|
||||
self.text.selection.clear();
|
||||
text
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.text.selection = None;
|
||||
self.text
|
||||
.buf
|
||||
.set_text(self.font_system, &text, &Attrs::new(), SHAPING, None);
|
||||
self.text.selection.clear();
|
||||
}
|
||||
|
||||
pub fn motion(&mut self, motion: Motion, select: bool) {
|
||||
let Some(sel) = self.text.selection else {
|
||||
return;
|
||||
};
|
||||
let layout = self.layout();
|
||||
let sel = apply_motion(sel, layout, motion, select);
|
||||
self.text.selection = Some(sel);
|
||||
if let TextSelection::Pos(cursor) = self.text.selection
|
||||
&& let Some(new) = self.buf_motion(cursor, motion)
|
||||
{
|
||||
if select {
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: cursor,
|
||||
end: new,
|
||||
};
|
||||
} else {
|
||||
self.text.selection = TextSelection::Pos(new);
|
||||
}
|
||||
} else if let TextSelection::Span { start, end } = self.text.selection {
|
||||
if select {
|
||||
if let Some(cursor) = self.buf_motion(end, motion) {
|
||||
self.text.selection = TextSelection::Span { start, end: cursor };
|
||||
}
|
||||
} else {
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
let sel = &mut self.text.selection;
|
||||
match motion {
|
||||
Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start),
|
||||
Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end),
|
||||
_ => {
|
||||
if let Some(cursor) = self.buf_motion(end, motion) {
|
||||
self.text.selection = TextSelection::Pos(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the `len` characters before the caret. This is the IME's
|
||||
/// preedit path: it re-sends the whole composition each time.
|
||||
pub fn replace(&mut self, len: usize, text: &str) {
|
||||
let text = self.string(text);
|
||||
for _ in 0..len {
|
||||
self.backspace(false);
|
||||
self.delete(false);
|
||||
}
|
||||
self.insert_str(&text);
|
||||
self.insert_inner(&text, false);
|
||||
}
|
||||
|
||||
fn string(&self, text: &str) -> String {
|
||||
@@ -160,183 +261,202 @@ impl<'a> TextEditCtx<'a> {
|
||||
|
||||
pub fn insert(&mut self, text: &str) {
|
||||
let text = self.string(text);
|
||||
self.insert_str(&text);
|
||||
}
|
||||
|
||||
fn insert_str(&mut self, text: &str) {
|
||||
if text.is_empty() {
|
||||
let mut lines = text.split('\n');
|
||||
let Some(first) = lines.next() else {
|
||||
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.text.view.buf.edit().insert_str(at, text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(at + text.len());
|
||||
self.insert_inner(first, true);
|
||||
for line in lines {
|
||||
self.newline();
|
||||
self.insert_inner(line, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_span(&mut self) -> bool {
|
||||
let Some(sel) = self.text.selection else {
|
||||
return false;
|
||||
};
|
||||
if sel.is_collapsed() {
|
||||
return false;
|
||||
if let TextSelection::Span { start, end } = self.text.selection {
|
||||
self.delete_between(start, end);
|
||||
let (start, _) = sort_cursors(start, end);
|
||||
self.text.selection = TextSelection::Pos(start);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
let 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
|
||||
}
|
||||
|
||||
fn set_caret(&mut self, index: usize) {
|
||||
let index = index.min(self.text.view.buf.text().len());
|
||||
let layout = self.layout();
|
||||
self.text.selection = Some(Selection::from_byte_index(
|
||||
layout,
|
||||
index,
|
||||
Affinity::default(),
|
||||
));
|
||||
pub fn delete_between(&mut self, start: Cursor, end: Cursor) {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let (start, end) = sort_cursors(start, end);
|
||||
if start.line == end.line {
|
||||
let line = &mut lines[start.line];
|
||||
let text = line.text();
|
||||
let text = text[..start.index].to_string() + &text[end.index..];
|
||||
edit_line(line, text);
|
||||
} else {
|
||||
// start
|
||||
let start_text = lines[start.line].text()[..start.index].to_string();
|
||||
let end_text = &lines[end.line].text()[end.index..];
|
||||
let text = start_text + end_text;
|
||||
edit_line(&mut lines[start.line], text);
|
||||
}
|
||||
// between
|
||||
let range = (start.line + 1)..=end.line;
|
||||
if !range.is_empty() {
|
||||
lines.splice(range, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_inner(&mut self, text: &str, mov: bool) {
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let line = &mut self.text.view.buf.lines[cursor.line];
|
||||
let mut line_text = line.text().to_string();
|
||||
line_text.insert_str(cursor.index, text);
|
||||
edit_line(line, line_text);
|
||||
if mov {
|
||||
for _ in 0..text.chars().count() {
|
||||
self.motion(Motion::Right, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newline(&mut self) {
|
||||
if self.text.mode == EditMode::MultiLine {
|
||||
self.insert_str("\n");
|
||||
if self.text.mode == EditMode::SingleLine {
|
||||
return;
|
||||
}
|
||||
self.clear_span();
|
||||
if let TextSelection::Pos(cursor) = &mut self.text.selection {
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
let new = line.split_off(cursor.index);
|
||||
cursor.line += 1;
|
||||
lines.insert(cursor.line, new);
|
||||
cursor.index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, word: bool) {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
&& (cursor.index != 0 || cursor.line != 0)
|
||||
{
|
||||
self.motion(if word { Motion::LeftWord } else { Motion::Left }, false);
|
||||
self.delete(word);
|
||||
}
|
||||
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) {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
}
|
||||
let Some(sel) = self.text.selection else {
|
||||
return;
|
||||
};
|
||||
let start = sel.focus().index();
|
||||
if start >= self.text.view.buf.text().len() {
|
||||
return;
|
||||
}
|
||||
let end = {
|
||||
let layout = self.layout();
|
||||
if !self.clear_span()
|
||||
&& let TextSelection::Pos(cursor) = &mut self.text.selection
|
||||
{
|
||||
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 {
|
||||
let clusters = sel.focus().logical_clusters(layout);
|
||||
let Some(cluster) = clusters[1].as_ref() else {
|
||||
return;
|
||||
};
|
||||
cluster.text_range().end
|
||||
let lines = &mut self.text.view.buf.lines;
|
||||
let line = &mut lines[cursor.line];
|
||||
if cursor.index == line.text().len() {
|
||||
if cursor.line == lines.len() - 1 {
|
||||
return;
|
||||
}
|
||||
let add = lines.remove(cursor.line + 1).into_text();
|
||||
let line = &mut lines[cursor.line];
|
||||
let mut cur = line.text().to_string();
|
||||
cur.push_str(&add);
|
||||
edit_line(line, cur);
|
||||
} else {
|
||||
let mut text = line.text().to_string();
|
||||
text.remove(cursor.index);
|
||||
edit_line(line, text);
|
||||
}
|
||||
}
|
||||
};
|
||||
self.delete_range(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_range(&mut self, start: usize, end: usize) {
|
||||
let len = self.text.view.buf.text().len();
|
||||
let (start, end) = (start.min(end).min(len), start.max(end).min(len));
|
||||
if start == end {
|
||||
return;
|
||||
}
|
||||
self.text.view.buf.edit().replace_range(start..end, "");
|
||||
self.text.view.buf.changed = true;
|
||||
self.set_caret(start);
|
||||
fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option<Cursor> {
|
||||
self.text
|
||||
.buf
|
||||
.cursor_motion(self.font_system, cursor, None, motion)
|
||||
.map(|r| r.0)
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
let len = self.text.view.buf.text().len();
|
||||
if len == 0 {
|
||||
return;
|
||||
pub fn select_word_at(&mut self, cursor: Cursor) {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.buf_motion(cursor, Motion::LeftWord),
|
||||
self.buf_motion(cursor, Motion::RightWord),
|
||||
) {
|
||||
self.text.selection = TextSelection::Span { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_line_at(&mut self, cursor: Cursor) {
|
||||
let end = self.text.buf.lines[cursor.line].text().len();
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: Cursor::new(cursor.line, 0),
|
||||
end: Cursor::new(cursor.line, end),
|
||||
}
|
||||
let layout = self.layout();
|
||||
let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default());
|
||||
let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default());
|
||||
self.text.selection = Some(Selection::new(anchor, focus));
|
||||
}
|
||||
|
||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||
let prev_sel = self.text.selection;
|
||||
let prev_hit = self.text.double_hit;
|
||||
|
||||
let outcome = {
|
||||
let layout = self.layout();
|
||||
if drag {
|
||||
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)
|
||||
})
|
||||
let hit = self.text.buf.hit(pos.x, pos.y);
|
||||
let sel = &mut self.text.selection;
|
||||
match sel {
|
||||
TextSelection::None => {
|
||||
if !drag && let Some(hit) = hit {
|
||||
*sel = TextSelection::Pos(hit)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((selection, double_hit)) = outcome {
|
||||
self.text.selection = selection;
|
||||
self.text.double_hit = double_hit;
|
||||
TextSelection::Pos(pos) => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => (),
|
||||
(Some(hit), false) => {
|
||||
if recent && hit == *pos {
|
||||
self.text.double_hit = Some(hit);
|
||||
return self.select_word_at(hit);
|
||||
} else {
|
||||
*pos = hit
|
||||
}
|
||||
}
|
||||
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
|
||||
},
|
||||
TextSelection::Span { start, end } => match (hit, drag) {
|
||||
(None, false) => *sel = TextSelection::None,
|
||||
(None, true) => *sel = TextSelection::Pos(*start),
|
||||
(Some(hit), false) => {
|
||||
if recent
|
||||
&& let Some(double) = self.text.double_hit
|
||||
&& double == hit
|
||||
{
|
||||
return self.select_line_at(hit);
|
||||
} else {
|
||||
*sel = TextSelection::Pos(hit)
|
||||
}
|
||||
}
|
||||
(Some(hit), true) => *end = hit,
|
||||
},
|
||||
}
|
||||
if let TextSelection::Span { start, end } = sel
|
||||
&& start == end
|
||||
{
|
||||
*sel = TextSelection::Pos(*start);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.text.selection = None;
|
||||
self.text.double_hit = None;
|
||||
self.text.selection = TextSelection::None;
|
||||
}
|
||||
|
||||
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 res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.refresh();
|
||||
}
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
if undo && let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
} else if self.text.content() != old.0 {
|
||||
self.text.history.push(old);
|
||||
}
|
||||
res
|
||||
@@ -361,25 +481,21 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowRight => {
|
||||
let motion = if modifiers.control {
|
||||
Motion::RightWord
|
||||
if modifiers.control {
|
||||
self.motion(Motion::RightWord, modifiers.shift)
|
||||
} else {
|
||||
Motion::Right
|
||||
};
|
||||
self.motion(motion, modifiers.shift);
|
||||
self.motion(Motion::Right, modifiers.shift)
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowLeft => {
|
||||
let motion = if modifiers.control {
|
||||
Motion::LeftWord
|
||||
if modifiers.control {
|
||||
self.motion(Motion::LeftWord, modifiers.shift)
|
||||
} else {
|
||||
Motion::Left
|
||||
};
|
||||
self.motion(motion, modifiers.shift);
|
||||
self.motion(Motion::Left, modifiers.shift)
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift),
|
||||
NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift),
|
||||
NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift),
|
||||
NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift),
|
||||
NamedKey::Escape => {
|
||||
self.deselect();
|
||||
return TextInputResult::Unfocus;
|
||||
@@ -391,18 +507,34 @@ impl<'a> TextEditCtx<'a> {
|
||||
match text.as_str() {
|
||||
"v" => return TextInputResult::Paste,
|
||||
"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);
|
||||
}
|
||||
}
|
||||
"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();
|
||||
return TextInputResult::Copy(content);
|
||||
}
|
||||
}
|
||||
"a" => self.select_all(),
|
||||
"z" => *undo = true,
|
||||
"a" => {
|
||||
if !self.text.buf.lines[0].text().is_empty()
|
||||
|| self.text.buf.lines.len() > 1
|
||||
{
|
||||
let lines = &self.text.buf.lines;
|
||||
let last_line = lines.len() - 1;
|
||||
let last_idx = lines[last_line].text().len();
|
||||
self.text.selection = TextSelection::Span {
|
||||
start: Cursor::new(0, 0),
|
||||
end: Cursor::new(last_line, last_idx),
|
||||
};
|
||||
}
|
||||
}
|
||||
"z" => {
|
||||
*undo = true;
|
||||
}
|
||||
_ => self.insert(text),
|
||||
}
|
||||
} else {
|
||||
@@ -415,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)]
|
||||
pub struct Modifiers {
|
||||
pub shift: bool,
|
||||
@@ -455,6 +569,33 @@ pub enum TextInputResult {
|
||||
Paste,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum TextSelection {
|
||||
#[default]
|
||||
None,
|
||||
Pos(Cursor),
|
||||
Span {
|
||||
start: Cursor,
|
||||
end: Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
TextSelection::None => (),
|
||||
TextSelection::Pos(cursor) => {
|
||||
cursor.line = 0;
|
||||
cursor.index = 0;
|
||||
cursor.affinity = Affinity::default();
|
||||
}
|
||||
TextSelection::Span { start: _, end: _ } => {
|
||||
*self = TextSelection::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextInputResult {
|
||||
pub fn unfocus(&self) -> bool {
|
||||
matches!(self, TextInputResult::Unfocus)
|
||||
@@ -476,15 +617,15 @@ impl DerefMut for TextEdit {
|
||||
}
|
||||
|
||||
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 {
|
||||
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();
|
||||
TextEditCtx {
|
||||
text: ui.widgets.get_mut(self).unwrap(),
|
||||
data: &mut ui.text,
|
||||
font_system: &mut ui.text.font_system,
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-28
@@ -6,8 +6,11 @@ pub use edit::*;
|
||||
use iris_core::util::MutDetect;
|
||||
|
||||
use crate::prelude::*;
|
||||
use cosmic_text::{Attrs, BufferLine, Cursor, Metrics, Shaping};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub const SHAPING: Shaping = Shaping::Advanced;
|
||||
|
||||
pub struct Text {
|
||||
pub content: MutDetect<String>,
|
||||
view: TextView,
|
||||
@@ -19,21 +22,11 @@ pub struct TextView {
|
||||
// cache
|
||||
tex: Option<RenderedText>,
|
||||
width: Option<f32>,
|
||||
pub hint: Option<StrongWidget>,
|
||||
pub hint: Option<WidgetHandle>,
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
fn is_blank(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
pub fn wrap_width(&self) -> Option<f32> {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<WidgetHandle>) -> Self {
|
||||
Self {
|
||||
attrs: attrs.into(),
|
||||
buf: buf.into(),
|
||||
@@ -52,6 +45,15 @@ impl TextView {
|
||||
.align(self.align)
|
||||
}
|
||||
|
||||
fn tex_region(&self, tex: &RenderedText) -> UiRegion {
|
||||
let region = tex.size.align(self.align);
|
||||
let dims = tex.handle.size();
|
||||
let mut region = region.offset(tex.top_left_offset);
|
||||
region.x.end = region.x.start + UiScalar::abs(dims.x);
|
||||
region.y.end = region.y.start + UiScalar::abs(dims.y);
|
||||
region
|
||||
}
|
||||
|
||||
fn render(&mut self, ctx: &mut SizeCtx) -> RenderedText {
|
||||
let width = if self.attrs.wrap {
|
||||
Some(ctx.px_size().x)
|
||||
@@ -66,7 +68,10 @@ impl TextView {
|
||||
return tex.clone();
|
||||
}
|
||||
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.attrs.changed = false;
|
||||
self.buf.changed = false;
|
||||
@@ -76,8 +81,9 @@ impl TextView {
|
||||
self.tex.as_ref()
|
||||
}
|
||||
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_blank()
|
||||
&& let Some(hint) = &self.hint
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
ctx.width(hint)
|
||||
} else {
|
||||
@@ -85,8 +91,9 @@ impl TextView {
|
||||
}
|
||||
}
|
||||
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||
if self.is_blank()
|
||||
&& let Some(hint) = &self.hint
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
ctx.height(hint)
|
||||
} else {
|
||||
@@ -95,35 +102,47 @@ impl TextView {
|
||||
}
|
||||
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
||||
let tex = self.render(&mut painter.size_ctx());
|
||||
let region = tex.size.align(self.align);
|
||||
if self.is_blank()
|
||||
&& let Some(hint) = &self.hint
|
||||
let region = self.tex_region(&tex);
|
||||
if let Some(hint) = &self.hint
|
||||
&& let [line] = &self.buf.lines[..]
|
||||
&& line.text().is_empty()
|
||||
{
|
||||
painter.widget(hint);
|
||||
} else {
|
||||
let within = region.within(&painter.region());
|
||||
painter.glyphs(&tex, within);
|
||||
painter.texture_within(&tex.handle, region);
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.buf.text().to_string()
|
||||
self.buf
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.text())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
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 {
|
||||
view: TextView::new(TextBuffer::new(&content), TextAttrs::default(), None),
|
||||
content: content.into(),
|
||||
content: content.into().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 {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,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 {
|
||||
type Target = TextAttrs;
|
||||
|
||||
|
||||
+26
-25
@@ -3,35 +3,35 @@ use crate::prelude::*;
|
||||
|
||||
// these methods should "not require any context" (require unit) because they're in core
|
||||
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 {
|
||||
padding: padding.into(),
|
||||
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 {
|
||||
inner: self.add_strong(state),
|
||||
align: align.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn center(self) -> impl WidgetFn<Rsc, Aligned> {
|
||||
fn center(self) -> impl WidgetFn<State, Aligned> {
|
||||
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| {
|
||||
let id = self.add(state);
|
||||
state.ui_mut().widgets.set_label(id, label.into());
|
||||
state.get_mut().set_label(id, label.into());
|
||||
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();
|
||||
move |state| Sized {
|
||||
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();
|
||||
move |state| MaxSize {
|
||||
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();
|
||||
move |state| MaxSize {
|
||||
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();
|
||||
move |state| Sized {
|
||||
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();
|
||||
move |state| Sized {
|
||||
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 {
|
||||
inner: self.add_strong(state),
|
||||
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| {
|
||||
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;
|
||||
ctx.widget(rsc).scroll(delta);
|
||||
ctx.widget().scroll(delta);
|
||||
})
|
||||
.add(state)
|
||||
}
|
||||
}
|
||||
|
||||
fn masked(self) -> impl WidgetFn<Rsc, Masked> {
|
||||
fn masked(self) -> impl WidgetFn<State, Masked> {
|
||||
move |state| Masked {
|
||||
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 {
|
||||
children: vec![w.add_strong(state), self.add_strong(state)],
|
||||
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 {
|
||||
children: vec![self.add_strong(state), w.add_strong(state)],
|
||||
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 {
|
||||
inner: self.add_strong(state),
|
||||
offset,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_any(self) -> impl WidgetIdFn<Rsc> {
|
||||
fn to_any(self) -> impl WidgetIdFn<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);
|
||||
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> {
|
||||
fn span(self, dir: Dir) -> SpanBuilder<Rsc, LEN, Wa, Tag>;
|
||||
fn stack(self) -> StackBuilder<Rsc, LEN, Wa, Tag>;
|
||||
pub trait CoreWidgetArr<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||
fn span(self, dir: Dir) -> SpanBuilder<State, LEN, Wa, Tag>;
|
||||
fn stack(self) -> StackBuilder<State, LEN, Wa, Tag>;
|
||||
}
|
||||
|
||||
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
|
||||
|
||||
@@ -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("aé", 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");
|
||||
}
|
||||
Reference in new issue
Block a user