iris: fold render state into Ui

This commit is contained in:
iris committed 2026-09-10 23:58:43 -04:00
1 parent 599d33287c
commit f49b284b46
35 files changed
+491 -429

No files matched your search

+1 -1
View File
@@ -192,7 +192,7 @@ impl AndroidAppState for BenchClient {
self.platform = Some(Arc::new(PlatformHandle::new(vm, view)));
}
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool {
false
}
+1 -3
View File
@@ -9,8 +9,6 @@ use android_view::{
};
#[cfg(not(feature = "transcript-screen"))]
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
#[cfg(not(feature = "transcript-screen"))]
use iris::prelude::*;
use log::LevelFilter;
use std::ffi::c_void;
@@ -59,7 +57,7 @@ impl AndroidAppState for Client {
Self { ui_state }
}
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool {
false
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ impl AndroidAppState for TranscriptClient {
client
}
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool {
// No screen stack of its own -- same "let the activity finish"
// answer `iris-android-app`'s tabs `Client` already gives.
false
+1 -1
View File
@@ -101,7 +101,7 @@ impl DefaultAppState for Client {
client
}
fn event(&mut self, event: AppEvent, rsc: &mut DefaultRsc<Self>, _render: &mut UiRenderState) {
fn event(&mut self, event: AppEvent, rsc: &mut DefaultRsc<Self>) {
match event {
AppEvent::Sessions(Ok(sessions)) => {
self.sessions = sessions;
+10 -10
View File
@@ -456,14 +456,14 @@ mod apply_tests {
}
struct TestRsc {
ui: UiData,
ui: Ui,
events: EventManager<TestRsc>,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
@@ -515,7 +515,7 @@ mod apply_tests {
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let old_items = vec![assistant(1, &reply(paragraphs, "and the last one is st"))];
@@ -601,7 +601,7 @@ mod apply_tests {
fn shapes_to_open(output: &str) -> u64 {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let items = run_of(3, output, true);
@@ -644,7 +644,7 @@ mod apply_tests {
fn shapes_for_message(text: &str) -> u64 {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let items = vec![
@@ -688,7 +688,7 @@ mod apply_tests {
fn cost_of_one_result(count: usize) -> u64 {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let before = run_of(count, "", false);
@@ -725,7 +725,7 @@ mod apply_tests {
#[test]
fn a_group_opens_and_closes_and_keeps_its_state_across_a_result() {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let before = run_of(3, "", false);
@@ -760,7 +760,7 @@ mod apply_tests {
#[test]
fn a_call_joining_an_open_run_appends_one_card() {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let before = run_of(2, "ok", true);
@@ -782,7 +782,7 @@ mod apply_tests {
#[test]
fn a_tail_that_stops_being_tool_calls_falls_back_to_a_rebuild() {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let before = vec![user(1, "stable"), call("t0", "", false)];
+4 -2
View File
@@ -3,7 +3,9 @@ use iris::harness::{Harness, TouchAction};
use iris::prelude::*;
fn fence_scroll_in(h: &Harness, top: f32, bottom: f32) -> Option<(WidgetId, PixelRegion)> {
h.render
let render_handle = h.rsc.ui.render_state();
let render_state = render_handle.get();
render_state
.active
.keys()
.copied()
@@ -16,7 +18,7 @@ fn fence_scroll_in(h: &Harness, top: f32, bottom: f32) -> Option<(WidgetId, Pixe
.is_some_and(|s| s.axis() == Axis::X)
})
.find_map(|id| {
let r = h.render.window_region(&id, &h.rsc)?;
let r = render_state.window_region(&id, &h.rsc)?;
(r.top_left.y >= top && r.bot_right.y <= bottom).then_some((id, r))
})
}
+14 -9
View File
@@ -158,7 +158,10 @@ fn the_composer_sits_above_a_simulated_ime_inset() {
let (mut h, screen) = opened();
let height = h.size().y;
let field_bottom = |h: &mut Harness| {
h.render
h.rsc
.ui
.render_state()
.get()
.window_region(&screen.composer.field, &h.rsc)
.expect("the composer field is on screen")
.bot_right
@@ -203,7 +206,10 @@ fn a_space_in_the_composer_finishes_layout() {
assert_eq!(screen.composer.field.edit(&mut h.rsc).text.text(), "hi ");
let region = h
.render
.rsc
.ui
.render_state()
.get()
.window_region(&screen.composer.field, &h.rsc)
.expect("the composer field is drawn");
let size = region.bot_right - region.top_left;
@@ -231,14 +237,13 @@ fn a_newline_leaves_the_caret_inside_the_composers_padding() {
screen.composer.field.edit(&mut h.rsc).insert("a\n");
h.frame(PHONE_FRAME_MS);
}
let message = h
.render
let render_handle = h.rsc.ui.render_state();
let render_state = render_handle.get();
let message = render_state
.debug(h.rsc.widgets(), "Message")
.find(|a| !a.primitives.is_empty())
.expect("the composer field is drawn");
let caret = h
.render
.primitive_corners(message.primitives.last().unwrap().slot, &h.rsc);
let caret = render_state.primitive_corners(message.primitives.last().unwrap().slot, &h.rsc);
let bar_bottom = height - ime;
let padding = 12.0 * PHONE_SCALE;
assert!(
@@ -249,11 +254,11 @@ fn a_newline_leaves_the_caret_inside_the_composers_padding() {
);
let mask = h.rsc.ui.masks[message.mask.idx()];
let bar = h.render.primitive_corners(mask.primitive, &h.rsc);
let bar = render_state.primitive_corners(mask.primitive, &h.rsc);
let visible_content_top = message
.primitives
.iter()
.map(|p| h.render.primitive_corners(p.slot, &h.rsc))
.map(|p| render_state.primitive_corners(p.slot, &h.rsc))
.filter(|r| r.bot_right.y > bar.top_left.y)
.map(|r| r.top_left.y)
.fold(f32::INFINITY, f32::min);
+17 -10
View File
@@ -21,20 +21,24 @@ fn opened() -> (Harness, ai_app::ui::TranscriptScreen) {
}
fn list_box(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> PixelRegion {
h.render
h.rsc
.ui
.render_state()
.get()
.window_region(&screen.list.id(), &h.rsc)
.expect("the list is on screen")
}
fn drawn_rows(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> Vec<(f32, f32)> {
let mut rows: Vec<(f32, f32)> = h
.render
let render_handle = h.rsc.ui.render_state();
let render_state = render_handle.get();
let mut rows: Vec<(f32, f32)> = render_state
.active
.get(&screen.list.id())
.expect("the list is drawn")
.children
.iter()
.filter_map(|id| h.render.window_region(id, &h.rsc))
.filter_map(|id| render_state.window_region(id, &h.rsc))
.map(|px| (px.top_left.y, px.bot_right.y))
.collect();
rows.sort_by(|a, b| a.0.total_cmp(&b.0));
@@ -76,12 +80,14 @@ fn the_row_across_the_top_edge_is_drawn() {
#[test]
fn the_list_is_clipped_to_its_own_box() {
let (h, screen) = opened();
let active = h.render.active.get(&screen.list.id()).expect("drawn");
let render_handle = h.rsc.ui.render_state();
let render_state = render_handle.get();
let active = render_state.active.get(&screen.list.id()).expect("drawn");
assert!(
active.mask != MaskIdx::NONE,
"the transcript's list is drawn with nothing clipping it",
);
let clip = h.render.mask_region(active.mask, &h.rsc);
let clip = render_state.mask_region(active.mask, &h.rsc);
let list = list_box(&h, &screen);
assert!(
clip.top_left.y >= list.top_left.y - 0.5 && clip.bot_right.y <= list.bot_right.y + 0.5,
@@ -89,8 +95,7 @@ fn the_list_is_clipped_to_its_own_box() {
edge still draws past it",
);
let rows = h
.render
let rows = render_state
.active
.get(&screen.list.id())
.expect("the list is drawn")
@@ -116,14 +121,16 @@ fn the_list_is_clipped_to_its_own_box() {
}
fn primitives_under(h: &Harness, id: WidgetId) -> Vec<MaskIdx> {
let Some(active) = h.render.active.get(&id) else {
let render_handle = h.rsc.ui.render_state();
let render_state = render_handle.get();
let Some(active) = render_state.active.get(&id) else {
return Vec::new();
};
let mut out: Vec<MaskIdx> = active
.primitives
.iter()
.filter(|p| p.binding != IMAGE_BINDING)
.map(|p| h.render.primitives.instance(p.slot).mask_idx)
.map(|p| render_state.primitives.instance(p.slot).mask_idx)
.collect();
for child in &active.children {
out.extend(primitives_under(h, *child));
+18
View File
@@ -6,6 +6,24 @@ without reading that result does not make the parent's size depend on it.
## Design
### UI ownership and frame access
`Ui` owns both the mutable widget-side `UiData` and the retained
`UiRenderState`. It dereferences to `UiData`, so resources expose one `Ui`
without adding a second layer to ordinary widget, text, and texture access.
The render state itself remains private. `Ui::render_state()` returns an owned
`RenderHandle`, whose only public operation is a shared `get()` guard over the
last completed frame. Owning the handle, rather than borrowing `Ui`, lets a
controller inspect retained ancestry while it mutates other resources.
`UiRsc::draw` is the mutation boundary: it clones the private handle, takes
the exclusive guard, and updates the render state with the `Rsc`. Event
dispatch holds a shared guard for the whole callback, so events and controller
methods can reuse the completed tree but cannot start a draw or observe a
partially updated one. Controller ancestry is walked directly through that
tree; the event manager still maintains its per-event active-widget index in
draw hooks so dispatch never has to scan every active widget.
### 1. The new `Widget` trait
```rust
+3 -2
View File
@@ -60,8 +60,9 @@ runs inside `cargo test`.
1. **Headless, in-process, no compositor and no GPU -- the default.**
`iris::harness` (`iris/src/harness.rs`), plus the fixture crate it
opens. `Harness::new(size, density)` builds an `Rsc`, a
`UiRenderState` and a state whose `FocusHost`/`OpenUrl` *record* what
opens. `Harness::new(size, density)` builds an `Rsc` whose `Ui` owns
the retained render state, and a state whose `FocusHost`/`OpenUrl`
*record* what
the platform was asked for; `frame(t_ms)`/`frames_until(..)` run
frames on a clock the test owns, and `replay(&TouchScript)` feeds a
recorded gesture one sample at a time exactly as
+9 -21
View File
@@ -2,14 +2,14 @@ use iris::prelude::*;
use std::time::Instant;
struct BenchRsc {
ui: UiData,
ui: Ui,
}
impl UiRsc for BenchRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
}
@@ -60,9 +60,7 @@ fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64,
}
fn bench_first_frame(n: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let (_list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
@@ -81,9 +79,7 @@ fn bench_first_frame(n: usize) {
}
fn bench_scroll(n: usize, ticks: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let (scroll, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
@@ -120,9 +116,7 @@ fn bench_scroll(n: usize, ticks: usize) {
}
fn bench_input_grows(n: usize, lines: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let (scroll, list_root) = build_message_list(&mut rsc, n, 20);
let list_area = rsc.ui.widgets.add_strong(Sized {
inner: list_root,
@@ -183,9 +177,7 @@ fn bench_input_grows(n: usize, lines: usize) {
}
fn bench_insert_above_anchor(n: usize, inserts: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let (list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0));
@@ -230,9 +222,7 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) {
}
fn bench_expand_holds_edge(n: usize, growths: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let growable_index = n.saturating_sub(3);
let mut growable = None;
@@ -302,9 +292,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
}
fn bench_redraw_big_text(chars: usize, redraws: usize) {
let mut rsc = BenchRsc {
ui: UiData::default(),
};
let mut rsc = BenchRsc { ui: Ui::default() };
let content: String = (0..chars)
.map(|i| char::from(b'a' + (i % 26) as u8))
.collect();
+20 -17
View File
@@ -1,5 +1,5 @@
use crate::{
ActiveData, WidgetId,
UiRenderState, WidgetId,
util::{HashMap, HashSet},
};
use std::any::{Any, TypeId};
@@ -56,7 +56,6 @@ pub trait Controller<Rsc>: ControllerValue {
pub struct ControllerManager<Rsc> {
by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>,
parents: HashMap<WidgetId, Option<WidgetId>>,
borrowed: HashSet<ControllerId>,
removed_while_borrowed: HashSet<WidgetId>,
command_target: Option<ControllerId>,
@@ -68,7 +67,6 @@ impl<Rsc> Default for ControllerManager<Rsc> {
fn default() -> Self {
Self {
by_widget: Default::default(),
parents: Default::default(),
borrowed: Default::default(),
removed_while_borrowed: Default::default(),
command_target: None,
@@ -111,7 +109,11 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
.then_some(ControllerId { host, kind })
}
pub fn nearest_id<C: Controller<Rsc>>(&self, mut origin: WidgetId) -> Option<ControllerId> {
pub fn nearest_id<C: Controller<Rsc>>(
&self,
mut origin: WidgetId,
render_state: &UiRenderState,
) -> Option<ControllerId> {
let kind = TypeId::of::<C>();
loop {
let candidate = ControllerId { host: origin, kind };
@@ -122,13 +124,14 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
if let Some(id) = self.id::<C>(origin) {
return Some(id);
}
origin = self.parents.get(&origin).copied().flatten()?;
origin = render_state.active.get(&origin)?.parent?;
}
}
pub fn path_to<C: Controller<Rsc>>(
&self,
mut origin: WidgetId,
render_state: &UiRenderState,
) -> Option<(ControllerId, Vec<WidgetId>)> {
let mut path = Vec::new();
loop {
@@ -136,18 +139,10 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
if let Some(id) = self.id::<C>(origin) {
return Some((id, path));
}
origin = self.parents.get(&origin).copied().flatten()?;
origin = render_state.active.get(&origin)?.parent?;
}
}
pub fn draw(&mut self, active: &ActiveData) {
self.parents.insert(active.id, active.parent);
}
pub fn undraw(&mut self, active: &ActiveData) {
self.parents.remove(&active.id);
}
pub fn take<C: Controller<Rsc>>(&mut self, id: ControllerId) -> Option<C> {
if id.kind != TypeId::of::<C>() {
return None;
@@ -232,9 +227,18 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
self.command_boundary
}
pub(crate) fn is_below(&self, mut widget: WidgetId, ancestor: WidgetId) -> bool {
pub(crate) fn is_below(
&self,
mut widget: WidgetId,
ancestor: WidgetId,
render_state: &UiRenderState,
) -> bool {
loop {
let Some(parent) = self.parents.get(&widget).copied().flatten() else {
let Some(parent) = render_state
.active
.get(&widget)
.and_then(|active| active.parent)
else {
return false;
};
if parent == ancestor {
@@ -250,7 +254,6 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
pub fn remove(&mut self, host: WidgetId) {
self.by_widget.remove(&host);
self.parents.remove(&host);
if self.borrowed.iter().any(|id| id.host == host) {
self.removed_while_borrowed.insert(host);
}
-2
View File
@@ -60,14 +60,12 @@ impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
}
fn draw(&mut self, active: &ActiveData) {
self.controllers.draw(active);
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().draw(active);
}
}
fn undraw(&mut self, active: &ActiveData) {
self.controllers.undraw(active);
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().undraw(active);
}
+27 -9
View File
@@ -44,7 +44,11 @@ pub trait HasEvents: Sized + UiRsc + HasState {
origin: impl IdLike,
f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T,
) -> Option<T> {
let id = self.events().controllers.nearest_id::<C>(origin.id())?;
let render_handle = self.ui().render_state();
let id = self
.events()
.controllers
.nearest_id::<C>(origin.id(), &render_handle.get())?;
self.with_controller(id, |controller, rsc| f(id, controller, rsc))
}
@@ -54,14 +58,22 @@ pub trait HasEvents: Sized + UiRsc + HasState {
fn run_command(&mut self, command: Command) -> CommandResult {
let revision = self.events().controllers.command_target_revision();
if let Some(boundary) = self.events().controllers.command_boundary()
&& self
.events()
.controllers
.command_target()
.is_none_or(|target| !self.events().controllers.is_below(target.host(), boundary))
{
return CommandResult::Unused;
if let Some(boundary) = self.events().controllers.command_boundary() {
let render_handle = self.ui().render_state();
let outside_boundary =
self.events()
.controllers
.command_target()
.is_none_or(|target| {
!self.events().controllers.is_below(
target.host(),
boundary,
&render_handle.get(),
)
});
if outside_boundary {
return CommandResult::Unused;
}
}
let Some((id, mut controller)) = self.events_mut().controllers.take_command_target() else {
return CommandResult::Unused;
@@ -96,6 +108,12 @@ pub trait RunEvents: HasEvents {
data: <E::Event as Event>::Data<'_>,
state: &mut Self::State,
) {
// Keep the last completed frame read-locked for the whole callback.
// Rsc methods may take further shared reads through `render_state`,
// while any attempt to start a render from an event fails at the
// mutable-borrow boundary instead of exposing an in-progress tree.
let render_handle = self.ui().render_state();
let _render_state = render_handle.get();
let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self)
}
+12 -12
View File
@@ -1,5 +1,5 @@
use crate::{
UiData, UiRenderState,
Ui, UiData,
render::{
data::{PrimitiveInstance, instance_slot_layout},
texture::GpuTextures,
@@ -180,13 +180,11 @@ impl UiRenderNode {
}
}
pub fn update(
&mut self,
device: &Device,
queue: &Queue,
ui: &mut UiData,
ui_render: &mut UiRenderState,
) -> FrameUpdateStats {
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) -> FrameUpdateStats {
let render_handle = ui.render_state.clone();
let mut render_guard = render_handle.get_mut();
let ui_render = &mut *render_guard;
let ui_data: &mut UiData = ui;
self.active.clear();
for (i, order) in ui_render.layers.iter_mut() {
self.active.push(i);
@@ -233,11 +231,11 @@ impl UiRenderNode {
} else {
false
};
let (entries, dirty) = ui.masks.for_upload();
let (entries, dirty) = ui_data.masks.for_upload();
let masks_resized = self.masks.update(device, queue, entries, dirty);
let (entries, dirty) = ui.move_offsets.for_upload();
let (entries, dirty) = ui_data.move_offsets.for_upload();
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
let (entries, dirty) = ui.paints.for_upload();
let (entries, dirty) = ui_data.paints.for_upload();
let paints_resized = self.paints.update(device, queue, entries, dirty);
if masks_resized || moves_resized || instances_resized || paints_resized {
self.masks_group = Self::masks_group(
@@ -249,7 +247,9 @@ impl UiRenderNode {
&self.paints,
);
}
let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout);
let rebuild_main = self
.textures
.update(&mut ui_data.textures, &self.rsc_layout);
if rebuild_main {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures);
}
+84 -2
View File
@@ -1,6 +1,11 @@
use crate::{
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
};
use std::{
cell::{Ref, RefCell, RefMut},
ops::{Deref, DerefMut},
rc::Rc,
};
mod access;
mod active;
@@ -23,6 +28,75 @@ pub struct UiData {
animating: Vec<WidgetId>,
}
#[derive(Clone)]
pub struct RenderHandle {
pub(crate) render_state: Rc<RefCell<UiRenderState>>,
}
impl RenderHandle {
/// The retained result of the last completed frame. The framework holds
/// the corresponding mutable borrow for the whole of a render update, so
/// a read attempted while that state is incomplete fails at the boundary
/// instead of observing half a frame.
pub fn get(&self) -> Ref<'_, UiRenderState> {
self.render_state
.try_borrow()
.expect("render state cannot be read while a frame is being rendered")
}
pub(crate) fn get_mut(&self) -> RefMut<'_, UiRenderState> {
self.render_state
.try_borrow_mut()
.expect("render state cannot be mutated while it is being read")
}
}
impl Default for RenderHandle {
fn default() -> Self {
Self {
render_state: Rc::new(RefCell::new(UiRenderState::new())),
}
}
}
#[derive(Default)]
pub struct Ui {
data: UiData,
pub(crate) render_state: RenderHandle,
}
impl Ui {
/// A read-only handle to the retained result of the last completed frame.
/// The handle is owned so a caller may keep its read guard while mutating
/// unrelated resources on the `Rsc` that owns this `Ui`.
pub fn render_state(&self) -> RenderHandle {
self.render_state.clone()
}
pub fn resize(&self, size: impl Into<crate::util::Vec2>) {
self.render_state.get_mut().resize(size);
}
pub fn set_density(&mut self, density: f32) {
self.data.text.density = density;
self.render_state.get_mut().set_density(density);
}
}
impl Deref for Ui {
type Target = UiData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DerefMut for Ui {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl UiData {
/// Ask for `id`'s [`crate::Widget::tick`] to run every frame until it
/// says it is done. Idempotent -- registering an already-animating
@@ -48,8 +122,16 @@ impl UiData {
}
pub trait UiRsc {
fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData;
fn ui(&self) -> &Ui;
fn ui_mut(&mut self) -> &mut Ui;
fn draw<'a>(&mut self, root: impl Into<Option<&'a crate::StrongWidget>>)
where
Self: Sized,
{
let render_state = self.ui().render_state.clone();
render_state.get_mut().update(root, self);
}
#[allow(unused_variables)]
fn on_add(&mut self, id: WeakWidget) {}
+28 -25
View File
@@ -1,6 +1,6 @@
use crate::{
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs,
TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
TextBuffer, TextData, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2,
WidgetId,
render::{
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
@@ -11,7 +11,7 @@ use crate::{
};
pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) render_state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
pub(super) region: UiRegion,
@@ -46,7 +46,7 @@ impl DrawResult<'_, '_> {
if !self.painter.size_dependencies.contains(&self.child) {
self.painter.size_dependencies.push(self.child);
}
self.painter.state.active[&self.child].size
self.painter.render_state.active[&self.child].size
}
}
@@ -94,10 +94,10 @@ impl<'a> Painter<'a> {
};
let h = match self.take_recycled(P::BINDING, drawn) {
Some(h) => {
self.state.primitives.recycle(&h, inst);
self.render_state.primitives.recycle(&h, inst);
h
}
None => self.state.write_primitive(self.layer, drawn, inst),
None => self.render_state.write_primitive(self.layer, drawn, inst),
};
if self.mask != MaskIdx::NONE {
self.rsc.ui_mut().masks.push_ref(self.mask);
@@ -109,7 +109,7 @@ impl<'a> Painter<'a> {
/// Take ownership of a handle this widget just wrote.
fn own(&mut self, h: PrimitiveHandle) {
self.state
self.render_state
.primitives
.set_handle_index(h.slot, self.primitives.len() as u32);
self.primitives.push(h);
@@ -156,7 +156,7 @@ impl<'a> Painter<'a> {
/// with no radius argument anywhere that could fall out of step with
/// the one being drawn.
pub fn set_mask_to_widget<W: ?Sized>(&mut self, shape: &StrongWidget<W>) {
let slot = self.state.first_primitive(shape.id()).unwrap_or_else(|| {
let slot = self.render_state.first_primitive(shape.id()).unwrap_or_else(|| {
panic!(
"'{}' was given as a mask's shape but drew no primitive, so there is nothing to \
clip to",
@@ -172,7 +172,7 @@ impl<'a> Painter<'a> {
"set_mask called twice while drawing one widget: the second would replace the first \
rather than nest inside it",
);
let binding = self.state.primitives.instance(shape).binding;
let binding = self.render_state.primitives.instance(shape).binding;
assert_eq!(
binding,
RectPrimitive::BINDING,
@@ -248,7 +248,7 @@ impl<'a> Painter<'a> {
let next = [offset.x, offset.y];
if self.rsc.ui().move_offsets[slot.idx()].delta != next {
self.rsc.ui_mut().move_offsets.get_mut(slot).delta = next;
self.state.note_move();
self.render_state.note_move();
}
}
@@ -262,7 +262,10 @@ impl<'a> Painter<'a> {
{
None
} else {
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
self.render_state
.active
.get(&id.id())
.map(|a| a.size.axis(axis))
};
if len.is_some() && !self.size_dependencies.contains(&id.id()) {
self.size_dependencies.push(id.id());
@@ -277,7 +280,7 @@ impl<'a> Painter<'a> {
) -> DrawResult<'p, 'a> {
self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.render_state.draw_inner(
self.layer,
id.id(),
region,
@@ -300,16 +303,16 @@ impl<'a> Painter<'a> {
) -> DrawResult<'p, 'a> {
let region = region.within(&self.region);
let retained = self
.state
.render_state
.active
.get(&id.id())
.map(|active| (active.layer, active.mask));
if self.state.place(id.id(), region, self.rsc).is_some() {
if self.render_state.place(id.id(), region, self.rsc).is_some() {
} else if let Some((layer, mask)) = retained {
self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.render_state.draw_inner(
layer,
id.id(),
region,
@@ -322,7 +325,7 @@ impl<'a> Painter<'a> {
} else {
self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner(
self.render_state.draw_inner(
self.layer,
id.id(),
region,
@@ -385,7 +388,7 @@ impl<'a> Painter<'a> {
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
Some(h) => {
self.state.primitives.recycle_image(
self.render_state.primitives.recycle_image(
&h,
self.id,
texture_idx,
@@ -395,7 +398,7 @@ impl<'a> Painter<'a> {
);
h
}
None => self.state.write_image(
None => self.render_state.write_image(
self.layer,
self.id,
texture_idx,
@@ -416,9 +419,9 @@ impl<'a> Painter<'a> {
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
let density = self.state.density;
self.state.shape_count += 1;
let ui = self.rsc.ui_mut();
let density = self.render_state.density;
self.render_state.shape_count += 1;
let ui: &mut UiData = self.rsc.ui_mut();
ui.text
.render(buffer, attrs, width, &mut ui.textures, density)
}
@@ -477,17 +480,17 @@ impl<'a> Painter<'a> {
}
pub fn output_size(&self) -> Vec2 {
self.state.output_size
self.render_state.output_size
}
/// Physical pixels per `dp` -- see `UiRenderState::density`'s field
/// doc. What `Len::dp`'s `apply_rest` call resolves against.
pub fn density(&self) -> f32 {
self.state.density
self.render_state.density
}
pub fn px_size(&mut self) -> Vec2 {
self.region.size().to_abs(self.state.output_size)
self.region.size().to_abs(self.render_state.output_size)
}
pub fn text_data(&mut self) -> &mut TextData {
@@ -495,11 +498,11 @@ impl<'a> Painter<'a> {
}
pub fn child_layer(&mut self) {
self.layer = self.state.layers.child(self.layer);
self.layer = self.render_state.layers.child(self.layer);
}
pub fn next_layer(&mut self) {
self.layer = self.state.layers.next(self.layer);
self.layer = self.render_state.layers.next(self.layer);
}
pub fn label(&self) -> &str {
+4 -4
View File
@@ -441,7 +441,7 @@ impl UiRenderState {
]
});
let mut painter = Painter {
state: self,
render_state: self,
region,
mask,
move_slot,
@@ -466,7 +466,7 @@ impl UiRenderState {
widget.size_hint(Axis::X).map(|len| len.fold_dp(density)),
widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)),
];
painter.state.draw_count += 1;
painter.render_state.draw_count += 1;
widget.draw(&mut painter);
let size = painter.size.unwrap_or_else(|| {
panic!(
@@ -491,10 +491,10 @@ impl UiRenderState {
);
}
drop(widget);
painter.state.draw_started.remove(&id);
painter.render_state.draw_started.remove(&id);
let Painter {
state: _,
render_state: _,
rsc: _,
region,
mask: _,
+1 -6
View File
@@ -40,12 +40,7 @@ impl DefaultAppState for State {
}
}
fn window_event(
&mut self,
event: winit::event::WindowEvent,
rsc: &mut DefaultRsc<Self>,
_render: &mut UiRenderState,
) {
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DefaultRsc<Self>) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
return;
}
+3 -7
View File
@@ -24,16 +24,12 @@ impl DefaultAppState for Client {
}
}
fn window_event(
&mut self,
_: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
fn window_event(&mut self, _: WindowEvent, rsc: &mut DefaultRsc<Self>) {
let render_state = rsc.ui.render_state();
let new = format!(
"widgets: {}\nactive: {}\nviews: {}",
rsc.widgets().len(),
render.active_widgets(),
render_state.get().active_widgets(),
self.ui_state.renderer.ui.view_count(),
);
if new != *rsc.widgets()[self.info].content {
+3 -9
View File
@@ -3,9 +3,7 @@ use crate::prelude::*;
#[test]
fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc);
let root = leaf.upgrade(&mut rsc).any();
let mut render = UiRenderState::new();
@@ -37,9 +35,7 @@ fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
#[test]
fn a_widget_with_no_label_never_reaches_the_tree() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE));
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
@@ -55,9 +51,7 @@ fn a_widget_with_no_label_never_reaches_the_tree() {
#[test]
fn bounds_follow_a_moved_widget_and_updates_stay_incremental() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc);
let leaf_strong = leaf.upgrade(&mut rsc).any();
let offset = rsc.ui.widgets.add_strong(Offset {
+3 -3
View File
@@ -4,7 +4,7 @@ use android_view::{
jni::{JavaVM, objects::GlobalRef},
ndk::native_window::NativeWindow,
};
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState};
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode};
use pollster::FutureExt;
use std::time::Instant;
use wgpu::{
@@ -330,10 +330,10 @@ impl AndroidRenderer {
})
}
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) -> FrameDiagnostics {
pub fn update(&mut self, ui: &mut Ui) -> FrameDiagnostics {
let atlas_pages_grown_prev = self.ui.take_atlas_pages_grown();
let image_bind_group_creates_prev = self.ui.take_image_bind_group_creates();
let stats = self.ui.update(&self.device, &self.queue, ui, render);
let stats = self.ui.update(&self.device, &self.queue, ui);
self.frame_count += 1;
FrameDiagnostics {
masks_resized: stats.masks_resized,
+46 -27
View File
@@ -123,7 +123,7 @@ pub trait HasAndroidUiState: Sized + 'static {
pub trait AndroidAppState: HasAndroidUiState {
fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self;
#[allow(unused_variables)]
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>, render: &mut UiRenderState) -> bool {
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>) -> bool {
false
}
#[allow(unused_variables)]
@@ -167,7 +167,7 @@ impl WindowInsets {
}
pub struct AndroidRsc<State: 'static> {
pub ui: UiData,
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
@@ -181,10 +181,10 @@ impl<State> AndroidRsc<State> {
}
impl<State> UiRsc for AndroidRsc<State> {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
@@ -232,7 +232,6 @@ impl<State: 'static> HasWidgetState for AndroidRsc<State> {
/// Java the same way android-view's own demo does.
pub struct IrisViewPeer<State: AndroidAppState> {
pub(super) rsc: AndroidRsc<State>,
pub(super) render: UiRenderState,
pub(super) state: State,
task_recv: TaskMsgReceiver<AndroidRsc<State>>,
/// The one ruler this view dates everything on: touch samples in
@@ -277,7 +276,9 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let ui_state = self.state.android_state_mut();
let cursor = ui_state.cursor.clone();
let old_focus = ui_state.focus;
self.render
let render_state = self.rsc.ui.render_state();
render_state
.get()
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
let ui_state = self.state.android_state_mut();
@@ -306,7 +307,11 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let ui_state = self.state.android_state_mut();
ui_state.cursor.end_frame();
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
let render_state = self.rsc.ui.render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, self.rsc.widgets())
{
ctx.view.post_frame_callback(&mut ctx.env);
}
}
@@ -360,11 +365,17 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
"render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}",
ui_state.root.is_some(),
self.rsc.widgets().len(),
self.render.active_widgets(),
self.rsc.ui.render_state().get().active_widgets(),
ui_state
.root
.as_ref()
.and_then(|r| self.render.window_region(r, &self.rsc)),
.and_then(|r| {
self.rsc
.ui
.render_state()
.get()
.window_region(r, &self.rsc)
}),
self.window_size(),
);
}
@@ -374,12 +385,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
ctx.view.post_frame_callback(&mut ctx.env);
}
let ui_state = self.state.android_state_mut();
self.render.update(&ui_state.root, &mut self.rsc);
self.rsc.draw(&ui_state.root);
let ui_state = self.state.android_state_mut();
let Some(renderer) = &mut ui_state.renderer else {
return;
};
let frame_diagnostics = renderer.update(&mut self.rsc.ui, &mut self.render);
let frame_diagnostics = renderer.update(&mut self.rsc.ui);
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
log::info!(
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
@@ -400,26 +411,33 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
.android_state_mut()
.frame_report
.record(now, parts, animating);
crate::diagnostics::log_frame(&self.render, now, parts, animating);
let render_state = self.rsc.ui.render_state();
crate::diagnostics::log_frame(&render_state.get(), now, parts, animating);
if crate::diagnostics::trace_enabled() {
let ui_state = self.state.android_state();
log::debug!(
target: "iris::frame",
"render(): after update active={} root_px={:?}",
self.render.active_widgets(),
self.rsc.ui.render_state().get().active_widgets(),
ui_state
.root
.as_ref()
.and_then(|r| self.render.window_region(r, &self.rsc)),
.and_then(|r| {
self.rsc
.ui
.render_state()
.get()
.window_region(r, &self.rsc)
}),
);
}
let ui_state = self.state.android_state_mut();
if let Some(tree_update) =
ui_state
.access
.update(self.rsc.widgets(), &self.render, &self.rsc)
{
if let Some(tree_update) = ui_state.access.update(
self.rsc.widgets(),
&self.rsc.ui.render_state().get(),
&self.rsc,
) {
let ui_state = self.state.android_state_mut();
if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) {
ctx.push_dynamic_deferred_callback(move |env, view| {
@@ -467,7 +485,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.after_input(ctx);
return true;
}
let handled = self.state.back_pressed(&mut self.rsc, &mut self.render);
let handled = self.state.back_pressed(&mut self.rsc);
if handled {
self.after_input(ctx);
}
@@ -646,7 +664,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
// dividing the whole window into a separate logical space, which
// is what used to make every `abs`-unit size (a fixed `.height(56)`
// in particular) mean something different from a `rest`-based one.
self.render.resize((width as f32, height as f32));
self.rsc.ui.resize((width as f32, height as f32));
// `AndroidRenderer::resize` only reconfigures the wgpu surface and
// rewrites the window uniform -- device, atlas, buffers and bind
@@ -779,9 +797,11 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>,
virtual_view_id: jint,
) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(),
render: &self.render,
render: &render_state,
rsc: &self.rsc,
};
let ui_state = self.state.android_state_mut();
@@ -798,9 +818,11 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>,
focus_type: jint,
) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(),
render: &self.render,
render: &render_state,
rsc: &self.rsc,
};
let ui_state = self.state.android_state_mut();
@@ -869,18 +891,15 @@ pub fn new_peer<'local, State: AndroidAppState>(
state: Default::default(),
_state: PhantomData,
};
rsc.ui.text.density = content_scale;
rsc.ui.set_density(content_scale);
let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
let mut state = State::new(ui_state, &mut rsc);
let platform_vm = env.get_java_vm().unwrap();
let platform_view = env.new_global_ref(&view.0).unwrap();
state.platform_ready(&mut rsc, platform_vm, platform_view);
let mut render = UiRenderState::new();
render.set_density(content_scale);
let peer = IrisViewPeer {
rsc,
render,
state,
task_recv,
device_clock: None,
+33 -37
View File
@@ -99,30 +99,18 @@ pub trait DefaultAppState: HasDefaultUiState {
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
#[allow(unused_variables)]
fn event(
&mut self,
event: Self::Event,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
fn event(&mut self, event: Self::Event, rsc: &mut DefaultRsc<Self>) {}
#[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {}
fn exit(&mut self, rsc: &mut DefaultRsc<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, rsc: &mut DefaultRsc<Self>) {}
fn window_attributes() -> WindowAttributes {
Default::default()
}
}
pub struct DefaultRsc<State: 'static> {
pub ui: UiData,
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
@@ -150,11 +138,11 @@ impl<State> DefaultRsc<State> {
}
impl<State> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
@@ -204,7 +192,6 @@ impl<State: 'static> HasWidgetState for DefaultRsc<State> {
pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
render: UiRenderState,
state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
}
@@ -232,26 +219,22 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
// `content_scale`. See `iris_core::TextData::density` for why the
// shaper keeps its own.
let scale = content_scale(default_state.window.as_ref());
rsc.ui.text.density = scale;
rsc.ui.set_density(scale);
let state = State::new(default_state, &mut rsc, proxy);
let mut render = UiRenderState::new();
render.set_density(scale);
Self {
rsc,
state,
render,
task_recv,
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render);
self.state.event(event, &mut self.rsc);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
@@ -290,7 +273,11 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} else {
"move"
};
let t_ms = cursor_state.time.duration_since(render.epoch()).as_millis() as u64;
let render_state = rsc.ui.render_state();
let t_ms = cursor_state
.time
.duration_since(render_state.get().epoch())
.as_millis() as u64;
crate::sense::log_input_event(
action,
cursor_state.pos.x,
@@ -300,7 +287,10 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
);
}
let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
let render_state = rsc.ui.render_state();
render_state
.get()
.run_sensors(rsc, state, cursor_state, window_size);
}
let ui_state = state.default_state_mut();
if old != ui_state.focus
@@ -324,23 +314,25 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
if animating {
ui_state.window.request_redraw();
}
render.update(&ui_state.root, rsc);
ui_state.renderer.update(&mut rsc.ui, render);
rsc.draw(&ui_state.root);
ui_state.renderer.update(&mut rsc.ui);
let mut parts = ui_state.renderer.draw();
parts.total = frame_start.elapsed();
crate::diagnostics::log_frame(render, frame_start, parts, animating);
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), render, rsc) {
let render_state = rsc.ui.render_state();
let render_state = render_state.get();
crate::diagnostics::log_frame(&render_state, frame_start, parts, animating);
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), &render_state, rsc)
{
ui_state.access_adapter.update_if_active(|| tree_update);
}
}
WindowEvent::Resized(size) => {
render.resize((size.width, size.height));
rsc.ui.resize((size.width, size.height));
ui_state.renderer.resize(size)
}
WindowEvent::ScaleFactorChanged { .. } => {
let scale = content_scale(ui_state.window.as_ref());
rsc.ui.text.density = scale;
render.set_density(scale);
rsc.ui.set_density(scale);
ui_state.window.request_redraw();
}
WindowEvent::KeyboardInput { event, .. } => {
@@ -422,16 +414,20 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
_ => (),
}
state.window_event(event, rsc, render);
state.window_event(event, rsc);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
let render_state = rsc.ui.render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, rsc.widgets())
{
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
}
fn exit(&mut self) {
self.state.exit(&mut self.rsc, &mut self.render);
self.state.exit(&mut self.rsc);
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
use crate::task::RequestRedraw;
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState, util::Vec2};
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
use pollster::FutureExt;
use std::sync::Arc;
use std::time::Instant;
@@ -26,8 +26,8 @@ pub struct UiRenderer {
}
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);
}
/// The two waits, so a desktop frame divides up the same way an
+12 -13
View File
@@ -168,7 +168,7 @@ impl OpenUrl for HarnessState {
/// `AndroidRsc` minus the windowing, for the same reason those two are
/// separate types (`AndroidRsc`'s own doc).
pub struct HarnessRsc {
pub ui: UiData,
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
@@ -176,10 +176,10 @@ pub struct HarnessRsc {
}
impl UiRsc for HarnessRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
@@ -239,7 +239,6 @@ impl<I: RscIdx<HarnessRsc>> std::ops::IndexMut<I> for HarnessRsc {
/// the pointer, all advanced by the caller. See the module doc.
pub struct Harness {
pub rsc: HarnessRsc,
pub render: UiRenderState,
pub state: HarnessState,
task_recv: TaskMsgReceiver<HarnessRsc>,
redraws: Arc<RedrawCounter>,
@@ -258,19 +257,16 @@ impl Harness {
let redraws = Arc::new(RedrawCounter::default());
let (tasks, task_recv) = Tasks::init(redraws.clone());
let mut rsc = HarnessRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
tasks,
state: WidgetState::default(),
_state: PhantomData,
};
rsc.ui.text.density = density;
let mut render = UiRenderState::new();
render.set_density(density);
render.resize(size);
rsc.ui.set_density(density);
rsc.ui.resize(size);
Self {
rsc,
render,
state: HarnessState::new(),
task_recv,
redraws,
@@ -302,15 +298,16 @@ impl Harness {
let now = self.at(t_ms);
let at = Instant::now();
let animating = self.rsc.ui.tick_animations(now);
self.render.update(&self.state.root, &mut self.rsc);
self.rsc.draw(&self.state.root);
// No GPU here, so there is nothing to acquire and nothing to
// submit: the frame is all `build`, which is honest rather than
// zero-filled (`FrameParts::whole`). `layout`/`redraw`/
// `primitives` are still real, because `render.update` just ran;
// see `iris::diagnostics::log_frame`'s own doc for why this reads
// those back rather than timing anything itself.
let render_state = self.rsc.ui.render_state();
crate::diagnostics::log_frame(
&self.render,
&render_state.get(),
now,
FrameParts::whole(at.elapsed()),
animating,
@@ -344,7 +341,9 @@ impl Harness {
}
crate::sense::log_input_event(action.word(), pos.x, pos.y, t_ms, &[]);
let cursor = self.cursor.clone();
self.render
let render_state = self.rsc.ui.render_state();
render_state
.get()
.run_sensors(&mut self.rsc, &mut self.state, cursor, self.size);
self.frame(t_ms);
self.cursor.end_frame();
+28 -78
View File
@@ -2,14 +2,14 @@ use crate::prelude::*;
use std::{cell::Cell, cell::RefCell, rc::Rc};
pub(crate) struct TestRsc {
pub(crate) ui: UiData,
pub(crate) ui: Ui,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
}
@@ -87,9 +87,7 @@ impl Widget for TracedParent {
#[test]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
let stack = rsc.ui.widgets.add_strong(Stack {
@@ -125,9 +123,7 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
#[test]
fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
@@ -155,9 +151,7 @@ fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
#[test]
fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0,
@@ -183,9 +177,7 @@ fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
#[test]
fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let changed_draws = Rc::new(Cell::new(0));
let sibling_draws = Rc::new(Cell::new(0));
let changed = rsc.ui.widgets.add_strong(CountedLeaf {
@@ -230,9 +222,7 @@ fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
#[test]
fn a_child_coordinate_offset_moves_only_the_child_subtree() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let child = rsc.ui.widgets.add_strong(FixedRect(40.0));
let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(ChildOffset {
@@ -274,9 +264,7 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
#[test]
fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let fill = rsc.ui.widgets.add_strong(Sized {
@@ -330,9 +318,7 @@ fn scrolled_rects(
#[test]
fn an_unchanged_frame_draws_and_rewrites_nothing() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 20000.0));
@@ -348,9 +334,7 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() {
#[test]
fn scrolling_moves_in_o1_without_a_redraw() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
@@ -370,9 +354,7 @@ fn scrolling_moves_in_o1_without_a_redraw() {
#[test]
fn hit_testing_follows_a_scrolled_widget() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new();
render.resize((800.0, 600.0));
@@ -398,9 +380,7 @@ fn hit_testing_follows_a_scrolled_widget() {
#[test]
fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
@@ -424,9 +404,7 @@ fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
#[test]
fn a_mask_stays_put_while_its_scrolled_content_moves() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 500);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
@@ -473,9 +451,7 @@ fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget)
#[test]
fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (field, root) = composer_like_tree(&mut rsc);
let mut render = UiRenderState::new();
@@ -515,9 +491,7 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
#[test]
fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
@@ -565,9 +539,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
#[test]
fn a_panned_widgets_own_hit_box_moves_exactly_once() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
@@ -601,9 +573,7 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
#[test]
fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked {
shape: None,
@@ -648,9 +618,7 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
#[test]
fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
@@ -690,9 +658,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
#[test]
fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let spacer = rsc.ui.widgets.add_strong(Sized {
inner: top.any(),
@@ -757,9 +723,7 @@ impl Widget for MoveThenPlace {
#[test]
fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(),
@@ -839,9 +803,7 @@ fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u3
#[test]
fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc);
let radius = render
@@ -879,9 +841,7 @@ fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
#[test]
fn a_mask_s_shape_decides_what_can_be_pressed() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc);
@@ -907,9 +867,7 @@ fn a_mask_s_shape_decides_what_can_be_pressed() {
#[test]
fn nested_masks_multiply_their_coverage() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child_id = child.id();
@@ -970,9 +928,7 @@ fn nested_masks_multiply_their_coverage() {
#[test]
fn a_plain_mask_still_clips_to_a_square_box() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let root = rsc
.ui
@@ -1002,9 +958,7 @@ fn a_plain_mask_still_clips_to_a_square_box() {
#[test]
fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
for (name, pin, want) in [("read", Pin::Start, 0.0), ("written", Pin::End, 4900.0)] {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
let tall = rsc.ui.widgets.add_strong(Sized {
inner: fill,
@@ -1037,9 +991,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
const PAD: f32 = 4.0;
const ROW: f32 = 20.0;
const HEADER: f32 = 30.0;
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any();
let header_id = header_fill.id();
let header = rsc.ui.widgets.add_strong(Sized {
@@ -1120,9 +1072,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
const SECOND: f32 = 70.0;
const GAP: f32 = 8.0;
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let first_id = first.id();
let first = rsc.ui.widgets.add_strong(Sized {
+6 -3
View File
@@ -402,10 +402,11 @@ pub trait OverlayRscExt: HasEvents {
where
W: WidgetLike<Self, Tag>,
{
let render_handle = self.ui().render_state();
let (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id())?;
.path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self);
let content = modal.add(self);
@@ -447,10 +448,11 @@ pub trait OverlayRscExt: HasEvents {
{
match options.kind {
OverlayKind::Single => {
let render_handle = self.ui().render_state();
let target = self
.events()
.controllers
.nearest_id::<SingleOverlayController>(origin.id())?;
.nearest_id::<SingleOverlayController>(origin.id(), &render_handle.get())?;
let overlay = overlay.add_strong(self).any();
self.with_controller(target, |single: &mut SingleOverlayController, rsc| {
single.open(
@@ -463,10 +465,11 @@ pub trait OverlayRscExt: HasEvents {
})
}
OverlayKind::Stackable => {
let render_handle = self.ui().render_state();
let (target, path) = self
.events()
.controllers
.path_to::<StackableOverlayController>(origin.id())?;
.path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self);
let overlay = overlay.add_strong(self).any();
open_stackable_at(
+58 -11
View File
@@ -2,15 +2,15 @@ use crate::prelude::*;
use std::{cell::Cell, rc::Rc, time::Instant};
struct SenseRsc {
ui: UiData,
ui: Ui,
events: EventManager<SenseRsc>,
}
impl UiRsc for SenseRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
@@ -28,6 +28,53 @@ impl HasState for SenseRsc {
type State = ();
}
#[derive(Clone)]
struct RenderProbe;
impl Event for RenderProbe {}
#[test]
fn an_event_can_read_the_completed_frame_while_mutating_ui_data() {
let mut rsc = SenseRsc {
ui: Ui::default(),
events: EventManager::default(),
};
rsc.ui.resize((100.0, 100.0));
let widget = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let weak = widget.weak();
let saw_active = Rc::new(Cell::new(false));
rsc.register_event(weak, RenderProbe, {
let saw_active = saw_active.clone();
move |_, rsc| {
let render_handle = rsc.ui().render_state();
let render_state = render_handle.get();
saw_active.set(render_state.active.contains_key(&weak.id()));
rsc.widgets_mut().set_label(weak, "changed".to_string());
assert!(render_state.active.contains_key(&weak.id()));
}
});
rsc.draw(&widget.any());
rsc.run_event::<RenderProbe>(weak, (), &mut ());
assert!(saw_active.get());
assert_eq!(rsc.widgets().label(weak), "changed");
}
#[test]
#[should_panic(expected = "render state cannot be mutated while it is being read")]
fn an_event_cannot_start_a_draw() {
let mut rsc = SenseRsc {
ui: Ui::default(),
events: EventManager::default(),
};
let widget = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let weak = widget.weak();
rsc.register_event(weak, RenderProbe, |_, rsc| rsc.draw(None));
rsc.run_event::<RenderProbe>(weak, (), &mut ());
}
impl HasEvents for SenseRsc {
fn events(&self) -> &EventManager<Self> {
&self.events
@@ -50,7 +97,7 @@ fn cursor_at(pos: Vec2) -> CursorState {
#[test]
fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
@@ -121,7 +168,7 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
#[test]
fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
@@ -180,7 +227,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
#[test]
fn capturing_one_widget_starves_every_other_widget_of_events() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
@@ -225,7 +272,7 @@ fn capturing_one_widget_starves_every_other_widget_of_events() {
#[test]
fn a_finger_drag_over_a_scroll_area_pans_it() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
@@ -316,7 +363,7 @@ fn the_clock_orders_samples_across_events() {
#[test]
fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let scroll_strong = rect(PaintId::WHITE)
@@ -370,7 +417,7 @@ fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
#[test]
fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
@@ -469,7 +516,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
),
] {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let seen = Rc::new(Cell::new(None));
@@ -526,7 +573,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
#[test]
fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
let mut rsc = SenseRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
+27 -75
View File
@@ -833,14 +833,14 @@ mod tests {
}
struct TestRsc {
ui: UiData,
ui: Ui,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
}
@@ -882,9 +882,7 @@ mod tests {
#[test]
fn a_list_shorter_than_the_viewport_is_drawn_whole_and_stays_at_the_bottom() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -912,9 +910,7 @@ mod tests {
#[test]
fn a_dir_up_span_grows_upward_from_item_zero() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::UP, Pin::End);
let rows = push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -949,9 +945,7 @@ mod tests {
#[test]
fn a_delta_moves_both_directions_the_same_way_on_screen() {
let moved_by = |dir: Dir| {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(dir, Pin::End);
let keys: Vec<RowKey> = (0..10).collect();
let rows = push_rows(&mut rsc, &mut list, &keys, 20.0);
@@ -1003,9 +997,7 @@ mod tests {
#[test]
fn a_reversed_span_hit_tests_in_screen_space() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::UP, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1030,9 +1022,7 @@ mod tests {
#[test]
fn bottom_anchored_by_default() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1078,9 +1068,7 @@ mod tests {
fn a_row_that_changes_height_draws_its_background_at_the_new_height_immediately() {
for key_to_change in 0..5u64 {
for new_height in [50.0f32, 8.0] {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut rows = Vec::new();
for key in 0..5u64 {
@@ -1118,9 +1106,7 @@ mod tests {
#[test]
fn a_fill_shaped_background_is_not_left_oversized() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut bg_ids = Vec::new();
for key in 0..5u64 {
@@ -1148,9 +1134,7 @@ mod tests {
#[test]
fn insert_above_anchor_is_o1_and_does_not_move_visible_rows() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[10, 11, 12], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1188,9 +1172,7 @@ mod tests {
#[test]
fn expanding_a_row_holds_the_edge_nearest_the_tap() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let rows = push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1234,9 +1216,7 @@ mod tests {
#[test]
fn expanding_a_row_holds_the_bottom_edge_when_tap_is_lower() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let rows = push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1270,9 +1250,7 @@ mod tests {
#[test]
fn moves_stay_o1_across_list_size() {
for &n in &[20usize, 200, 2000] {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let keys: Vec<RowKey> = (0..n as u64).collect();
push_rows(&mut rsc, &mut list, &keys, 20.0);
@@ -1300,9 +1278,7 @@ mod tests {
#[test]
fn a_large_accumulated_offset_rebases_without_moving_the_content() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let keys: Vec<RowKey> = (0..100).collect();
let rows = push_rows(&mut rsc, &mut list, &keys, 1_000.0);
@@ -1348,9 +1324,7 @@ mod tests {
#[test]
fn replacing_the_last_row_stays_pinned_to_the_bottom() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1392,9 +1366,7 @@ mod tests {
#[test]
fn replace_back_forgets_the_evicted_keys_own_height() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1429,9 +1401,7 @@ mod tests {
#[test]
fn replacing_the_last_row_out_of_view_does_not_move_visible_rows() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list);
@@ -1474,9 +1444,7 @@ mod tests {
#[test]
fn replacing_the_last_row_many_times_does_not_leak_primitives() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
for key in 0..5u64 {
let (_bg_id, row) = background_styled_row(&mut rsc, 20.0);
@@ -1511,9 +1479,7 @@ mod tests {
#[test]
fn an_ancestor_redrawing_a_dirty_row_leaves_no_stale_copy() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut rows = Vec::new();
for key in 0..5u64 {
@@ -1574,9 +1540,7 @@ mod tests {
#[test]
fn a_fling_moves_the_list_and_then_settles() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
@@ -1602,9 +1566,7 @@ mod tests {
#[test]
fn a_registered_fling_is_driven_by_tick_animations_and_then_unregisters() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap());
@@ -1637,9 +1599,7 @@ mod tests {
#[test]
fn a_negative_delta_moves_toward_the_end() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(2000.0);
@@ -1661,9 +1621,7 @@ mod tests {
#[test]
fn amt_counts_only_what_the_child_could_take() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100.0);
render.update(&root, &mut rsc);
@@ -1684,9 +1642,7 @@ mod tests {
#[test]
fn a_fling_stops_at_the_first_row() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().fling(50_000.0);
@@ -1710,9 +1666,7 @@ mod tests {
#[test]
fn scrolling_past_the_start_lands_on_it_in_the_same_frame() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100_000.0);
@@ -1734,9 +1688,7 @@ mod tests {
#[test]
fn anchor_position_display_reports_slot_and_offset() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let (list_weak, root, mut render) = build_flingable_list(&mut rsc);
let _ = (&root, &mut render);
let list_ref = rsc.ui.widgets.get(&list_weak).unwrap();
+1 -4
View File
@@ -87,7 +87,6 @@ mod tests {
use super::*;
use crate::layout_tests::TestRsc;
use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests};
use iris_core::UiData;
use std::time::Duration;
fn area() -> (Fixture, WidgetId) {
@@ -95,9 +94,7 @@ mod tests {
}
fn area_on(axis: Axis) -> (Fixture, WidgetId) {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
let id = fill.id();
let long = Some(Len::abs(1000.0));
+1 -1
View File
@@ -498,7 +498,7 @@ pub trait TextEditable {
impl<I: IdLike<Widget = TextEdit>> TextEditable for I {
fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> {
let ui = ui.ui_mut();
let ui: &mut UiData = ui.ui_mut();
TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text,
+2 -6
View File
@@ -222,9 +222,7 @@ mod tests {
use crate::prelude::*;
fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, StrongWidget) {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let text = wtext(content).add_strong(&mut rsc);
let id = text.weak();
let root = text.any();
@@ -306,9 +304,7 @@ mod tests {
#[test]
fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() {
let mut rsc = TestRsc {
ui: UiData::default(),
};
let mut rsc = TestRsc { ui: Ui::default() };
let root = wtext("hello there")
.size(18)
.color(PaintId::WHITE)
+9 -9
View File
@@ -161,7 +161,7 @@ pub trait TextSelectable {
impl<I: IdLike<Widget = Text>> TextSelectable for I {
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> {
let ui = ui.ui_mut();
let ui: &mut UiData = ui.ui_mut();
TextSelectionCtx {
view: &mut ui.widgets.get_mut(self).unwrap().view,
data: &mut ui.text,
@@ -250,7 +250,7 @@ impl SelectionController {
id: WidgetId,
f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T,
) -> Option<T> {
let ui = rsc.ui_mut();
let ui: &mut UiData = rsc.ui_mut();
let text = ui
.widgets
.get_dyn_mut(id)?
@@ -481,16 +481,16 @@ mod controller_tests {
use super::*;
struct TestRsc {
ui: UiData,
ui: Ui,
events: EventManager<TestRsc>,
}
impl UiRsc for TestRsc {
fn ui(&self) -> &UiData {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut UiData {
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
@@ -532,7 +532,7 @@ mod controller_tests {
StrongWidget,
) {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let first = wtext("first").add(&mut rsc);
@@ -571,7 +571,7 @@ mod controller_tests {
#[test]
fn a_widget_without_an_order_override_keeps_draw_order() {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let first = wtext("back").add(&mut rsc);
@@ -604,7 +604,7 @@ mod controller_tests {
#[test]
fn nearest_controller_prefers_the_inner_scope() {
let mut rsc = TestRsc {
ui: UiData::default(),
ui: Ui::default(),
events: EventManager::default(),
};
let leaf = wtext("leaf").add(&mut rsc);
@@ -624,7 +624,7 @@ mod controller_tests {
let found = rsc
.events()
.controllers
.nearest_id::<SelectionController>(leaf.id())
.nearest_id::<SelectionController>(leaf.id(), &render)
.unwrap();
assert_eq!(found.host(), inner.id());
}
+1 -6
View File
@@ -51,12 +51,7 @@ fn solid_paints_and_images_round_trip_through_an_srgb_target() {
}
fn render(gpu: &Gpu, renderer: &mut UiRenderNode, harness: &mut Harness) -> [[u8; 4]; 2] {
renderer.update(
&gpu.device,
&gpu.queue,
&mut harness.rsc.ui,
&mut harness.render,
);
renderer.update(&gpu.device, &gpu.queue, &mut harness.rsc.ui);
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Iris colour-space target"),
size: wgpu::Extent3d {