2 Commits
Author SHA1 Message Date
iris a8602c1626 Clean up shared UI runtime state 2026-09-11 00:55:33 -04:00
iris f49b284b46 iris: fold render state into Ui 2026-09-10 23:58:43 -04:00
61 changed files with 1117 additions and 1247 deletions

No files matched your search

+6 -6
View File
@@ -32,17 +32,17 @@ fn typed_argv() -> Option<String> {
} }
fn main() { fn main() {
DefaultApp::<Client>::run(); DesktopApp::<Client>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
pub struct Client { pub struct Client {
ui_state: DefaultUiState, ui_state: DesktopUiState,
#[allow(dead_code)] #[allow(dead_code)]
screen: Option<ai_app::ui::TranscriptScreen>, screen: Option<ai_app::ui::TranscriptScreen>,
} }
impl DefaultAppState for Client { impl DesktopAppState for Client {
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
WindowAttributes::default() WindowAttributes::default()
.with_title("iris transcript (bench fixture)") .with_title("iris transcript (bench fixture)")
@@ -53,8 +53,8 @@ impl DefaultAppState for Client {
} }
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) { let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
+7 -7
View File
@@ -3,12 +3,12 @@ use ai_app::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRo
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<Client>::run(); DesktopApp::<Client>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
pub struct Client { pub struct Client {
ui_state: DefaultUiState, ui_state: DesktopUiState,
#[allow(dead_code)] #[allow(dead_code)]
screen: ai_app::ui::TranscriptScreen, screen: ai_app::ui::TranscriptScreen,
} }
@@ -157,13 +157,13 @@ fn fold_event(items: Vec<Item>, seq: u64) -> Vec<Item> {
> A quoted line, to show the bar and the indent. > A quoted line, to show the bar and the indent.
"; ";
impl DefaultAppState for Client { impl DesktopAppState for Client {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows()); let mut screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows());
screen.push_row( screen.push_row(
rsc, rsc,
&FoldedRow::Single(TranscriptItem::CommandRow { &FoldedRow::Single(TranscriptItem::CommandRow {
+16 -50
View File
@@ -3,13 +3,13 @@
//! `android_logger` as the logger to forward to and nothing else. //! `android_logger` as the logger to forward to and nothing else.
use crate::client::log_ring::{self, LogRing}; use crate::client::log_ring::{self, LogRing};
use std::{
fs, panic,
path::{Path, PathBuf},
sync::OnceLock,
};
/// Installs the ring in front of `android_logger`, so `logcat` still sees /// Installs the in-process ring in front of Android's logger.
/// exactly what it saw before and the ring sees it too.
///
/// Called once, from `JNI_OnLoad`. A second call is refused by `log`
/// itself; the message says which caller, since two initialisation paths
/// is a programmer error rather than something to recover from.
pub fn install(max_level: log::LevelFilter) { pub fn install(max_level: log::LevelFilter) {
let inner = android_logger::AndroidLogger::new( let inner = android_logger::AndroidLogger::new(
android_logger::Config::default() android_logger::Config::default()
@@ -23,10 +23,6 @@ pub fn install(max_level: log::LevelFilter) {
) )
.is_err() .is_err()
{ {
// Not a panic: a logger already installed means logging works,
// just without the ring, and taking the app down over a
// diagnostic would be worse than the diagnostic being missing.
// The line goes through whatever logger did win.
log::warn!("iris app log: a logger was already installed, so there is no ring"); log::warn!("iris app log: a logger was already installed, so there is no ring");
} }
install_panic_hook(); install_panic_hook();
@@ -40,9 +36,6 @@ pub fn ring() -> &'static LogRing {
pub fn diagnostics_line() -> String { pub fn diagnostics_line() -> String {
let where_to_read = match crate::android::devlog::authority() { let where_to_read = match crate::android::devlog::authority() {
Some(authority) => format!("devlog provider: content://{authority}"), Some(authority) => format!("devlog provider: content://{authority}"),
// Not "off": Android creates a provider lazily, so this is what
// "nobody has asked for it yet" looks like, and it is a different
// thing from a build that does not have one.
None => "devlog provider: declared, not created yet".to_string(), None => "devlog provider: declared, not created yet".to_string(),
}; };
format!("{}\n{where_to_read}", ring().summary()) format!("{}\n{where_to_read}", ring().summary())
@@ -53,26 +46,17 @@ pub fn diagnostics_line() -> String {
/// start. /// start.
const CRASH_FILE: &str = "last-panic.txt"; const CRASH_FILE: &str = "last-panic.txt";
/// How many of the dying run's own log lines the panic hook saves with /// Enough preceding log lines to explain a crash without evicting the next run.
/// the panic, and [`set_crash_dir`] replays.
///
/// The panic's message and location say *what* broke; these say what the
/// app was doing on the way there, which is the half that is otherwise
/// unrecoverable -- the ring is memory only, so an abort takes every line
/// before the panic with it. Bounded rather than the whole ring because
/// this is written by a hook on a process that is about to die, and
/// because the replay pushes each line into the new run's ring, where an
/// unbounded paste would evict the run that is actually being watched.
const CRASH_CONTEXT_LINES: usize = 80; const CRASH_CONTEXT_LINES: usize = 80;
const PREVIOUS_RUN_TARGET: &str = "previous_run"; const PREVIOUS_RUN_TARGET: &str = "previous_run";
static CRASH_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new(); static CRASH_PATH: OnceLock<PathBuf> = OnceLock::new();
/// Copies aborting panics into the device-readable log ring. /// Copies aborting panics into the device-readable log ring.
fn install_panic_hook() { fn install_panic_hook() {
let previous = std::panic::take_hook(); let previous = panic::take_hook();
std::panic::set_hook(Box::new(move |info| { panic::set_hook(Box::new(move |info| {
let where_at = match info.location() { let where_at = match info.location() {
Some(at) => format!("{}:{}:{}", at.file(), at.line(), at.column()), Some(at) => format!("{}:{}:{}", at.file(), at.line(), at.column()),
None => "an unknown location".to_string(), None => "an unknown location".to_string(),
@@ -81,41 +65,23 @@ fn install_panic_hook() {
let line = format!("iris panic at {where_at}: {message}"); let line = format!("iris panic at {where_at}: {message}");
log::error!("{line}"); log::error!("{line}");
if let Some(path) = CRASH_PATH.get() { if let Some(path) = CRASH_PATH.get() {
// The panic line first, then what the app was doing before
// it: one file, split again on that first newline by
// `set_crash_dir`.
let context = ring() let context = ring()
.try_tail_text(CRASH_CONTEXT_LINES) .try_tail_text(CRASH_CONTEXT_LINES)
.unwrap_or_else(|| { .unwrap_or_else(|| {
"(the log ring was locked as this run died; no context)".to_string() "(the log ring was locked as this run died; no context)".to_string()
}); });
// Best effort by design: a panic is already the failure, and let _ = fs::write(path, format!("{line}\n{context}"));
// failing to record it must not become a second one.
let _ = std::fs::write(path, format!("{line}\n{context}"));
} }
previous(info); previous(info);
})); }));
} }
/// Tells the panic hook where to leave its report, and replays the report /// Configures crash persistence and replays a report left by the previous run.
/// a previous run left there into the ring before deleting it. pub fn set_crash_dir(dir: &Path) {
///
/// Called from **both** `MainActivity.nativeSetFilesDir` and
/// `DevLogProvider.nativeReady` -- whichever of the two runs first in
/// this process, since after a crash Dev Updater's query starts the
/// process for the provider alone and no activity ever runs. Safe to call
/// twice: the file is gone after the first, so the second finds nothing
/// and says nothing. The panic itself is replayed at `error` level and
/// says it is from the previous run, so a crash loop shows the reason it
/// is looping in the Runtime tab of the run that is still up.
pub fn set_crash_dir(dir: &std::path::Path) {
let path = dir.join(CRASH_FILE); let path = dir.join(CRASH_FILE);
if let Ok(previous) = std::fs::read_to_string(&path) { if let Ok(previous) = fs::read_to_string(&path) {
// Delete before replaying rather than after: a replay that itself // Delete first so a panic during replay cannot create a replay loop.
// panicked would otherwise leave the file to be replayed again on let _ = fs::remove_file(&path);
// every start, and a crash loop nothing can get out of is worse
// than one report lost.
let _ = std::fs::remove_file(&path);
replay_crash(&previous); replay_crash(&previous);
} }
let _ = CRASH_PATH.set(path); let _ = CRASH_PATH.set(path);
+18 -11
View File
@@ -1,12 +1,19 @@
use crate::android::bench_jni::PlatformHandle; use crate::android::bench_jni::PlatformHandle;
use crate::client::transcript_fold::{TranscriptItem, fold_event}; use crate::client::transcript_fold::{TranscriptItem, fold_event};
use crate::ui::{self, TranscriptScreen};
use android_view::jni::{JavaVM, objects::GlobalRef}; use android_view::jni::{JavaVM, objects::GlobalRef};
use event_model::SeqEvent; use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*; use iris::prelude::*;
use std::sync::atomic::{AtomicBool, Ordering}; use std::{
use std::sync::{Arc, Mutex}; fs, mem,
use std::time::{Duration, Instant}; sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
mpsc,
},
time::{Duration, Instant},
};
const STREAM_EVENTS_PER_SEC: u64 = 20; const STREAM_EVENTS_PER_SEC: u64 = 20;
const STREAM_SECONDS: u64 = 20; const STREAM_SECONDS: u64 = 20;
@@ -40,7 +47,7 @@ pub struct BenchClient {
content: WeakWidget<WidgetPtr>, content: WeakWidget<WidgetPtr>,
report_display: WeakWidget<TextEdit>, report_display: WeakWidget<TextEdit>,
top_bar: WeakWidget<WidgetPtr>, top_bar: WeakWidget<WidgetPtr>,
screen: Option<crate::ui::TranscriptScreen>, screen: Option<TranscriptScreen>,
items: Vec<TranscriptItem>, items: Vec<TranscriptItem>,
stream_tail: Vec<SeqEvent>, stream_tail: Vec<SeqEvent>,
platform: Option<Arc<PlatformHandle>>, platform: Option<Arc<PlatformHandle>>,
@@ -80,7 +87,7 @@ fn process_cpu_ms() -> Option<u64> {
// SAFETY: `rusage` is a plain-old-data struct `getrusage` fully // SAFETY: `rusage` is a plain-old-data struct `getrusage` fully
// initialises on success; on failure it is never read. // initialises on success; on failure it is never read.
unsafe { unsafe {
let mut usage: libc::rusage = std::mem::zeroed(); let mut usage: libc::rusage = mem::zeroed();
if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 { if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 {
return None; return None;
} }
@@ -91,7 +98,7 @@ fn process_cpu_ms() -> Option<u64> {
} }
fn peak_rss_kb() -> Option<u64> { fn peak_rss_kb() -> Option<u64> {
std::fs::read_to_string("/proc/self/status") fs::read_to_string("/proc/self/status")
.ok()? .ok()?
.lines() .lines()
.find_map(|line| line.strip_prefix("VmHWM:")) .find_map(|line| line.strip_prefix("VmHWM:"))
@@ -174,7 +181,7 @@ impl AndroidAppState for BenchClient {
last_top_pad: 0.0, last_top_pad: 0.0,
}; };
match crate::ui::fixture::build_screen(rsc) { match ui::fixture::build_screen(rsc) {
Ok((opened, tree)) => { Ok((opened, tree)) => {
client.items = opened.items; client.items = opened.items;
client.stream_tail = opened.stream_tail; client.stream_tail = opened.stream_tail;
@@ -192,7 +199,7 @@ impl AndroidAppState for BenchClient {
self.platform = Some(Arc::new(PlatformHandle::new(vm, view))); 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 false
} }
@@ -392,7 +399,7 @@ impl BenchClient {
} }
fn rebuild_transcript(&mut self, rsc: &mut Rsc) { fn rebuild_transcript(&mut self, rsc: &mut Rsc) {
let (screen, tree) = crate::ui::build_tree(rsc, crate::ui::fixture::rows(&self.items)); let (screen, tree) = ui::build_tree(rsc, ui::fixture::rows(&self.items));
(self.content)(rsc).set(tree); (self.content)(rsc).set(tree);
self.screen = Some(screen); self.screen = Some(screen);
} }
@@ -653,7 +660,7 @@ where
T: Send + 'static, T: Send + 'static,
F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static, F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static,
{ {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = mpsc::channel();
ctx.update(move |state: &mut BenchClient, rsc| { ctx.update(move |state: &mut BenchClient, rsc| {
let _ = tx.send(f(state, rsc)); let _ = tx.send(f(state, rsc));
}); });
@@ -769,7 +776,7 @@ async fn run_stream_phase(
ctx.update(move |state: &mut BenchClient, rsc| { ctx.update(move |state: &mut BenchClient, rsc| {
let old_items = state.items.clone(); let old_items = state.items.clone();
state.items = fold_event(&state.items, &event); state.items = fold_event(&state.items, &event);
match &state.screen { match state.screen.as_mut() {
Some(screen) => screen.apply(rsc, &old_items, &state.items), Some(screen) => screen.apply(rsc, &old_items, &state.items),
None => state.rebuild_transcript(rsc), None => state.rebuild_transcript(rsc),
} }
+3 -3
View File
@@ -4,7 +4,7 @@
use android_view::jni::JNIEnv; use android_view::jni::JNIEnv;
use android_view::jni::objects::{JClass, JObject, JString}; use android_view::jni::objects::{JClass, JObject, JString};
use android_view::jni::sys::{jlong, jobjectArray}; use android_view::jni::sys::{jlong, jobjectArray};
use std::sync::OnceLock; use std::{path::Path, ptr, sync::OnceLock};
/// Gated with its one reader: the tabs demo links no `client-core` and so /// Gated with its one reader: the tabs demo links no `client-core` and so
/// has no ring to lay out, and an ungated constant is a warning in that /// has no ring to lay out, and an ungated constant is a warning in that
@@ -43,7 +43,7 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady(
) { ) {
#[cfg(feature = "transcript-screen")] #[cfg(feature = "transcript-screen")]
if let Some(dir) = string_arg(&mut env, &files_dir) { if let Some(dir) = string_arg(&mut env, &files_dir) {
crate::android::app_log::set_crash_dir(std::path::Path::new(&dir)); crate::android::app_log::set_crash_dir(Path::new(&dir));
} }
#[cfg(not(feature = "transcript-screen"))] #[cfg(not(feature = "transcript-screen"))]
let _ = &files_dir; let _ = &files_dir;
@@ -138,7 +138,7 @@ fn line_fields(_since: u64) -> Vec<String> {
/// down to report that its diagnostic is unavailable would be worse than /// down to report that its diagnostic is unavailable would be worse than
/// the diagnostic being unavailable. /// the diagnostic being unavailable.
fn string_array(env: &mut JNIEnv, fields: &[String]) -> jobjectArray { fn string_array(env: &mut JNIEnv, fields: &[String]) -> jobjectArray {
let null = std::ptr::null_mut(); let null = ptr::null_mut();
let Ok(class) = env.find_class("java/lang/String") else { let Ok(class) = env.find_class("java/lang/String") else {
return null; return null;
}; };
+8 -7
View File
@@ -9,10 +9,11 @@ use android_view::{
}; };
#[cfg(not(feature = "transcript-screen"))] #[cfg(not(feature = "transcript-screen"))]
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
#[cfg(not(feature = "transcript-screen"))]
use iris::prelude::*;
use log::LevelFilter; use log::LevelFilter;
use std::ffi::c_void; use std::{
ffi::c_void,
path::{Path, PathBuf},
};
/// The app's own log ring and its upload -- only where `client-core` is /// The app's own log ring and its upload -- only where `client-core` is
/// linked, which is every build that has a server to send to. The plain /// linked, which is every build that has a server to send to. The plain
@@ -52,14 +53,14 @@ impl HasAndroidUiState for Client {
impl AndroidAppState for Client { impl AndroidAppState for Client {
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self { fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
// `widgets.info` is the winit example's frame-debug readout, kept // `widgets.info` is the winit example's frame-debug readout, kept
// current from `DefaultAppState::window_event` -- android-view has // current from `DesktopAppState::window_event` -- android-view has
// no per-frame hook to drive the equivalent from here yet, so it // no per-frame hook to drive the equivalent from here yet, so it
// is left at its built "" text rather than wired to nothing. // is left at its built "" text rather than wired to nothing.
let _ = tabs_ui::build(rsc, &mut ui_state); let _ = tabs_ui::build(rsc, &mut ui_state);
Self { ui_state } 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 false
} }
} }
@@ -125,8 +126,8 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeSetFilesDir
}; };
#[cfg(feature = "transcript-screen")] #[cfg(feature = "transcript-screen")]
{ {
app_log::set_crash_dir(std::path::Path::new(&dir)); app_log::set_crash_dir(Path::new(&dir));
enrollment::set_files_dir(std::path::PathBuf::from(&dir)); enrollment::set_files_dir(PathBuf::from(&dir));
} }
log::debug!("iris app: files directory is {dir}"); log::debug!("iris app: files directory is {dir}");
} }
+17 -9
View File
@@ -1,11 +1,19 @@
use crate::client::api::{ApiClient, UreqTransport}; use crate::client::api::{ApiClient, UreqTransport};
use crate::client::event_stream::{StreamItem, follow_session_events}; use crate::client::event_stream::{StreamItem, follow_session_events};
use crate::client::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs}; use crate::client::transcript_fold::{
TranscriptItem, fold_event, fold_page, group_tool_runs, raw_seq,
};
use crate::ui::{self, TranscriptScreen};
use event_model::SeqEvent; use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*; use iris::prelude::*;
use std::sync::Arc; use std::{
use std::sync::atomic::{AtomicU64, Ordering}; sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
thread,
};
pub struct TranscriptClient { pub struct TranscriptClient {
ui_state: AndroidUiState, ui_state: AndroidUiState,
@@ -15,7 +23,7 @@ pub struct TranscriptClient {
/// `set` calls the way `desktop-app`'s `transcript_ptr` isn't touched /// `set` calls the way `desktop-app`'s `transcript_ptr` isn't touched
/// by rebuilding the session list beside it. /// by rebuilding the session list beside it.
content: WeakWidget<WidgetPtr>, content: WeakWidget<WidgetPtr>,
screen: Option<crate::ui::TranscriptScreen>, screen: Option<TranscriptScreen>,
items: Vec<TranscriptItem>, items: Vec<TranscriptItem>,
session_id: Option<String>, session_id: Option<String>,
generation: Arc<AtomicU64>, generation: Arc<AtomicU64>,
@@ -113,7 +121,7 @@ impl AndroidAppState for TranscriptClient {
client 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" // No screen stack of its own -- same "let the activity finish"
// answer `iris-android-app`'s tabs `Client` already gives. // answer `iris-android-app`'s tabs `Client` already gives.
false false
@@ -189,7 +197,7 @@ impl TranscriptClient {
.as_ref() .as_ref()
.ok() .ok()
.and_then(|values| values.last()) .and_then(|values| values.last())
.and_then(crate::client::transcript_fold::raw_seq) .and_then(raw_seq)
.unwrap_or(0); .unwrap_or(0);
let result = page.and_then(|values| fold_page(&values)); let result = page.and_then(|values| fold_page(&values));
@@ -251,7 +259,7 @@ impl TranscriptClient {
.filter(|t| !t.is_empty()); .filter(|t| !t.is_empty());
let rows = group_tool_runs(&self.items); let rows = group_tool_runs(&self.items);
let (screen, tree) = crate::ui::build_tree(rsc, rows); let (screen, tree) = ui::build_tree(rsc, rows);
if let Some(text) = in_progress { if let Some(text) = in_progress {
screen.composer.field.edit(rsc).set(&text); screen.composer.field.edit(rsc).set(&text);
@@ -274,14 +282,14 @@ impl TranscriptClient {
fn apply_event(&mut self, rsc: &mut AndroidRsc<Self>, event: &SeqEvent) { fn apply_event(&mut self, rsc: &mut AndroidRsc<Self>, event: &SeqEvent) {
let old_items = self.items.clone(); let old_items = self.items.clone();
self.items = fold_event(&self.items, event); self.items = fold_event(&self.items, event);
match &self.screen { match self.screen.as_mut() {
Some(screen) => screen.apply(rsc, &old_items, &self.items), Some(screen) => screen.apply(rsc, &old_items, &self.items),
None => self.rebuild_transcript(rsc), None => self.rebuild_transcript(rsc),
} }
} }
fn send_message(&mut self, session_id: String, text: String) { fn send_message(&mut self, session_id: String, text: String) {
std::thread::spawn(move || { thread::spawn(move || {
if let Ok(transport) = build_transport() { if let Ok(transport) = build_transport() {
let api = ApiClient::new(transport); let api = ApiClient::new(transport);
let _ = api.send_message(&session_id, &text, &[]); let _ = api.send_message(&session_id, &text, &[]);
+15 -15
View File
@@ -1,8 +1,9 @@
use std::collections::VecDeque; use std::collections::{HashSet, VecDeque};
use std::fs; use std::fs;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
pub const CACHE_BUDGET_BYTES: u64 = 256_000_000; pub const CACHE_BUDGET_BYTES: u64 = 256_000_000;
@@ -14,7 +15,7 @@ pub struct CachedTail {
pub struct TranscriptCache { pub struct TranscriptCache {
root: PathBuf, root: PathBuf,
warn: std::sync::Arc<dyn Fn(&str) + Send + Sync>, warn: Arc<dyn Fn(&str) + Send + Sync>,
} }
impl TranscriptCache { impl TranscriptCache {
@@ -28,7 +29,7 @@ impl TranscriptCache {
) -> Self { ) -> Self {
Self { Self {
root: root.into(), root: root.into(),
warn: std::sync::Arc::new(warn), warn: Arc::new(warn),
} }
} }
@@ -40,7 +41,7 @@ impl TranscriptCache {
/// successful list fetch. The path out for a session deleted on /// successful list fetch. The path out for a session deleted on
/// another device: nothing here would otherwise hear about it, and /// another device: nothing here would otherwise hear about it, and
/// unlike a draft's few bytes what it leaves behind is megabytes. /// unlike a draft's few bytes what it leaves behind is megabytes.
pub fn retain_only(&self, ids: &std::collections::HashSet<String>) { pub fn retain_only(&self, ids: &HashSet<String>) {
guard_io((), self.warn.as_ref(), || { guard_io((), self.warn.as_ref(), || {
for dir in session_dirs(&self.root)? { for dir in session_dirs(&self.root)? {
if let Some(name) = dir.file_name().and_then(|n| n.to_str()) if let Some(name) = dir.file_name().and_then(|n| n.to_str())
@@ -61,12 +62,12 @@ impl TranscriptCache {
/// for. /// for.
pub fn evict_to_budget(&self, keep: &str, budget: u64) { pub fn evict_to_budget(&self, keep: &str, budget: u64) {
guard_io((), self.warn.as_ref(), || { guard_io((), self.warn.as_ref(), || {
let mut dirs: Vec<(PathBuf, std::time::SystemTime)> = session_dirs(&self.root)? let mut dirs: Vec<(PathBuf, SystemTime)> = session_dirs(&self.root)?
.into_iter() .into_iter()
.map(|d| { .map(|d| {
let modified = fs::metadata(&d) let modified = fs::metadata(&d)
.and_then(|m| m.modified()) .and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH); .unwrap_or(UNIX_EPOCH);
(d, modified) (d, modified)
}) })
.collect(); .collect();
@@ -132,7 +133,7 @@ fn dir_size(path: &Path) -> u64 {
/// closed by paging back through it, but nothing is served across one. /// closed by paging back through it, but nothing is served across one.
pub struct SessionCache { pub struct SessionCache {
dir: PathBuf, dir: PathBuf,
warn: std::sync::Arc<dyn Fn(&str) + Send + Sync>, warn: Arc<dyn Fn(&str) + Send + Sync>,
state: Mutex<WriterState>, state: Mutex<WriterState>,
} }
@@ -145,7 +146,7 @@ struct WriterState {
} }
impl SessionCache { impl SessionCache {
fn new(dir: PathBuf, warn: std::sync::Arc<dyn Fn(&str) + Send + Sync>) -> Self { fn new(dir: PathBuf, warn: Arc<dyn Fn(&str) + Send + Sync>) -> Self {
Self { Self {
dir, dir,
warn, warn,
@@ -333,7 +334,7 @@ impl SessionCache {
pub fn touch(&self) { pub fn touch(&self) {
self.guard((), |this, _state| { self.guard((), |this, _state| {
if this.dir.is_dir() { if this.dir.is_dir() {
let now = std::time::SystemTime::now(); let now = SystemTime::now();
filetime_set_modified(&this.dir, now)?; filetime_set_modified(&this.dir, now)?;
} }
Ok(()) Ok(())
@@ -708,7 +709,7 @@ fn guard_io<T>(
} }
} }
fn filetime_set_modified(path: &Path, _when: std::time::SystemTime) -> io::Result<()> { fn filetime_set_modified(path: &Path, _when: SystemTime) -> io::Result<()> {
use std::io::Write; use std::io::Write;
// Rewriting a marker file's contents (rather than the directory itself, // Rewriting a marker file's contents (rather than the directory itself,
// which `std` has no portable "touch" for) bumps the directory's own // which `std` has no portable "touch" for) bumps the directory's own
@@ -728,18 +729,17 @@ fn filetime_set_modified(path: &Path, _when: std::time::SystemTime) -> io::Resul
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::collections::HashSet;
fn cache(temp: &Path) -> TranscriptCache { fn cache(temp: &Path) -> TranscriptCache {
let said: std::sync::Arc<Mutex<Vec<String>>> = Default::default(); let said: Arc<Mutex<Vec<String>>> = Default::default();
let said2 = said.clone(); let said2 = said.clone();
TranscriptCache::with_warn(temp.join("v1/host_8443"), move |msg| { TranscriptCache::with_warn(temp.join("v1/host_8443"), move |msg| {
said2.lock().unwrap().push(msg.to_string()); said2.lock().unwrap().push(msg.to_string());
}) })
} }
fn cache_with_log(temp: &Path) -> (TranscriptCache, std::sync::Arc<Mutex<Vec<String>>>) { fn cache_with_log(temp: &Path) -> (TranscriptCache, Arc<Mutex<Vec<String>>>) {
let said: std::sync::Arc<Mutex<Vec<String>>> = Default::default(); let said: Arc<Mutex<Vec<String>>> = Default::default();
let said2 = said.clone(); let said2 = said.clone();
( (
TranscriptCache::with_warn(temp.join("v1/host_8443"), move |msg| { TranscriptCache::with_warn(temp.join("v1/host_8443"), move |msg| {
+3 -2
View File
@@ -1,4 +1,5 @@
use event_model::{Event, QuestionOption, SeqEvent, SessionStatus}; use event_model::{Event, QuestionOption, SeqEvent, SessionStatus};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct QuestionCard { pub struct QuestionCard {
@@ -260,11 +261,11 @@ fn split_run(tail: &[TranscriptItem], behind: Option<&str>) -> Vec<TranscriptIte
/// round that loses nothing. /// round that loses nothing.
pub fn join_pages(earlier: &[TranscriptItem], later: &[TranscriptItem]) -> Vec<TranscriptItem> { pub fn join_pages(earlier: &[TranscriptItem], later: &[TranscriptItem]) -> Vec<TranscriptItem> {
let (older, newer) = heal_split_message(earlier, later); let (older, newer) = heal_split_message(earlier, later);
let started_earlier: std::collections::HashSet<&str> = older let started_earlier: HashSet<&str> = older
.iter() .iter()
.filter_map(TranscriptItem::as_tool_run) .filter_map(TranscriptItem::as_tool_run)
.collect(); .collect();
let ended_later: std::collections::HashMap<String, TranscriptItem> = newer let ended_later: HashMap<String, TranscriptItem> = newer
.iter() .iter()
.filter_map(|item| item.as_tool_run().map(|id| (id.to_string(), item.clone()))) .filter_map(|item| item.as_tool_run().map(|id| (id.to_string(), item.clone())))
.filter(|(id, _)| started_earlier.contains(id.as_str())) .filter(|(id, _)| started_earlier.contains(id.as_str()))
+28 -22
View File
@@ -5,8 +5,14 @@ use crate::client::transcript_fold::{
}; };
use event_model::SeqEvent; use event_model::SeqEvent;
use iris::prelude::*; use iris::prelude::*;
use std::sync::Arc; use std::{
use std::sync::atomic::{AtomicU64, Ordering}; process,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
thread,
};
const LIST_WIDTH: f32 = 260.0; const LIST_WIDTH: f32 = 260.0;
@@ -31,12 +37,12 @@ enum AppEvent {
} }
pub fn run() { pub fn run() {
DefaultApp::<Client>::run(); DesktopApp::<Client>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct Client { struct Client {
ui_state: DefaultUiState, ui_state: DesktopUiState,
api: Arc<ApiClient<UreqTransport>>, api: Arc<ApiClient<UreqTransport>>,
stream_transport: Arc<UreqTransport>, stream_transport: Arc<UreqTransport>,
proxy: Proxy<AppEvent>, proxy: Proxy<AppEvent>,
@@ -49,17 +55,17 @@ struct Client {
generation: Arc<AtomicU64>, generation: Arc<AtomicU64>,
} }
impl DefaultAppState for Client { impl DesktopAppState for Client {
type Event = AppEvent; type Event = AppEvent;
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
proxy: Proxy<AppEvent>, proxy: Proxy<AppEvent>,
) -> Self { ) -> Self {
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| { let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
eprintln!("desktop-app: {e}"); eprintln!("desktop-app: {e}");
std::process::exit(2); process::exit(2);
}); });
let build_transport = let build_transport =
|| UreqTransport::new(server.base_url(), server.token.clone(), &ca_pem); || UreqTransport::new(server.base_url(), server.token.clone(), &ca_pem);
@@ -70,7 +76,7 @@ impl DefaultAppState for Client {
"desktop-app: couldn't set up TLS to {}: {e}", "desktop-app: couldn't set up TLS to {}: {e}",
server.base_url() server.base_url()
); );
std::process::exit(1); process::exit(1);
}); });
let api = Arc::new(ApiClient::new(rest_transport)); let api = Arc::new(ApiClient::new(rest_transport));
let stream_transport = Arc::new(stream_transport); let stream_transport = Arc::new(stream_transport);
@@ -101,7 +107,7 @@ impl DefaultAppState for Client {
client client
} }
fn event(&mut self, event: AppEvent, rsc: &mut DefaultRsc<Self>, _render: &mut UiRenderState) { fn event(&mut self, event: AppEvent, rsc: &mut DesktopRsc<Self>) {
match event { match event {
AppEvent::Sessions(Ok(sessions)) => { AppEvent::Sessions(Ok(sessions)) => {
self.sessions = sessions; self.sessions = sessions;
@@ -141,7 +147,7 @@ impl DefaultAppState for Client {
if self.current(&session_id, generation) { if self.current(&session_id, generation) {
let old_items = self.items.clone(); let old_items = self.items.clone();
self.items = fold_event(&self.items, &event); self.items = fold_event(&self.items, &event);
match &self.screen { match self.screen.as_mut() {
Some(screen) => screen.apply(rsc, &old_items, &self.items), Some(screen) => screen.apply(rsc, &old_items, &self.items),
None => self.rebuild_transcript(rsc), None => self.rebuild_transcript(rsc),
} }
@@ -171,7 +177,7 @@ impl Client {
&& self.generation.load(Ordering::SeqCst) == generation && self.generation.load(Ordering::SeqCst) == generation
} }
fn show_message(&mut self, rsc: &mut DefaultRsc<Self>, message: &str) { fn show_message(&mut self, rsc: &mut DesktopRsc<Self>, message: &str) {
let widget = placeholder(rsc, message); let widget = placeholder(rsc, message);
(self.transcript_ptr)(rsc).set(widget); (self.transcript_ptr)(rsc).set(widget);
} }
@@ -179,13 +185,13 @@ impl Client {
fn spawn_fetch_sessions(&self) { fn spawn_fetch_sessions(&self) {
let api = self.api.clone(); let api = self.api.clone();
let proxy = self.proxy.clone(); let proxy = self.proxy.clone();
std::thread::spawn(move || { thread::spawn(move || {
let result = api.fetch_sessions().map_err(|e| e.to_string()); let result = api.fetch_sessions().map_err(|e| e.to_string());
let _ = proxy.send_event(AppEvent::Sessions(result)); let _ = proxy.send_event(AppEvent::Sessions(result));
}); });
} }
fn rebuild_list(&mut self, rsc: &mut DefaultRsc<Self>) { fn rebuild_list(&mut self, rsc: &mut DesktopRsc<Self>) {
let list = Span::empty(Dir::DOWN).gap(2).add(rsc); let list = Span::empty(Dir::DOWN).gap(2).add(rsc);
for session in &self.sessions { for session in &self.sessions {
let selected = self.selected.as_deref() == Some(session.id.as_str()); let selected = self.selected.as_deref() == Some(session.id.as_str());
@@ -199,7 +205,7 @@ impl Client {
(self.list_ptr)(rsc).set(tree); (self.list_ptr)(rsc).set(tree);
} }
fn select_session(&mut self, rsc: &mut DefaultRsc<Self>, session_id: String) { fn select_session(&mut self, rsc: &mut DesktopRsc<Self>, session_id: String) {
let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
self.selected = Some(session_id.clone()); self.selected = Some(session_id.clone());
self.items.clear(); self.items.clear();
@@ -211,7 +217,7 @@ impl Client {
let stream_transport = self.stream_transport.clone(); let stream_transport = self.stream_transport.clone();
let proxy = self.proxy.clone(); let proxy = self.proxy.clone();
let live_generation = self.generation.clone(); let live_generation = self.generation.clone();
std::thread::spawn(move || { thread::spawn(move || {
let page: Result<Vec<serde_json::Value>, String> = api let page: Result<Vec<serde_json::Value>, String> = api
.fetch_transcript_page(&session_id, None, 200, true) .fetch_transcript_page(&session_id, None, 200, true)
.map_err(|e| e.to_string()); .map_err(|e| e.to_string());
@@ -257,14 +263,14 @@ impl Client {
fn send_message(&mut self, session_id: String, text: String) { fn send_message(&mut self, session_id: String, text: String) {
let api = self.api.clone(); let api = self.api.clone();
let proxy = self.proxy.clone(); let proxy = self.proxy.clone();
std::thread::spawn(move || { thread::spawn(move || {
if let Err(e) = api.send_message(&session_id, &text, &[]) { if let Err(e) = api.send_message(&session_id, &text, &[]) {
let _ = proxy.send_event(AppEvent::SendFailed(e.to_string())); let _ = proxy.send_event(AppEvent::SendFailed(e.to_string()));
} }
}); });
} }
fn rebuild_transcript(&mut self, rsc: &mut DefaultRsc<Self>) { fn rebuild_transcript(&mut self, rsc: &mut DesktopRsc<Self>) {
let in_progress = self let in_progress = self
.screen .screen
.as_ref() .as_ref()
@@ -294,7 +300,7 @@ impl Client {
} }
fn session_row( fn session_row(
rsc: &mut DefaultRsc<Client>, rsc: &mut DesktopRsc<Client>,
session: &SessionSummary, session: &SessionSummary,
selected: bool, selected: bool,
) -> StrongWidget { ) -> StrongWidget {
@@ -313,7 +319,7 @@ fn session_row(
.background(rect(bg)) .background(rect(bg))
.on( .on(
CursorSense::click(), CursorSense::click(),
move |ctx, rsc: &mut DefaultRsc<Client>| { move |ctx, rsc: &mut DesktopRsc<Client>| {
ctx.state.select_session(rsc, id.clone()); ctx.state.select_session(rsc, id.clone());
}, },
) )
@@ -321,7 +327,7 @@ fn session_row(
.any() .any()
} }
fn placeholder(rsc: &mut DefaultRsc<Client>, message: &str) -> StrongWidget { fn placeholder(rsc: &mut DesktopRsc<Client>, message: &str) -> StrongWidget {
wtext(message.to_string()) wtext(message.to_string())
.color(PaintId::WHITE) .color(PaintId::WHITE)
.wrap(true) .wrap(true)
+5 -8
View File
@@ -4,25 +4,22 @@
//! callers are in the library; the binary is only `fn main`. //! callers are in the library; the binary is only `fn main`.
use crate::client::config::EnrolledServer; use crate::client::config::EnrolledServer;
use std::{env, fs, path::PathBuf};
use super::config; use super::config;
struct Args { struct Args {
ca_path: Option<std::path::PathBuf>, ca_path: Option<PathBuf>,
link: Option<String>, link: Option<String>,
} }
fn parse_args() -> Result<Args, String> { fn parse_args() -> Result<Args, String> {
let mut ca_path = None; let mut ca_path = None;
let mut link = None; let mut link = None;
let mut args = std::env::args().skip(1); let mut args = env::args().skip(1);
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
match arg.as_str() { match arg.as_str() {
"--ca" => { "--ca" => ca_path = Some(PathBuf::from(args.next().ok_or("--ca needs a path")?)),
ca_path = Some(std::path::PathBuf::from(
args.next().ok_or("--ca needs a path")?,
))
}
"--link" => link = Some(args.next().ok_or("--link needs a value")?), "--link" => link = Some(args.next().ok_or("--link needs a value")?),
other => return Err(format!("unrecognised argument '{other}'")), other => return Err(format!("unrecognised argument '{other}'")),
} }
@@ -56,7 +53,7 @@ pub fn load_startup_config() -> Result<(EnrolledServer, Vec<u8>), String> {
// server at a certificate it did not carry -- and so the flag still // server at a certificate it did not carry -- and so the flag still
// means what it did before the link could carry one. // means what it did before the link could carry one.
let ca_pem = match (&args.ca_path, &server.ca_pem) { let ca_pem = match (&args.ca_path, &server.ca_pem) {
(Some(path), _) => std::fs::read(path) (Some(path), _) => fs::read(path)
.map_err(|e| format!("couldn't read the CA at {}: {e}", path.display()))?, .map_err(|e| format!("couldn't read the CA at {}: {e}", path.display()))?,
(None, Some(pem)) => pem.clone().into_bytes(), (None, Some(pem)) => pem.clone().into_bytes(),
(None, None) => { (None, None) => {
+76 -92
View File
@@ -1,8 +1,5 @@
pub mod composer; pub mod composer;
// The checked-in bench fixture opened as a real screen -- 1.9 MB of // Keep the 1.9 MB fixture out of ordinary APKs.
// `include_str!`, so it is a feature rather than always present: a build
// meant for a phone must not carry it. `bench` turns it on; so does the
// default, which is what makes `cargo test` here run the harness tests.
#[cfg(feature = "fixture")] #[cfg(feature = "fixture")]
pub mod fixture; pub mod fixture;
pub mod markdown; pub mod markdown;
@@ -11,9 +8,9 @@ pub(crate) mod tap;
pub mod theme; pub mod theme;
pub mod tool; pub mod tool;
use crate::client::transcript_fold::TranscriptRow as FoldedRow; use crate::client::transcript_fold::{TranscriptItem, TranscriptRow as FoldedRow, group_tool_runs};
use iris::prelude::*; use iris::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{mem, rc::Rc};
use theme::Theme; use theme::Theme;
pub struct TranscriptScreen { pub struct TranscriptScreen {
@@ -25,9 +22,9 @@ pub struct TranscriptScreen {
/// `.jump_to_end()` directly. /// `.jump_to_end()` directly.
pub list: WeakWidget<LazySpan>, pub list: WeakWidget<LazySpan>,
pub composer: composer::Composer, pub composer: composer::Composer,
rebuilds: std::cell::Cell<usize>, rebuilds: usize,
tail: RefCell<Option<(RowKey, row::TailRow)>>, tail: Option<(RowKey, row::TailRow)>,
session_working: std::cell::Cell<bool>, session_working: bool,
theme: Rc<Theme>, theme: Rc<Theme>,
} }
@@ -36,7 +33,7 @@ impl TranscriptScreen {
/// what a caller's SSE loop or a sent message calls as new events /// what a caller's SSE loop or a sent message calls as new events
/// arrive. `LazySpan::push_back` is O(1) and keeps the view pinned to the /// arrive. `LazySpan::push_back` is O(1) and keeps the view pinned to the
/// newest content when it already was (I3). /// newest content when it already was (I3).
pub fn push_row<Rsc: HasEvents>(&self, rsc: &mut Rsc, row: &FoldedRow) pub fn push_row<Rsc: HasEvents>(&mut self, rsc: &mut Rsc, row: &FoldedRow)
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
@@ -45,27 +42,27 @@ impl TranscriptScreen {
// grows through `RowBlocks::apply_delta`, which appends to what is // grows through `RowBlocks::apply_delta`, which appends to what is
// already drawn -- so the cap only ever catches a row that arrived // already drawn -- so the cap only ever catches a row that arrived
// long, which is the one nobody is watching arrive. // long, which is the one nobody is watching arrive.
let (key, widget, tail) = row::build_row( let row::BuiltRow { key, widget, tail } = row::build_row(
rsc, rsc,
self.list, self.list,
row, row,
self.session_working.get(), self.session_working,
true, true,
self.theme.clone(), self.theme.clone(),
); );
(self.list)(rsc).push_back(LazyItem::new(key, widget)); (self.list)(rsc).push_back(LazyItem::new(key, widget));
*self.tail.borrow_mut() = tail.map(|t| (key, t)); self.tail = tail.map(|t| (key, t));
} }
pub fn set_session_working<Rsc: HasEvents>(&self, rsc: &mut Rsc, working: bool) pub fn set_session_working<Rsc: HasEvents>(&mut self, rsc: &mut Rsc, working: bool)
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
if self.session_working.replace(working) == working { if self.session_working == working {
return; return;
} }
let mut tail = self.tail.borrow_mut(); self.session_working = working;
if let Some((_, row::TailRow::Tools(tools))) = tail.as_mut() { if let Some((_, row::TailRow::Tools(tools))) = self.tail.as_mut() {
let calls = tools.calls(); let calls = tools.calls();
tools.apply_calls(rsc, &calls, working); tools.apply_calls(rsc, &calls, working);
} }
@@ -73,7 +70,7 @@ impl TranscriptScreen {
#[cfg(test)] #[cfg(test)]
fn tail_card_count(&self) -> usize { fn tail_card_count(&self) -> usize {
match self.tail.borrow().as_ref() { match self.tail.as_ref() {
Some((_, row::TailRow::Tools(tools))) => tools.card_count(), Some((_, row::TailRow::Tools(tools))) => tools.card_count(),
_ => 0, _ => 0,
} }
@@ -84,12 +81,11 @@ impl TranscriptScreen {
/// displayless machine, and the tests below). Answers whether there /// displayless machine, and the tests below). Answers whether there
/// was such a row to act on, so a caller that expected one can say so /// was such a row to act on, so a caller that expected one can say so
/// rather than silently producing the collapsed picture. /// rather than silently producing the collapsed picture.
pub fn expand_tail_tools<Rsc: HasEvents>(&self, rsc: &mut Rsc, expanded: bool) -> bool pub fn expand_tail_tools<Rsc: HasEvents>(&mut self, rsc: &mut Rsc, expanded: bool) -> bool
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
let tail = self.tail.borrow(); let Some((_, row::TailRow::Tools(tools))) = self.tail.as_ref() else {
let Some((_, row::TailRow::Tools(tools))) = tail.as_ref() else {
return false; return false;
}; };
tools.set_group_expanded(rsc, expanded); tools.set_group_expanded(rsc, expanded);
@@ -99,12 +95,16 @@ impl TranscriptScreen {
/// The `ReplaceLast` fast path: update the tail row in place if this /// The `ReplaceLast` fast path: update the tail row in place if this
/// really is a change to the same row, and say whether that worked. /// really is a change to the same row, and say whether that worked.
/// `false` for anything the caller must rebuild instead. /// `false` for anything the caller must rebuild instead.
fn apply_tail_delta<Rsc: HasEvents>(&self, rsc: &mut Rsc, key: RowKey, row: &FoldedRow) -> bool fn apply_tail_delta<Rsc: HasEvents>(
&mut self,
rsc: &mut Rsc,
key: RowKey,
row: &FoldedRow,
) -> bool
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
let mut tail = self.tail.borrow_mut(); let Some((tail_key, kept)) = self.tail.as_mut() else {
let Some((tail_key, kept)) = tail.as_mut() else {
return false; return false;
}; };
if *tail_key != key { if *tail_key != key {
@@ -116,19 +116,16 @@ impl TranscriptScreen {
// A tool call is drawn as a card, never as markdown, so a // A tool call is drawn as a card, never as markdown, so a
// row that kept blocks and now holds one is a different // row that kept blocks and now holds one is a different
// row -- rebuild it. // row -- rebuild it.
if matches!( if matches!(item, TranscriptItem::ToolRun { .. }) {
item,
crate::client::transcript_fold::TranscriptItem::ToolRun { .. }
) {
return false; return false;
} }
blocks.apply_delta(rsc, sender, &markdown_src) blocks.apply_delta(rsc, sender, &markdown_src)
} }
(row::TailRow::Tools(tools), FoldedRow::Tools(calls)) => { (row::TailRow::Tools(tools), FoldedRow::Tools(calls)) => {
tools.apply_calls(rsc, calls, self.session_working.get()) tools.apply_calls(rsc, calls, self.session_working)
} }
(row::TailRow::Tools(tools), FoldedRow::Single(item)) => { (row::TailRow::Tools(tools), FoldedRow::Single(item)) => {
tools.apply_calls(rsc, std::slice::from_ref(item), self.session_working.get()) tools.apply_calls(rsc, std::slice::from_ref(item), self.session_working)
} }
(row::TailRow::Blocks(_), FoldedRow::Tools(_)) => false, (row::TailRow::Blocks(_), FoldedRow::Tools(_)) => false,
} }
@@ -143,15 +140,13 @@ impl TranscriptScreen {
/// often the fallback actually fires rather than assuming it never /// often the fallback actually fires rather than assuming it never
/// does. /// does.
pub fn apply<Rsc: HasEvents>( pub fn apply<Rsc: HasEvents>(
&self, &mut self,
rsc: &mut Rsc, rsc: &mut Rsc,
old: &[crate::client::transcript_fold::TranscriptItem], old: &[TranscriptItem],
new: &[crate::client::transcript_fold::TranscriptItem], new: &[TranscriptItem],
) where ) where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
use crate::client::transcript_fold::group_tool_runs;
let old_rows = group_tool_runs(old); let old_rows = group_tool_runs(old);
let new_rows = group_tool_runs(new); let new_rows = group_tool_runs(new);
@@ -172,25 +167,29 @@ impl TranscriptScreen {
return; return;
} }
let (new_key, widget, kept) = row::build_row( let row::BuiltRow {
key: new_key,
widget,
tail: kept,
} = row::build_row(
rsc, rsc,
self.list, self.list,
&new_rows[common], &new_rows[common],
self.session_working.get(), self.session_working,
false, false,
self.theme.clone(), self.theme.clone(),
); );
let evicted = (self.list)(rsc).replace_back(LazyItem::new(new_key, widget)); let evicted = (self.list)(rsc).replace_back(LazyItem::new(new_key, widget));
drop(evicted); // frees the old row's widget, same as a pop would drop(evicted); // frees the old row's widget, same as a pop would
*self.tail.borrow_mut() = kept.map(|t| (new_key, t)); self.tail = kept.map(|t| (new_key, t));
for row in &new_rows[common + 1..] { for row in &new_rows[common + 1..] {
self.push_row(rsc, row); self.push_row(rsc, row);
} }
} }
RowDiff::Rebuild => { RowDiff::Rebuild => {
self.rebuilds.set(self.rebuilds.get() + 1); self.rebuilds += 1;
(self.list)(rsc).clear(); (self.list)(rsc).clear();
*self.tail.borrow_mut() = None; self.tail = None;
for row in &new_rows { for row in &new_rows {
self.push_row(rsc, row); self.push_row(rsc, row);
} }
@@ -198,8 +197,8 @@ impl TranscriptScreen {
} }
} }
pub fn take_rebuilds(&self) -> usize { pub fn take_rebuilds(&mut self) -> usize {
self.rebuilds.replace(0) mem::take(&mut self.rebuilds)
} }
/// The semantic paint IDs used by this screen. A caller can replace /// The semantic paint IDs used by this screen. A caller can replace
@@ -268,7 +267,11 @@ where
// be a reply already streaming when this screen opened, and a // be a reply already streaming when this screen opened, and a
// capped row cannot take a delta (`RowBlocks::capped`). // capped row cannot take a delta (`RowBlocks::capped`).
let cap = i + 1 < rows.len(); let cap = i + 1 < rows.len();
let (key, widget, kept) = row::build_row(rsc, list, row, false, cap, theme.clone()); let row::BuiltRow {
key,
widget,
tail: kept,
} = row::build_row(rsc, list, row, false, cap, theme.clone());
list(rsc).push_back(LazyItem::new(key, widget)); list(rsc).push_back(LazyItem::new(key, widget));
tail = kept.map(|t| (key, t)); tail = kept.map(|t| (key, t));
} }
@@ -303,11 +306,11 @@ where
( (
TranscriptScreen { TranscriptScreen {
tail: RefCell::new(tail), tail,
session_working: std::cell::Cell::new(false), session_working: false,
list, list,
composer, composer,
rebuilds: std::cell::Cell::new(0), rebuilds: 0,
theme, theme,
}, },
tree, tree,
@@ -348,7 +351,6 @@ fn diff_rows(old: &[FoldedRow], new: &[FoldedRow]) -> RowDiff {
#[cfg(test)] #[cfg(test)]
mod diff_tests { mod diff_tests {
use super::*; use super::*;
use crate::client::transcript_fold::TranscriptItem;
fn user(seq: u64, text: &str) -> FoldedRow { fn user(seq: u64, text: &str) -> FoldedRow {
FoldedRow::Single(TranscriptItem::UserMsg { FoldedRow::Single(TranscriptItem::UserMsg {
@@ -433,7 +435,8 @@ mod diff_tests {
#[cfg(test)] #[cfg(test)]
mod apply_tests { mod apply_tests {
use super::*; use super::*;
use crate::client::transcript_fold::TranscriptItem; use crate::client::text_cap::MESSAGE_LINES;
use std::iter;
struct TestFocus { struct TestFocus {
focus: Option<WeakWidget<TextEdit>>, focus: Option<WeakWidget<TextEdit>>,
@@ -456,14 +459,14 @@ mod apply_tests {
} }
struct TestRsc { struct TestRsc {
ui: UiData, ui: Ui,
events: EventManager<TestRsc>, events: EventManager<TestRsc>,
} }
impl UiRsc for TestRsc { impl UiRsc for TestRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui &mut self.ui
} }
fn on_draw(&mut self, active: &ActiveData) { fn on_draw(&mut self, active: &ActiveData) {
@@ -515,7 +518,7 @@ mod apply_tests {
fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) { fn cost_of_one_delta(paragraphs: usize) -> (u64, u64) {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let old_items = vec![assistant(1, &reply(paragraphs, "and the last one is st"))]; let old_items = vec![assistant(1, &reply(paragraphs, "and the last one is st"))];
@@ -523,10 +526,7 @@ mod apply_tests {
1, 1,
&reply(paragraphs, "and the last one is still going."), &reply(paragraphs, "and the last one is still going."),
)]; )];
let (screen, tree) = build_tree( let (mut screen, tree) = build_tree(&mut rsc, group_tool_runs(&old_items));
&mut rsc,
crate::client::transcript_fold::group_tool_runs(&old_items),
);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0)); render.resize((1080.0, 20000.0));
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
@@ -535,7 +535,7 @@ mod apply_tests {
screen.apply(&mut rsc, &old_items, &new_items); screen.apply(&mut rsc, &old_items, &new_items);
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
assert_eq!(screen.take_rebuilds(), 0, "the delta path must be taken"); assert_eq!(screen.take_rebuilds(), 0, "the delta path must be taken");
let (draws, _, _, shapes) = render.take_counters(); let RenderCounters { draws, shapes, .. } = render.take_counters();
(draws, shapes) (draws, shapes)
} }
@@ -585,8 +585,7 @@ mod apply_tests {
rsc: &mut TestRsc, rsc: &mut TestRsc,
items: &[TranscriptItem], items: &[TranscriptItem],
) -> (TranscriptScreen, StrongWidget, UiRenderState) { ) -> (TranscriptScreen, StrongWidget, UiRenderState) {
let (screen, tree) = let (mut screen, tree) = build_tree(rsc, group_tool_runs(items));
build_tree(rsc, crate::client::transcript_fold::group_tool_runs(items));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0)); render.resize((1080.0, 20000.0));
render.update(&tree, rsc); render.update(&tree, rsc);
@@ -601,14 +600,11 @@ mod apply_tests {
fn shapes_to_open(output: &str) -> u64 { fn shapes_to_open(output: &str) -> u64 {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let items = run_of(3, output, true); let items = run_of(3, output, true);
let (screen, tree) = build_tree( let (mut screen, tree) = build_tree(&mut rsc, group_tool_runs(&items));
&mut rsc,
crate::client::transcript_fold::group_tool_runs(&items),
);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0)); render.resize((1080.0, 20000.0));
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
@@ -619,13 +615,12 @@ mod apply_tests {
"the fixture's only row must be the tool run" "the fixture's only row must be the tool run"
); );
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
let (_, _, _, shapes) = render.take_counters(); render.take_counters().shapes
shapes
} }
#[test] #[test]
fn collapsed_cards_shape_only_their_summary_lines() { fn collapsed_cards_shape_only_their_summary_lines() {
let long: String = std::iter::repeat_n("a line of tool output\n", 4_000).collect(); let long: String = iter::repeat_n("a line of tool output\n", 4_000).collect();
assert!(long.len() > 80_000, "the long case must actually be long"); assert!(long.len() > 80_000, "the long case must actually be long");
let short_shapes = shapes_to_open("ok\n"); let short_shapes = shapes_to_open("ok\n");
@@ -644,7 +639,7 @@ mod apply_tests {
fn shapes_for_message(text: &str) -> u64 { fn shapes_for_message(text: &str) -> u64 {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let items = vec![ let items = vec![
@@ -659,22 +654,18 @@ mod apply_tests {
settled: true, settled: true,
}, },
]; ];
let (_screen, tree) = build_tree( let (_screen, tree) = build_tree(&mut rsc, group_tool_runs(&items));
&mut rsc,
crate::client::transcript_fold::group_tool_runs(&items),
);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0)); render.resize((1080.0, 20000.0));
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
let (_, _, _, shapes) = render.take_counters(); render.take_counters().shapes
shapes
} }
#[test] #[test]
fn a_long_message_is_drawn_only_as_far_as_the_cap() { fn a_long_message_is_drawn_only_as_far_as_the_cap() {
let paragraphs = |n: usize| "a paragraph of a reply\n\n".repeat(n); let paragraphs = |n: usize| "a paragraph of a reply\n\n".repeat(n);
let capped = shapes_for_message(&paragraphs(crate::client::text_cap::MESSAGE_LINES * 4)); let capped = shapes_for_message(&paragraphs(MESSAGE_LINES * 4));
let bigger = shapes_for_message(&paragraphs(crate::client::text_cap::MESSAGE_LINES * 40)); let bigger = shapes_for_message(&paragraphs(MESSAGE_LINES * 40));
assert!( assert!(
capped > 0, capped > 0,
"the screen shaped nothing, so this compares zeroes" "the screen shaped nothing, so this compares zeroes"
@@ -688,14 +679,14 @@ mod apply_tests {
fn cost_of_one_result(count: usize) -> u64 { fn cost_of_one_result(count: usize) -> u64 {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let before = run_of(count, "", false); let before = run_of(count, "", false);
let mut after = before.clone(); let mut after = before.clone();
after[0] = call("t0", "the result", true); after[0] = call("t0", "the result", true);
let (screen, tree, mut render) = open_run(&mut rsc, &before); let (mut screen, tree, mut render) = open_run(&mut rsc, &before);
screen.apply(&mut rsc, &before, &after); screen.apply(&mut rsc, &before, &after);
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
assert_eq!( assert_eq!(
@@ -703,8 +694,7 @@ mod apply_tests {
0, 0,
"a result arriving must not rebuild the whole screen" "a result arriving must not rebuild the whole screen"
); );
let (draws, _, _, _) = render.take_counters(); render.take_counters().draws
draws
} }
#[test] #[test]
@@ -725,17 +715,14 @@ mod apply_tests {
#[test] #[test]
fn a_group_opens_and_closes_and_keeps_its_state_across_a_result() { fn a_group_opens_and_closes_and_keeps_its_state_across_a_result() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let before = run_of(3, "", false); let before = run_of(3, "", false);
let mut after = before.clone(); let mut after = before.clone();
after[1] = call("t1", "done", true); after[1] = call("t1", "done", true);
let (screen, tree) = build_tree( let (mut screen, tree) = build_tree(&mut rsc, group_tool_runs(&before));
&mut rsc,
crate::client::transcript_fold::group_tool_runs(&before),
);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 20000.0)); render.resize((1080.0, 20000.0));
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
@@ -760,14 +747,14 @@ mod apply_tests {
#[test] #[test]
fn a_call_joining_an_open_run_appends_one_card() { fn a_call_joining_an_open_run_appends_one_card() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let before = run_of(2, "ok", true); let before = run_of(2, "ok", true);
let mut after = before.clone(); let mut after = before.clone();
after.push(call("t2", "", false)); after.push(call("t2", "", false));
let (screen, tree, mut render) = open_run(&mut rsc, &before); let (mut screen, tree, mut render) = open_run(&mut rsc, &before);
assert_eq!(screen.tail_card_count(), 2); assert_eq!(screen.tail_card_count(), 2);
screen.apply(&mut rsc, &before, &after); screen.apply(&mut rsc, &before, &after);
render.update(&tree, &mut rsc); render.update(&tree, &mut rsc);
@@ -782,15 +769,12 @@ mod apply_tests {
#[test] #[test]
fn a_tail_that_stops_being_tool_calls_falls_back_to_a_rebuild() { fn a_tail_that_stops_being_tool_calls_falls_back_to_a_rebuild() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let before = vec![user(1, "stable"), call("t0", "", false)]; let before = vec![user(1, "stable"), call("t0", "", false)];
let after = vec![user(1, "stable"), user(2, "not a tool call at all")]; let after = vec![user(1, "stable"), user(2, "not a tool call at all")];
let (screen, _tree) = build_tree( let (mut screen, _tree) = build_tree(&mut rsc, group_tool_runs(&before));
&mut rsc,
crate::client::transcript_fold::group_tool_runs(&before),
);
screen.apply(&mut rsc, &before, &after); screen.apply(&mut rsc, &before, &after);
assert_eq!( assert_eq!(
screen.take_rebuilds(), screen.take_rebuilds(),
+89 -113
View File
@@ -1,32 +1,31 @@
use crate::client::markdown_blocks::{Block, BlockKind, common_prefix, split_blocks}; use crate::client::markdown_blocks::{Block, BlockKind, common_prefix, split_blocks};
use crate::client::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label}; use crate::client::text_cap::{MESSAGE_BYTES, MESSAGE_LINES, cut, show_all_label};
use crate::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow}; use crate::client::transcript_fold::{
ItemKey, QuestionCard, TranscriptItem, TranscriptRow as FoldedRow,
};
use crate::ui::markdown::{BlockFrame, Link, frame_of, render_block}; use crate::ui::markdown::{BlockFrame, Link, frame_of, render_block};
use crate::ui::tap::{hold_edge, on_tap}; use crate::ui::tap::{hold_edge, on_tap};
use crate::ui::theme::Theme; use crate::ui::theme::Theme;
use crate::ui::tool::ToolRow; use crate::ui::tool::{ToolRow, build_tool_row};
use iris::prelude::*; use iris::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{
cell::RefCell,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
rc::Rc,
slice,
};
const BLOCK_GAP_DP: f32 = 8.0; const BLOCK_GAP_DP: f32 = 8.0;
pub const BASE_SIZE: f32 = 16.0; pub const BASE_SIZE: f32 = 16.0;
/// `ItemKey::Seq` already is the `RowKey` (`u64`) this crate's `LazySpan` wants. /// Maps string run IDs above the sequence-number range used by transcripts.
/// `ItemKey::RunId` is a string (a tool call's own id), so it is hashed into pub fn row_key(key: &ItemKey) -> RowKey {
/// one -- collisions are not a correctness risk worth guarding against here
/// (a `DefaultHasher` collision across the run ids one session produces is
/// astronomically unlikely, and the consequence of one would only be two
/// tool-call rows sharing a list slot, not data loss), and the high bit is
/// forced on so a hashed key can never collide with a real sequence number
/// (this build never produces 2^63 events).
pub fn row_key(key: &crate::client::transcript_fold::ItemKey) -> RowKey {
use crate::client::transcript_fold::ItemKey;
use std::hash::{Hash, Hasher};
match key { match key {
ItemKey::Seq(seq) => *seq, ItemKey::Seq(seq) => *seq,
ItemKey::RunId(id) => { ItemKey::RunId(id) => {
let mut h = std::collections::hash_map::DefaultHasher::new(); let mut h = DefaultHasher::new();
id.hash(&mut h); id.hash(&mut h);
h.finish() | (1 << 63) h.finish() | (1 << 63)
} }
@@ -94,19 +93,10 @@ fn tool_call_markdown(tool: &str, input: &str, output: &str) -> String {
pub struct RowBlocks { pub struct RowBlocks {
blocks: Vec<Block>, blocks: Vec<Block>,
fields: Vec<WeakWidget<Text>>, fields: Vec<WeakWidget<Text>>,
links: Vec<Rc<RefCell<Vec<Link>>>>, links: Vec<LinkTargets>,
column: WeakWidget<Span>, column: WeakWidget<Span>,
sender: Option<String>, sender: Option<String>,
/// Whether this row draws less than the whole message /// A capped row must be rebuilt before accepting a delta.
/// ([`cap_message`]). A delta cannot be appended to a capped row --
/// the new text would go on *below* the "Show all" that says it is
/// hidden -- so [`RowBlocks::apply_delta`] refuses one and the caller
/// rebuilds instead.
///
/// Never `true` for the row a reply is actually streaming into: the
/// live tail is built uncapped ([`build_row`]'s `cap`), which is what
/// keeps the refusal from costing anything in practice. This field is
/// the belt to that braces.
capped: bool, capped: bool,
theme: Rc<Theme>, theme: Rc<Theme>,
} }
@@ -126,17 +116,7 @@ fn display_blocks(markdown_src: &str) -> Vec<Block> {
} }
} }
/// `blocks` cut to what a row draws, with the line count of the **whole** /// Caps at a block boundary when possible, or within the first oversized block.
/// message; `None` when all of it fits.
///
/// The cut prefers a **block boundary**, because a message is markdown and
/// a whole paragraph is a smaller version of a message in a way that half
/// a paragraph is not. Where one block is over the bound by itself -- the
/// reply that is one enormous fence -- that block is truncated instead of
/// being dropped or drawn whole: dropping it would leave a row saying
/// nothing, and a truncated fence still renders as a fence, since the
/// renderer already knows the block's kind and pulldown-cmark closes an
/// unterminated one at the end of its input.
fn cap_message(blocks: Vec<Block>, cap: bool) -> (Vec<Block>, Option<usize>) { fn cap_message(blocks: Vec<Block>, cap: bool) -> (Vec<Block>, Option<usize>) {
let total = || blocks.iter().map(|b| b.source.lines().count()).sum(); let total = || blocks.iter().map(|b| b.source.lines().count()).sum();
if !cap { if !cap {
@@ -167,30 +147,50 @@ fn cap_message(blocks: Vec<Block>, cap: bool) -> (Vec<Block>, Option<usize>) {
(kept, None) (kept, None)
} }
/// A message's own text, kept so that asking for the whole of a capped row /// Shared with the "Show all" callback to avoid copying a long message.
/// can rebuild it. `Rc` rather than a copy per closure: the source of a
/// long message is the largest string in the row, and the tap handler
/// would otherwise hold a second one for the lifetime of the row.
struct RowSource { struct RowSource {
sender: Option<String>, sender: Option<String>,
markdown: String, markdown: String,
} }
#[derive(Clone)]
struct LinkTargets(Rc<RefCell<Vec<Link>>>);
impl LinkTargets {
fn new(links: Vec<Link>) -> Self {
Self(Rc::new(RefCell::new(links)))
}
fn replace(&self, links: Vec<Link>) {
*self.0.borrow_mut() = links;
}
fn url_at(&self, byte: usize) -> Option<String> {
self.0
.borrow()
.iter()
.find(|link| link.range.contains(&byte))
.map(|link| link.url.clone())
}
}
struct BuiltBlock {
field: WeakWidget<Text>,
widget: StrongWidget,
links: LinkTargets,
}
const FRAME_PAD_DP: f32 = 10.0; const FRAME_PAD_DP: f32 = 10.0;
const QUOTE_BAR_DP: f32 = 3.0; const QUOTE_BAR_DP: f32 = 3.0;
const FRAME_RADIUS_DP: f32 = 8.0; const FRAME_RADIUS_DP: f32 = 8.0;
fn build_block<Rsc: HasEvents>( fn build_block<Rsc: HasEvents>(rsc: &mut Rsc, block: &Block, theme: &Theme) -> BuiltBlock
rsc: &mut Rsc,
block: &Block,
theme: &Theme,
) -> (WeakWidget<Text>, StrongWidget, Rc<RefCell<Vec<Link>>>)
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
let frame = frame_of(block.kind, theme); let frame = frame_of(block.kind, theme);
let rendered = render_block(block, BASE_SIZE, theme); let rendered = render_block(block, BASE_SIZE, theme);
let links = Rc::new(RefCell::new(rendered.links)); let links = LinkTargets::new(rendered.links);
let verbatim = matches!(frame, BlockFrame::Verbatim { .. }); let verbatim = matches!(frame, BlockFrame::Verbatim { .. });
let field = wtext(rendered.text) let field = wtext(rendered.text)
.spans(rendered.spans) .spans(rendered.spans)
@@ -218,17 +218,10 @@ where
selection.drag(id, rsc, input) selection.drag(id, rsc, input)
}) })
.unwrap_or(SelectionInput::Tapped); .unwrap_or(SelectionInput::Tapped);
// A *tap*, decided by the same `DragArbiter` the pan and // Panning or selecting across a link must not open it.
// the selection are: a gesture that panned the list past
// this link, or held long enough to select, must not also
// follow it (`GestureOutcome::Tapped`'s doc).
if outcome == SelectionInput::Tapped { if outcome == SelectionInput::Tapped {
let byte = field.selection(rsc).byte_at(pos, size); let byte = field.selection(rsc).byte_at(pos, size);
let url = tap_links let url = tap_links.url_at(byte);
.borrow()
.iter()
.find(|l| l.range.contains(&byte))
.map(|l| l.url.clone());
if let Some(url) = url { if let Some(url) = url {
log::info!("iris link: opening {url}"); log::info!("iris link: opening {url}");
<Rsc::State as OpenUrl>::open_url(ctx.state, &url); <Rsc::State as OpenUrl>::open_url(ctx.state, &url);
@@ -257,7 +250,11 @@ where
.add_strong(rsc) .add_strong(rsc)
.any(), .any(),
}; };
(field, framed, links) BuiltBlock {
field,
widget: framed,
links,
}
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -305,10 +302,10 @@ where
let mut fields = Vec::with_capacity(blocks.len()); let mut fields = Vec::with_capacity(blocks.len());
let mut links = Vec::with_capacity(blocks.len()); let mut links = Vec::with_capacity(blocks.len());
for block in &blocks { for block in &blocks {
let (field, framed, block_links) = build_block(rsc, block, &theme); let built = build_block(rsc, block, &theme);
fields.push(field); fields.push(built.field);
links.push(block_links); links.push(built.links);
column.push(framed); column.push(built.widget);
} }
if let Some(lines) = hidden { if let Some(lines) = hidden {
column.push(show_all( column.push(show_all(
@@ -323,14 +320,7 @@ where
} }
let column = column.add(rsc); let column = column.add(rsc);
// `.add` (weak), not `.add_strong` -- `header` is about to be embedded // The parent composition performs the header's single strong registration.
// as a child of the `.span(Dir::DOWN)` below, whose own composition is
// what performs the *one* real strong registration each child gets.
// Calling `.add_strong`/`.upgrade` here too, then feeding a `.weak()`
// copy into that composition, tried to strong-register the same id
// twice and panicked with "was already added"
// (`core/src/widget/like.rs:12`) -- found running this crate's own
// `run-headless.sh` example, the first real render of a row.
let header: WeakWidget = match &source.sender { let header: WeakWidget = match &source.sender {
Some(name) => wtext(name.clone()) Some(name) => wtext(name.clone())
.size(13.0) .size(13.0)
@@ -359,9 +349,7 @@ where
) )
} }
/// The `RowBlocks` the rebuild produces is **discarded**, because a capped /// Rebuilds a capped row uncapped; its incremental state is intentionally discarded.
/// row is never the row a reply is streaming into (`build_row`'s `cap`) --
/// so nothing is holding one for it, and there is nothing to keep in step.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn show_all<Rsc: HasEvents>( fn show_all<Rsc: HasEvents>(
rsc: &mut Rsc, rsc: &mut Rsc,
@@ -395,12 +383,7 @@ where
} }
impl RowBlocks { impl RowBlocks {
/// Bring this row up to date with `markdown_src` **without** re-laying /// Updates only the changed tail blocks, or returns `false` when a rebuild is required.
/// out the blocks that did not change, and say whether that was
/// possible. `false` means the caller must rebuild the row the
/// ordinary way: an earlier block was rewritten (markdown allows it --
/// a trailing `---` turns the paragraph above into a heading), the
/// sender changed, or the message got shorter.
pub fn apply_delta<Rsc: HasEvents>( pub fn apply_delta<Rsc: HasEvents>(
&mut self, &mut self,
rsc: &mut Rsc, rsc: &mut Rsc,
@@ -413,20 +396,12 @@ impl RowBlocks {
if self.sender.as_deref() != sender { if self.sender.as_deref() != sender {
return false; return false;
} }
// A capped row draws less than the message it was built from, so
// appending to it would put the new text *below* the "Show all"
// saying the rest is hidden. The caller rebuilds instead, and
// rebuilds uncapped (`TranscriptScreen::apply`), so this refusal
// costs one rebuild per message rather than one per delta.
if self.capped { if self.capped {
return false; return false;
} }
let new_blocks = display_blocks(markdown_src); let new_blocks = display_blocks(markdown_src);
let common = common_prefix(&self.blocks, &new_blocks); let common = common_prefix(&self.blocks, &new_blocks);
// Everything already drawn must either be kept whole (`common == // A delta may append or rewrite only the current final block.
// len`, a pure append) or be kept except for the last block, which
// is the one a delta lands in. Anything else means an already
// laid-out block is no longer what it was.
if new_blocks.len() < self.blocks.len() || common + 1 < self.blocks.len() { if new_blocks.len() < self.blocks.len() || common + 1 < self.blocks.len() {
return false; return false;
} }
@@ -449,14 +424,14 @@ impl RowBlocks {
(Some(field), Some(links)) => { (Some(field), Some(links)) => {
let rendered = render_block(block, BASE_SIZE, &self.theme); let rendered = render_block(block, BASE_SIZE, &self.theme);
field(rsc).set_with_spans(rendered.text, rendered.spans); field(rsc).set_with_spans(rendered.text, rendered.spans);
*links.borrow_mut() = rendered.links; links.replace(rendered.links);
} }
_ => { _ => {
let (field, framed, links) = build_block(rsc, block, &self.theme); let built = build_block(rsc, block, &self.theme);
self.fields.push(field); self.fields.push(built.field);
self.links.push(links); self.links.push(built.links);
if let Some(column) = rsc.ui_mut().widgets.get_mut(&self.column) { if let Some(column) = rsc.ui_mut().widgets.get_mut(&self.column) {
column.push(framed); column.push(built.widget);
} }
} }
} }
@@ -481,20 +456,19 @@ where
build_text_row(rsc, list, key, sender, &markdown_src, cap, theme) build_text_row(rsc, list, key, sender, &markdown_src, cap, theme)
} }
/// Two mechanisms would have been two answers to the same question ("what /// Incremental state retained for whichever kind of row is currently last.
/// can this row do cheaply?"), so the caller holds one of these for its
/// tail row and asks it, rather than holding a `RowBlocks` and a
/// `ToolRow` and choosing between them at each call site.
pub enum TailRow { pub enum TailRow {
Blocks(RowBlocks), Blocks(RowBlocks),
Tools(ToolRow), Tools(ToolRow),
} }
/// `cap` draws a long message as [`cap_message`]'s worth of it behind a pub struct BuiltRow {
/// "Show all"; the caller passes `false` for the **live tail**, the row a pub key: RowKey,
/// reply is streaming into, because a row that grows while it is capped pub widget: StrongWidget,
/// would appear to stop growing (`RowBlocks::capped`). Every other row is pub tail: Option<TailRow>,
/// capped. }
/// `cap` limits historical rows; a live tail must remain uncapped.
pub fn build_row<Rsc: HasEvents>( pub fn build_row<Rsc: HasEvents>(
rsc: &mut Rsc, rsc: &mut Rsc,
list: WeakWidget<LazySpan>, list: WeakWidget<LazySpan>,
@@ -502,33 +476,35 @@ pub fn build_row<Rsc: HasEvents>(
working: bool, working: bool,
cap: bool, cap: bool,
theme: Rc<Theme>, theme: Rc<Theme>,
) -> (RowKey, StrongWidget, Option<TailRow>) ) -> BuiltRow
where where
Rsc::State: FocusHost + OpenUrl, Rsc::State: FocusHost + OpenUrl,
{ {
// A lone tool call is a card too, not a message with markdown in it: // A single tool call still draws as a card, without a redundant group wrapper.
// `group_tool_runs` leaves one call as a `Single` because "Called 1
// tool" hides a card to say the same thing in more words, and the
// *card* is what both cases draw (`ToolRows.kt`).
let calls = match row { let calls = match row {
FoldedRow::Single(item @ TranscriptItem::ToolRun { .. }) => { FoldedRow::Single(item @ TranscriptItem::ToolRun { .. }) => Some(slice::from_ref(item)),
Some(std::slice::from_ref(item))
}
FoldedRow::Tools(calls) => Some(calls.as_slice()), FoldedRow::Tools(calls) => Some(calls.as_slice()),
FoldedRow::Single(_) => None, FoldedRow::Single(_) => None,
}; };
if let Some(calls) = calls { if let Some(calls) = calls {
let key = row_key(&calls[0].key()); let key = row_key(&calls[0].key());
let (widget, tools) = let (widget, tools) = build_tool_row(rsc, list, key, calls.to_vec(), working, theme);
crate::ui::tool::build_tool_row(rsc, list, key, calls.to_vec(), working, theme); return BuiltRow {
return (key, widget, Some(TailRow::Tools(tools))); key,
widget,
tail: Some(TailRow::Tools(tools)),
};
} }
let FoldedRow::Single(item) = row else { let FoldedRow::Single(item) = row else {
unreachable!("every Tools row took the branch above"); unreachable!("every Tools row took the branch above");
}; };
let key = row_key(&item.key()); let key = row_key(&item.key());
let (widget, blocks) = build_single(rsc, list, key, item, cap, theme); let (widget, blocks) = build_single(rsc, list, key, item, cap, theme);
(key, widget, Some(TailRow::Blocks(blocks))) BuiltRow {
key,
widget,
tail: Some(TailRow::Blocks(blocks)),
}
} }
#[cfg(test)] #[cfg(test)]
+6 -2
View File
@@ -5,7 +5,11 @@ use crate::ui::markdown::highlight_into;
use crate::ui::tap::{hold_edge, on_tap}; use crate::ui::tap::{hold_edge, on_tap};
use crate::ui::theme::Theme; use crate::ui::theme::Theme;
use iris::prelude::*; use iris::prelude::*;
use std::{cell::Cell, cell::RefCell, collections::HashMap, rc::Rc}; use std::{
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
rc::Rc,
};
const NAME_SIZE: f32 = 14.0; const NAME_SIZE: f32 = 14.0;
const BODY_SIZE: f32 = 12.0; const BODY_SIZE: f32 = 12.0;
@@ -591,7 +595,7 @@ impl ToolRow {
let changed: Vec<usize> = (0..old.len()).filter(|&i| old[i] != calls[i]).collect(); let changed: Vec<usize> = (0..old.len()).filter(|&i| old[i] != calls[i]).collect();
self.shared.working.set(working); self.shared.working.set(working);
*self.shared.calls.borrow_mut() = calls.to_vec(); *self.shared.calls.borrow_mut() = calls.to_vec();
let ids: std::collections::HashSet<String> = calls let ids: HashSet<String> = calls
.iter() .iter()
.filter_map(|c| match c { .filter_map(|c| match c {
TranscriptItem::ToolRun { id, .. } => Some(id.clone()), TranscriptItem::ToolRun { id, .. } => Some(id.clone()),
+6 -4
View File
@@ -3,7 +3,9 @@ use iris::harness::{Harness, TouchAction};
use iris::prelude::*; use iris::prelude::*;
fn fence_scroll_in(h: &Harness, top: f32, bottom: f32) -> Option<(WidgetId, PixelRegion)> { 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 .active
.keys() .keys()
.copied() .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) .is_some_and(|s| s.axis() == Axis::X)
}) })
.find_map(|id| { .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)) (r.top_left.y >= top && r.bot_right.y <= bottom).then_some((id, r))
}) })
} }
@@ -47,7 +49,7 @@ fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() {
let mut h = Harness::new(phone_size(), PHONE_SCALE); let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let screen = opened.screen; let mut screen = opened.screen;
h.frame(0); h.frame(0);
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
@@ -117,7 +119,7 @@ fn a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting() {
let mut h = Harness::new(phone_size(), PHONE_SCALE); let mut h = Harness::new(phone_size(), PHONE_SCALE);
let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds");
let screen = opened.screen; let mut screen = opened.screen;
h.frame(0); h.frame(0);
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
+1 -1
View File
@@ -87,7 +87,7 @@ fn a_press_ended_by_a_cancel_leaves_no_origin_for_the_next_one() {
fn panning_a_code_fence_then_tapping_elsewhere_moves_nothing() { fn panning_a_code_fence_then_tapping_elsewhere_moves_nothing() {
use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow}; use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow};
let (mut h, screen) = opened(); let (mut h, mut screen) = opened();
let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg { let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg {
seq: 9_000_000, seq: 9_000_000,
text: "```\none two three four five six seven eight nine ten eleven twelve\n\ text: "```\none two three four five six seven eight nine ten eleven twelve\n\
+14 -9
View File
@@ -158,7 +158,10 @@ fn the_composer_sits_above_a_simulated_ime_inset() {
let (mut h, screen) = opened(); let (mut h, screen) = opened();
let height = h.size().y; let height = h.size().y;
let field_bottom = |h: &mut Harness| { let field_bottom = |h: &mut Harness| {
h.render h.rsc
.ui
.render_state()
.get()
.window_region(&screen.composer.field, &h.rsc) .window_region(&screen.composer.field, &h.rsc)
.expect("the composer field is on screen") .expect("the composer field is on screen")
.bot_right .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 "); assert_eq!(screen.composer.field.edit(&mut h.rsc).text.text(), "hi ");
let region = h let region = h
.render .rsc
.ui
.render_state()
.get()
.window_region(&screen.composer.field, &h.rsc) .window_region(&screen.composer.field, &h.rsc)
.expect("the composer field is drawn"); .expect("the composer field is drawn");
let size = region.bot_right - region.top_left; 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"); screen.composer.field.edit(&mut h.rsc).insert("a\n");
h.frame(PHONE_FRAME_MS); h.frame(PHONE_FRAME_MS);
} }
let message = h let render_handle = h.rsc.ui.render_state();
.render let render_state = render_handle.get();
let message = render_state
.debug(h.rsc.widgets(), "Message") .debug(h.rsc.widgets(), "Message")
.find(|a| !a.primitives.is_empty()) .find(|a| !a.primitives.is_empty())
.expect("the composer field is drawn"); .expect("the composer field is drawn");
let caret = h let caret = render_state.primitive_corners(message.primitives.last().unwrap().slot, &h.rsc);
.render
.primitive_corners(message.primitives.last().unwrap().slot, &h.rsc);
let bar_bottom = height - ime; let bar_bottom = height - ime;
let padding = 12.0 * PHONE_SCALE; let padding = 12.0 * PHONE_SCALE;
assert!( 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 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 let visible_content_top = message
.primitives .primitives
.iter() .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) .filter(|r| r.bot_right.y > bar.top_left.y)
.map(|r| r.top_left.y) .map(|r| r.top_left.y)
.fold(f32::INFINITY, f32::min); .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 { 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) .window_region(&screen.list.id(), &h.rsc)
.expect("the list is on screen") .expect("the list is on screen")
} }
fn drawn_rows(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> Vec<(f32, f32)> { fn drawn_rows(h: &Harness, screen: &ai_app::ui::TranscriptScreen) -> Vec<(f32, f32)> {
let mut rows: Vec<(f32, f32)> = h let render_handle = h.rsc.ui.render_state();
.render let render_state = render_handle.get();
let mut rows: Vec<(f32, f32)> = render_state
.active .active
.get(&screen.list.id()) .get(&screen.list.id())
.expect("the list is drawn") .expect("the list is drawn")
.children .children
.iter() .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)) .map(|px| (px.top_left.y, px.bot_right.y))
.collect(); .collect();
rows.sort_by(|a, b| a.0.total_cmp(&b.0)); 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] #[test]
fn the_list_is_clipped_to_its_own_box() { fn the_list_is_clipped_to_its_own_box() {
let (h, screen) = opened(); 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!( assert!(
active.mask != MaskIdx::NONE, active.mask != MaskIdx::NONE,
"the transcript's list is drawn with nothing clipping it", "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); let list = list_box(&h, &screen);
assert!( assert!(
clip.top_left.y >= list.top_left.y - 0.5 && clip.bot_right.y <= list.bot_right.y + 0.5, 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", edge still draws past it",
); );
let rows = h let rows = render_state
.render
.active .active
.get(&screen.list.id()) .get(&screen.list.id())
.expect("the list is drawn") .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> { 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(); return Vec::new();
}; };
let mut out: Vec<MaskIdx> = active let mut out: Vec<MaskIdx> = active
.primitives .primitives
.iter() .iter()
.filter(|p| p.binding != IMAGE_BINDING) .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(); .collect();
for child in &active.children { for child in &active.children {
out.extend(primitives_under(h, *child)); 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 ## 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 ### 1. The new `Widget` trait
```rust ```rust
+3 -2
View File
@@ -60,8 +60,9 @@ runs inside `cargo test`.
1. **Headless, in-process, no compositor and no GPU -- the default.** 1. **Headless, in-process, no compositor and no GPU -- the default.**
`iris::harness` (`iris/src/harness.rs`), plus the fixture crate it `iris::harness` (`iris/src/harness.rs`), plus the fixture crate it
opens. `Harness::new(size, density)` builds an `Rsc`, a opens. `Harness::new(size, density)` builds an `Rsc` whose `Ui` owns
`UiRenderState` and a state whose `FocusHost`/`OpenUrl` *record* what the retained render state, and a state whose `FocusHost`/`OpenUrl`
*record* what
the platform was asked for; `frame(t_ms)`/`frames_until(..)` run the platform was asked for; `frame(t_ms)`/`frames_until(..)` run
frames on a clock the test owns, and `replay(&TouchScript)` feeds a frames on a clock the test owns, and `replay(&TouchScript)` feeds a
recorded gesture one sample at a time exactly as recorded gesture one sample at a time exactly as
+45 -27
View File
@@ -2,14 +2,14 @@ use iris::prelude::*;
use std::time::Instant; use std::time::Instant;
struct BenchRsc { struct BenchRsc {
ui: UiData, ui: Ui,
} }
impl UiRsc for BenchRsc { impl UiRsc for BenchRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.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) { fn bench_first_frame(n: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (_list, root) = build_message_list(&mut rsc, n, 20); let (_list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
@@ -70,7 +68,12 @@ fn bench_first_frame(n: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let elapsed = start.elapsed(); let elapsed = start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
report( report(
&format!("(a) first frame, N={n}"), &format!("(a) first frame, N={n}"),
elapsed, elapsed,
@@ -81,9 +84,7 @@ fn bench_first_frame(n: usize) {
} }
fn bench_scroll(n: usize, ticks: usize) { fn bench_scroll(n: usize, ticks: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root) = build_message_list(&mut rsc, n, 20); let (scroll, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
@@ -101,7 +102,12 @@ fn bench_scroll(n: usize, ticks: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -120,9 +126,7 @@ fn bench_scroll(n: usize, ticks: usize) {
} }
fn bench_input_grows(n: usize, lines: usize) { fn bench_input_grows(n: usize, lines: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, list_root) = build_message_list(&mut rsc, n, 20); let (scroll, list_root) = build_message_list(&mut rsc, n, 20);
let list_area = rsc.ui.widgets.add_strong(Sized { let list_area = rsc.ui.widgets.add_strong(Sized {
inner: list_root, inner: list_root,
@@ -161,7 +165,12 @@ fn bench_input_grows(n: usize, lines: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -183,9 +192,7 @@ fn bench_input_grows(n: usize, lines: usize) {
} }
fn bench_insert_above_anchor(n: usize, inserts: usize) { fn bench_insert_above_anchor(n: usize, inserts: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (list, root) = build_message_list(&mut rsc, n, 20); let (list, root) = build_message_list(&mut rsc, n, 20);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((1080.0, 2000.0)); render.resize((1080.0, 2000.0));
@@ -208,7 +215,12 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -230,9 +242,7 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) {
} }
fn bench_expand_holds_edge(n: usize, growths: usize) { fn bench_expand_holds_edge(n: usize, growths: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let growable_index = n.saturating_sub(3); let growable_index = n.saturating_sub(3);
let mut growable = None; let mut growable = None;
@@ -280,7 +290,12 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -302,9 +317,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
} }
fn bench_redraw_big_text(chars: usize, redraws: usize) { fn bench_redraw_big_text(chars: usize, redraws: usize) {
let mut rsc = BenchRsc { let mut rsc = BenchRsc { ui: Ui::default() };
ui: UiData::default(),
};
let content: String = (0..chars) let content: String = (0..chars)
.map(|i| char::from(b'a' + (i % 26) as u8)) .map(|i| char::from(b'a' + (i % 26) as u8))
.collect(); .collect();
@@ -326,7 +339,12 @@ fn bench_redraw_big_text(chars: usize, redraws: usize) {
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
} }
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
report( report(
&format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"), &format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"),
total, total,
+20 -17
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
ActiveData, WidgetId, UiRenderState, WidgetId,
util::{HashMap, HashSet}, util::{HashMap, HashSet},
}; };
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
@@ -56,7 +56,6 @@ pub trait Controller<Rsc>: ControllerValue {
pub struct ControllerManager<Rsc> { pub struct ControllerManager<Rsc> {
by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>, by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>,
parents: HashMap<WidgetId, Option<WidgetId>>,
borrowed: HashSet<ControllerId>, borrowed: HashSet<ControllerId>,
removed_while_borrowed: HashSet<WidgetId>, removed_while_borrowed: HashSet<WidgetId>,
command_target: Option<ControllerId>, command_target: Option<ControllerId>,
@@ -68,7 +67,6 @@ impl<Rsc> Default for ControllerManager<Rsc> {
fn default() -> Self { fn default() -> Self {
Self { Self {
by_widget: Default::default(), by_widget: Default::default(),
parents: Default::default(),
borrowed: Default::default(), borrowed: Default::default(),
removed_while_borrowed: Default::default(), removed_while_borrowed: Default::default(),
command_target: None, command_target: None,
@@ -111,7 +109,11 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
.then_some(ControllerId { host, kind }) .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>(); let kind = TypeId::of::<C>();
loop { loop {
let candidate = ControllerId { host: origin, kind }; let candidate = ControllerId { host: origin, kind };
@@ -122,13 +124,14 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
if let Some(id) = self.id::<C>(origin) { if let Some(id) = self.id::<C>(origin) {
return Some(id); return Some(id);
} }
origin = self.parents.get(&origin).copied().flatten()?; origin = render_state.active.get(&origin)?.parent?;
} }
} }
pub fn path_to<C: Controller<Rsc>>( pub fn path_to<C: Controller<Rsc>>(
&self, &self,
mut origin: WidgetId, mut origin: WidgetId,
render_state: &UiRenderState,
) -> Option<(ControllerId, Vec<WidgetId>)> { ) -> Option<(ControllerId, Vec<WidgetId>)> {
let mut path = Vec::new(); let mut path = Vec::new();
loop { loop {
@@ -136,18 +139,10 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
if let Some(id) = self.id::<C>(origin) { if let Some(id) = self.id::<C>(origin) {
return Some((id, path)); 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> { pub fn take<C: Controller<Rsc>>(&mut self, id: ControllerId) -> Option<C> {
if id.kind != TypeId::of::<C>() { if id.kind != TypeId::of::<C>() {
return None; return None;
@@ -232,9 +227,18 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
self.command_boundary 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 { 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; return false;
}; };
if parent == ancestor { if parent == ancestor {
@@ -250,7 +254,6 @@ impl<Rsc: 'static> ControllerManager<Rsc> {
pub fn remove(&mut self, host: WidgetId) { pub fn remove(&mut self, host: WidgetId) {
self.by_widget.remove(&host); self.by_widget.remove(&host);
self.parents.remove(&host);
if self.borrowed.iter().any(|id| id.host == host) { if self.borrowed.iter().any(|id| id.host == host) {
self.removed_while_borrowed.insert(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) { fn draw(&mut self, active: &ActiveData) {
self.controllers.draw(active);
for t in self.widget_to_types.get(&active.id).into_flat_iter() { for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().draw(active); self.types.get_mut(t).unwrap().draw(active);
} }
} }
fn undraw(&mut self, active: &ActiveData) { fn undraw(&mut self, active: &ActiveData) {
self.controllers.undraw(active);
for t in self.widget_to_types.get(&active.id).into_flat_iter() { for t in self.widget_to_types.get(&active.id).into_flat_iter() {
self.types.get_mut(t).unwrap().undraw(active); 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, origin: impl IdLike,
f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T, f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T,
) -> Option<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)) 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 { fn run_command(&mut self, command: Command) -> CommandResult {
let revision = self.events().controllers.command_target_revision(); let revision = self.events().controllers.command_target_revision();
if let Some(boundary) = self.events().controllers.command_boundary() if let Some(boundary) = self.events().controllers.command_boundary() {
&& self let render_handle = self.ui().render_state();
.events() let outside_boundary =
.controllers self.events()
.command_target() .controllers
.is_none_or(|target| !self.events().controllers.is_below(target.host(), boundary)) .command_target()
{ .is_none_or(|target| {
return CommandResult::Unused; !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 { let Some((id, mut controller)) = self.events_mut().controllers.take_command_target() else {
return CommandResult::Unused; return CommandResult::Unused;
@@ -96,6 +108,12 @@ pub trait RunEvents: HasEvents {
data: <E::Event as Event>::Data<'_>, data: <E::Event as Event>::Data<'_>,
state: &mut Self::State, 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); let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self) f(EventCtx { state, data }, self)
} }
+12 -12
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
UiData, UiRenderState, Ui, UiData,
render::{ render::{
data::{PrimitiveInstance, instance_slot_layout}, data::{PrimitiveInstance, instance_slot_layout},
texture::GpuTextures, texture::GpuTextures,
@@ -180,13 +180,11 @@ impl UiRenderNode {
} }
} }
pub fn update( pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) -> FrameUpdateStats {
&mut self, let render_handle = ui.render_state.clone();
device: &Device, let mut render_guard = render_handle.get_mut();
queue: &Queue, let ui_render = &mut *render_guard;
ui: &mut UiData, let ui_data: &mut UiData = ui;
ui_render: &mut UiRenderState,
) -> FrameUpdateStats {
self.active.clear(); self.active.clear();
for (i, order) in ui_render.layers.iter_mut() { for (i, order) in ui_render.layers.iter_mut() {
self.active.push(i); self.active.push(i);
@@ -233,11 +231,11 @@ impl UiRenderNode {
} else { } else {
false 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 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 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); let paints_resized = self.paints.update(device, queue, entries, dirty);
if masks_resized || moves_resized || instances_resized || paints_resized { if masks_resized || moves_resized || instances_resized || paints_resized {
self.masks_group = Self::masks_group( self.masks_group = Self::masks_group(
@@ -249,7 +247,9 @@ impl UiRenderNode {
&self.paints, &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 { if rebuild_main {
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures); self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures);
} }
+1 -1
View File
@@ -32,7 +32,7 @@ fn entry_node(entry: &Entry) -> Node {
/// Owns the last tree pushed out, so `update` can tell "nothing /// Owns the last tree pushed out, so `update` can tell "nothing
/// accessibility-relevant changed" from "something did" without asking /// accessibility-relevant changed" from "something did" without asking
/// the platform adapter to diff two `Node`s itself. One of these per /// the platform adapter to diff two `Node`s itself. One of these per
/// window/view -- `default::DefaultUiState` and `android::AndroidUiState` /// window/view -- `desktop::DesktopUiState` and `android::AndroidUiState`
/// each keep one. /// each keep one.
#[derive(Default)] #[derive(Default)]
pub struct AccessTree { pub struct AccessTree {
+84 -2
View File
@@ -1,6 +1,11 @@
use crate::{ use crate::{
Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena, Mask, MoveOffset, Paints, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
}; };
use std::{
cell::{Ref, RefCell, RefMut},
ops::{Deref, DerefMut},
rc::Rc,
};
mod access; mod access;
mod active; mod active;
@@ -23,6 +28,75 @@ pub struct UiData {
animating: Vec<WidgetId>, 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 { impl UiData {
/// Ask for `id`'s [`crate::Widget::tick`] to run every frame until it /// Ask for `id`'s [`crate::Widget::tick`] to run every frame until it
/// says it is done. Idempotent -- registering an already-animating /// says it is done. Idempotent -- registering an already-animating
@@ -48,8 +122,16 @@ impl UiData {
} }
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &UiData; fn ui(&self) -> &Ui;
fn ui_mut(&mut self) -> &mut UiData; 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)] #[allow(unused_variables)]
fn on_add(&mut self, id: WeakWidget) {} fn on_add(&mut self, id: WeakWidget) {}
+28 -25
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, 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, WidgetId,
render::{ render::{
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive, Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
@@ -11,7 +11,7 @@ use crate::{
}; };
pub struct Painter<'a> { 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) rsc: &'a mut dyn UiRsc,
pub(super) region: UiRegion, pub(super) region: UiRegion,
@@ -46,7 +46,7 @@ impl DrawResult<'_, '_> {
if !self.painter.size_dependencies.contains(&self.child) { if !self.painter.size_dependencies.contains(&self.child) {
self.painter.size_dependencies.push(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) { let h = match self.take_recycled(P::BINDING, drawn) {
Some(h) => { Some(h) => {
self.state.primitives.recycle(&h, inst); self.render_state.primitives.recycle(&h, inst);
h 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 { if self.mask != MaskIdx::NONE {
self.rsc.ui_mut().masks.push_ref(self.mask); 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. /// Take ownership of a handle this widget just wrote.
fn own(&mut self, h: PrimitiveHandle) { fn own(&mut self, h: PrimitiveHandle) {
self.state self.render_state
.primitives .primitives
.set_handle_index(h.slot, self.primitives.len() as u32); .set_handle_index(h.slot, self.primitives.len() as u32);
self.primitives.push(h); self.primitives.push(h);
@@ -156,7 +156,7 @@ impl<'a> Painter<'a> {
/// with no radius argument anywhere that could fall out of step with /// with no radius argument anywhere that could fall out of step with
/// the one being drawn. /// the one being drawn.
pub fn set_mask_to_widget<W: ?Sized>(&mut self, shape: &StrongWidget<W>) { 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!( panic!(
"'{}' was given as a mask's shape but drew no primitive, so there is nothing to \ "'{}' was given as a mask's shape but drew no primitive, so there is nothing to \
clip 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 \ "set_mask called twice while drawing one widget: the second would replace the first \
rather than nest inside it", rather than nest inside it",
); );
let binding = self.state.primitives.instance(shape).binding; let binding = self.render_state.primitives.instance(shape).binding;
assert_eq!( assert_eq!(
binding, binding,
RectPrimitive::BINDING, RectPrimitive::BINDING,
@@ -248,7 +248,7 @@ impl<'a> Painter<'a> {
let next = [offset.x, offset.y]; let next = [offset.x, offset.y];
if self.rsc.ui().move_offsets[slot.idx()].delta != next { if self.rsc.ui().move_offsets[slot.idx()].delta != next {
self.rsc.ui_mut().move_offsets.get_mut(slot).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 None
} else { } 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()) { if len.is_some() && !self.size_dependencies.contains(&id.id()) {
self.size_dependencies.push(id.id()); self.size_dependencies.push(id.id());
@@ -277,7 +280,7 @@ impl<'a> Painter<'a> {
) -> DrawResult<'p, 'a> { ) -> DrawResult<'p, 'a> {
self.children.push(id.id()); self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot); let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner( self.render_state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
region, region,
@@ -300,16 +303,16 @@ impl<'a> Painter<'a> {
) -> DrawResult<'p, 'a> { ) -> DrawResult<'p, 'a> {
let region = region.within(&self.region); let region = region.within(&self.region);
let retained = self let retained = self
.state .render_state
.active .active
.get(&id.id()) .get(&id.id())
.map(|active| (active.layer, active.mask)); .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 { } else if let Some((layer, mask)) = retained {
self.children.push(id.id()); self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id()); self.rsc.widgets_mut().needs_redraw.insert(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot); let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner( self.render_state.draw_inner(
layer, layer,
id.id(), id.id(),
region, region,
@@ -322,7 +325,7 @@ impl<'a> Painter<'a> {
} else { } else {
self.children.push(id.id()); self.children.push(id.id());
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot); let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
self.state.draw_inner( self.render_state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
region, region,
@@ -385,7 +388,7 @@ impl<'a> Painter<'a> {
fn write_image(&mut self, texture_idx: u32, region: UiRegion) { fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) { let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
Some(h) => { Some(h) => {
self.state.primitives.recycle_image( self.render_state.primitives.recycle_image(
&h, &h,
self.id, self.id,
texture_idx, texture_idx,
@@ -395,7 +398,7 @@ impl<'a> Painter<'a> {
); );
h h
} }
None => self.state.write_image( None => self.render_state.write_image(
self.layer, self.layer,
self.id, self.id,
texture_idx, texture_idx,
@@ -416,9 +419,9 @@ impl<'a> Painter<'a> {
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> RenderedText { ) -> RenderedText {
let density = self.state.density; let density = self.render_state.density;
self.state.shape_count += 1; self.render_state.shape_count += 1;
let ui = self.rsc.ui_mut(); let ui: &mut UiData = self.rsc.ui_mut();
ui.text ui.text
.render(buffer, attrs, width, &mut ui.textures, density) .render(buffer, attrs, width, &mut ui.textures, density)
} }
@@ -477,17 +480,17 @@ impl<'a> Painter<'a> {
} }
pub fn output_size(&self) -> Vec2 { pub fn output_size(&self) -> Vec2 {
self.state.output_size self.render_state.output_size
} }
/// Physical pixels per `dp` -- see `UiRenderState::density`'s field /// Physical pixels per `dp` -- see `UiRenderState::density`'s field
/// doc. What `Len::dp`'s `apply_rest` call resolves against. /// doc. What `Len::dp`'s `apply_rest` call resolves against.
pub fn density(&self) -> f32 { pub fn density(&self) -> f32 {
self.state.density self.render_state.density
} }
pub fn px_size(&mut self) -> Vec2 { 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 { pub fn text_data(&mut self) -> &mut TextData {
@@ -495,11 +498,11 @@ impl<'a> Painter<'a> {
} }
pub fn child_layer(&mut self) { 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) { 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 { pub fn label(&self) -> &str {
+19 -11
View File
@@ -18,6 +18,14 @@ pub enum RedrawKind {
Updates, Updates,
} }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderCounters {
pub draws: u64,
pub region_rewrites: u64,
pub moves: u64,
pub shapes: u64,
}
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub primitives: Primitives, pub primitives: Primitives,
@@ -115,13 +123,13 @@ impl UiRenderState {
} }
} }
pub fn take_counters(&mut self) -> (u64, u64, u64, u64) { pub fn take_counters(&mut self) -> RenderCounters {
( RenderCounters {
std::mem::take(&mut self.draw_count), draws: std::mem::take(&mut self.draw_count),
std::mem::take(&mut self.region_mut_count), region_rewrites: std::mem::take(&mut self.region_mut_count),
std::mem::take(&mut self.mov_count), moves: std::mem::take(&mut self.mov_count),
std::mem::take(&mut self.shape_count), shapes: std::mem::take(&mut self.shape_count),
) }
} }
pub(super) fn note_move(&mut self) { pub(super) fn note_move(&mut self) {
@@ -441,7 +449,7 @@ impl UiRenderState {
] ]
}); });
let mut painter = Painter { let mut painter = Painter {
state: self, render_state: self,
region, region,
mask, mask,
move_slot, move_slot,
@@ -466,7 +474,7 @@ impl UiRenderState {
widget.size_hint(Axis::X).map(|len| len.fold_dp(density)), widget.size_hint(Axis::X).map(|len| len.fold_dp(density)),
widget.size_hint(Axis::Y).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); widget.draw(&mut painter);
let size = painter.size.unwrap_or_else(|| { let size = painter.size.unwrap_or_else(|| {
panic!( panic!(
@@ -491,10 +499,10 @@ impl UiRenderState {
); );
} }
drop(widget); drop(widget);
painter.state.draw_started.remove(&id); painter.render_state.draw_started.remove(&id);
let Painter { let Painter {
state: _, render_state: _,
rsc: _, rsc: _,
region, region,
mask: _, mask: _,
+9 -14
View File
@@ -4,24 +4,24 @@ const ROWS: usize = 1000;
const SETTLE_FRAMES: usize = 4; const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6; const FRAMES: usize = 6;
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
span: WeakWidget<Span>, span: WeakWidget<Span>,
frame: usize, frame: usize,
appended: bool, appended: bool,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let mut span = Span::empty(Dir::DOWN); let mut span = Span::empty(Dir::DOWN);
for _ in 0..ROWS { for _ in 0..ROWS {
let img = image::DynamicImage::new_rgba8(32, 32); let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc); let widget = image::<DesktopRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget); let widget = rsc.ui.widgets.add_strong(widget);
span.push(widget.any()); span.push(widget.any());
} }
@@ -40,12 +40,7 @@ impl DefaultAppState for State {
} }
} }
fn window_event( fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc<Self>) {
&mut self,
event: winit::event::WindowEvent,
rsc: &mut DefaultRsc<Self>,
_render: &mut UiRenderState,
) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) { if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
return; return;
} }
@@ -58,7 +53,7 @@ impl DefaultAppState for State {
if self.frame == SETTLE_FRAMES && !self.appended { if self.frame == SETTLE_FRAMES && !self.appended {
self.appended = true; self.appended = true;
let img = image::DynamicImage::new_rgba8(32, 32); let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc); let widget = image::<DesktopRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget); let widget = rsc.ui.widgets.add_strong(widget);
rsc.ui rsc.ui
.widgets .widgets
@@ -76,5 +71,5 @@ impl DefaultAppState for State {
} }
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
+6 -6
View File
@@ -2,12 +2,12 @@ use iris::prelude::*;
use winit::{dpi::LogicalSize, window::WindowAttributes}; use winit::{dpi::LogicalSize, window::WindowAttributes};
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
const ROWS: usize = 800; const ROWS: usize = 800;
@@ -58,14 +58,14 @@ fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
} }
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
} }
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
+6 -6
View File
@@ -1,18 +1,18 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
rect(PaintId::RED).set_root(rsc, &mut ui_state); rect(PaintId::RED).set_root(rsc, &mut ui_state);
+9 -13
View File
@@ -2,19 +2,19 @@ use iris::prelude::*;
use winit::event::WindowEvent; use winit::event::WindowEvent;
fn main() { fn main() {
DefaultApp::<Client>::run(); DesktopApp::<Client>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
pub struct Client { pub struct Client {
ui_state: DefaultUiState, ui_state: DesktopUiState,
info: WeakWidget<Text>, info: WeakWidget<Text>,
} }
impl DefaultAppState for Client { impl DesktopAppState for Client {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let widgets = tabs_ui::build(rsc, &mut ui_state); let widgets = tabs_ui::build(rsc, &mut ui_state);
@@ -24,16 +24,12 @@ impl DefaultAppState for Client {
} }
} }
fn window_event( fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc<Self>) {
&mut self, let render_state = rsc.ui.render_state();
_: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
let new = format!( let new = format!(
"widgets: {}\nactive: {}\nviews: {}", "widgets: {}\nactive: {}\nviews: {}",
rsc.widgets().len(), rsc.widgets().len(),
render.active_widgets(), render_state.get().active_widgets(),
self.ui_state.renderer.ui.view_count(), self.ui_state.renderer.ui.view_count(),
); );
if new != *rsc.widgets()[self.info].content { if new != *rsc.widgets()[self.info].content {
+6 -6
View File
@@ -2,18 +2,18 @@ use iris::prelude::*;
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let rect = rect(PaintId::RED).add(rsc); let rect = rect(PaintId::RED).add(rsc);
+7 -7
View File
@@ -1,15 +1,15 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
type Rsc = DefaultRsc<State>; type Rsc = DesktopRsc<State>;
#[derive(Clone, Copy, WidgetView)] #[derive(Clone, Copy, WidgetView)]
struct Test { struct Test {
@@ -35,10 +35,10 @@ impl Test {
} }
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
+9 -9
View File
@@ -101,8 +101,8 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
.into() .into()
} }
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))] #[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))]
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new(); let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input); let state: ItemStruct = parse_macro_input!(input);
@@ -112,14 +112,14 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
for field in &state.fields { for field in &state.fields {
if !found_attr if !found_attr
&& let Type::Path(path) = &field.ty && let Type::Path(path) = &field.ty
&& path.path.is_ident("DefaultUiState") && path.path.is_ident("DesktopUiState")
{ {
state_field = Some(field); state_field = Some(field);
} }
let Some(attr) = field let Some(attr) = field
.attrs .attrs
.iter() .iter()
.find(|a| a.path().is_ident("default_ui_state")) .find(|a| a.path().is_ident("desktop_ui_state"))
else { else {
continue; continue;
}; };
@@ -127,7 +127,7 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
output.extend( output.extend(
Error::new( Error::new(
attr.span(), attr.span(),
"cannot have more than one default_ui_state attribute", "cannot have more than one desktop_ui_state attribute",
) )
.into_compile_error(), .into_compile_error(),
); );
@@ -138,18 +138,18 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
} }
let Some(field) = state_field else { let Some(field) = state_field else {
output.extend( output.extend(
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(), Error::new(state.ident.span(), "no DesktopUiState field found").into_compile_error(),
); );
return output.into(); return output.into();
}; };
let sname = &state.ident; let sname = &state.ident;
let fname = field.ident.as_ref().unwrap(); let fname = field.ident.as_ref().unwrap();
output.extend(quote! { output.extend(quote! {
impl iris::default::HasDefaultUiState for #sname { impl iris::desktop::HasDesktopUiState for #sname {
fn default_state(&self) -> &iris::default::DefaultUiState { fn desktop_state(&self) -> &iris::desktop::DesktopUiState {
&self.#fname &self.#fname
} }
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState { fn desktop_state_mut(&mut self) -> &mut iris::desktop::DesktopUiState {
&mut self.#fname &mut self.#fname
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@
# `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md, # `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md,
# carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that # carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that
# density to iris the way `DisplayMetrics.density` does on Android # density to iris the way `DisplayMetrics.density` does on Android
# (`iris::default::content_scale`). So a screenshot from here and one # (`iris::desktop::content_scale`). So a screenshot from here and one
# from the phone are the same layout at the same density, and what # from the phone are the same layout at the same density, and what
# differs is only the renderer. Without it the output stays desktop- # differs is only the renderer. Without it the output stays desktop-
# shaped, which is what every other example wants. # shaped, which is what every other example wants.
+3 -9
View File
@@ -3,9 +3,7 @@ use crate::prelude::*;
#[test] #[test]
fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() { fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc); let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc);
let root = leaf.upgrade(&mut rsc).any(); let root = leaf.upgrade(&mut rsc).any();
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
@@ -37,9 +35,7 @@ fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
#[test] #[test]
fn a_widget_with_no_label_never_reaches_the_tree() { fn a_widget_with_no_label_never_reaches_the_tree() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE)); let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE));
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((800.0, 600.0)); render.resize((800.0, 600.0));
@@ -55,9 +51,7 @@ fn a_widget_with_no_label_never_reaches_the_tree() {
#[test] #[test]
fn bounds_follow_a_moved_widget_and_updates_stay_incremental() { fn bounds_follow_a_moved_widget_and_updates_stay_incremental() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc); let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc);
let leaf_strong = leaf.upgrade(&mut rsc).any(); let leaf_strong = leaf.upgrade(&mut rsc).any();
let offset = rsc.ui.widgets.add_strong(Offset { let offset = rsc.ui.widgets.add_strong(Offset {
+4 -4
View File
@@ -4,7 +4,7 @@ use android_view::{
jni::{JavaVM, objects::GlobalRef}, jni::{JavaVM, objects::GlobalRef},
ndk::native_window::NativeWindow, ndk::native_window::NativeWindow,
}; };
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState}; use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode};
use pollster::FutureExt; use pollster::FutureExt;
use std::time::Instant; use std::time::Instant;
use wgpu::{ use wgpu::{
@@ -176,7 +176,7 @@ impl AndroidRenderer {
.filter(|part| !part.is_empty()) .filter(|part| !part.is_empty())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
// Say which adapter won, in the same words `default::render` uses, // Say which adapter won, in the same words `desktop::render` uses,
// and at startup rather than only on the Diagnostics page: the // and at startup rather than only on the Diagnostics page: the
// backend alone (logged by `view.rs` when a renderer is built) does // backend alone (logged by `view.rs` when a renderer is built) does
// not separate the cases that matter. In this checkout's emulator // not separate the cases that matter. In this checkout's emulator
@@ -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 atlas_pages_grown_prev = self.ui.take_atlas_pages_grown();
let image_bind_group_creates_prev = self.ui.take_image_bind_group_creates(); 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; self.frame_count += 1;
FrameDiagnostics { FrameDiagnostics {
masks_resized: stats.masks_resized, masks_resized: stats.masks_resized,
+128 -200
View File
@@ -11,13 +11,7 @@ use android_view::{
}, },
ndk::event::{Axis, Keycode, MotionAction}, ndk::event::{Axis, Keycode, MotionAction},
}; };
use std::{ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
cell::RefCell,
marker::{PhantomData, Sized},
rc::Rc,
sync::Arc,
time::Instant,
};
use super::{ use super::{
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
@@ -25,10 +19,7 @@ use super::{
render::{AndroidRedrawHandle, AndroidRenderer}, render::{AndroidRedrawHandle, AndroidRenderer},
}; };
/// The android-view analogue of `default::DefaultUiState`. `renderer` is an /// Android host state. The renderer follows the `SurfaceView` lifecycle.
/// `Option` because a `SurfaceView`'s surface does not outlive backgrounding
/// the way a winit `Window` does -- `surfaceDestroyed`/`surfaceCreated` can
/// happen any number of times over the life of one `IrisViewPeer`.
/// How many frames after each `surface_changed` `render()` logs a full /// How many frames after each `surface_changed` `render()` logs a full
/// diagnostic line for -- see the log site's own comment. /// diagnostic line for -- see the log site's own comment.
const DIAGNOSTIC_FRAMES: u64 = 10; const DIAGNOSTIC_FRAMES: u64 = 10;
@@ -39,24 +30,13 @@ pub struct AndroidUiState {
pub focus: Option<WeakWidget<TextEdit>>, pub focus: Option<WeakWidget<TextEdit>>,
pub cursor: CursorState, pub cursor: CursorState,
pub last_click: Instant, pub last_click: Instant,
/// The IME preedit's previous length, in `char`s -- the same /// Previous IME preedit length, in characters.
/// re-send-the-whole-composition bookkeeping `default::DefaultUiState`
/// keeps for winit's `Ime::Preedit`, since android-view's
/// `setComposingText` has the identical shape (see `android/ime.rs`).
pub compose_len: usize, pub compose_len: usize,
/// Set by `attr::FocusHost::focus_gained` when a `TextEdit` is focused; /// Deferred until the input callback regains access to JNI.
/// consumed by the touch handler after the sensor pass finishes, since
/// showing the keyboard is a JNI call and `focus_gained` runs deep
/// inside the platform-agnostic sensor dispatch with no `CallbackCtx`
/// in reach.
pub pending_show_keyboard: bool, pub pending_show_keyboard: bool,
/// A URL a tapped link asked the platform to open, for the same /// Also deferred until a JNI callback is available.
/// reason `pending_show_keyboard` is a flag rather than a call --
/// see `android/platform.rs`.
pub pending_open_url: Option<String>, pub pending_open_url: Option<String>,
/// Window insets, filled in from outside the normal `ViewPeer` callback /// Filled by the native insets callback registered in `android/insets.rs`.
/// path -- see `android/insets.rs` for why they need a registry of
/// their own.
shared: Rc<RefCell<Shared>>, shared: Rc<RefCell<Shared>>,
pub access_adapter: AccessAdapter, pub access_adapter: AccessAdapter,
pub access: AccessTree, pub access: AccessTree,
@@ -123,7 +103,7 @@ pub trait HasAndroidUiState: Sized + 'static {
pub trait AndroidAppState: HasAndroidUiState { pub trait AndroidAppState: HasAndroidUiState {
fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self; fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self;
#[allow(unused_variables)] #[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 false
} }
#[allow(unused_variables)] #[allow(unused_variables)]
@@ -132,13 +112,7 @@ pub trait AndroidAppState: HasAndroidUiState {
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {} fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {}
} }
/// `insets::Insets` as `f32`, for the widget-facing callback above -- a /// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
/// distinct type from `insets::Insets` so a caller of `on_insets_changed`
/// is not coupled to that module's own (`i32`, JNI-shaped) representation.
/// Both are physical pixels; this used to divide by `content_scale` into a
/// separate *logical* unit (hence the old name, `LogicalInsets`), back when
/// the rest of layout was logical too -- see `AndroidUiState::content_scale`'s
/// field comment for why that stopgap is gone.
#[derive(Clone, Copy, Default, Debug, PartialEq)] #[derive(Clone, Copy, Default, Debug, PartialEq)]
pub struct WindowInsets { pub struct WindowInsets {
pub left: f32, pub left: f32,
@@ -166,99 +140,19 @@ impl WindowInsets {
} }
} }
pub struct AndroidRsc<State: 'static> { pub type AndroidRsc<State> = AppRsc<State>;
pub ui: UiData,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> AndroidRsc<State> {
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 AndroidRsc<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 AndroidRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for AndroidRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State: 'static> HasTasks for AndroidRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for AndroidRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
/// The `ViewPeer` android-view dispatches every callback to. One per /// The `ViewPeer` android-view dispatches every callback to. One per
/// `RustView` instance; `new_peer` (below) builds it and hands the id to /// `RustView` instance; `new_peer` (below) builds it and hands the id to
/// Java the same way android-view's own demo does. /// Java the same way android-view's own demo does.
pub struct IrisViewPeer<State: AndroidAppState> { pub struct IrisViewPeer<State: AndroidAppState> {
pub(super) rsc: AndroidRsc<State>, pub(super) rsc: AndroidRsc<State>,
pub(super) render: UiRenderState,
pub(super) state: State, pub(super) state: State,
task_recv: TaskMsgReceiver<AndroidRsc<State>>, task_recv: TaskMsgReceiver<AndroidRsc<State>>,
/// The one ruler this view dates everything on: touch samples in /// Converts input and Choreographer timestamps onto one monotonic clock.
/// `on_touch_event` and the `Choreographer` frame time in `do_frame`.
/// Anchored by whichever of the two arrives first and never
/// re-anchored after, which is what lets a fling be advanced on the
/// same clock the gesture that launched it was measured on. Its path
/// out is the peer's own drop: it holds nothing but three numbers and
/// is meaningless to any other view.
device_clock: Option<DeviceClock>, device_clock: Option<DeviceClock>,
} }
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::Index<I> for AndroidRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::IndexMut<I> for AndroidRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
impl<State: AndroidAppState> IrisViewPeer<State> { impl<State: AndroidAppState> IrisViewPeer<State> {
fn drain_tasks(&mut self) { fn drain_tasks(&mut self) {
while let Ok(update) = self.task_recv.try_recv() { while let Ok(update) = self.task_recv.try_recv() {
@@ -277,7 +171,9 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
let cursor = ui_state.cursor.clone(); let cursor = ui_state.cursor.clone();
let old_focus = ui_state.focus; 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); .run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
@@ -297,7 +193,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
/// Common tail for every callback that might have changed the cursor, /// Common tail for every callback that might have changed the cursor,
/// the text focus, or the widget tree: run the sensors that touch /// the text focus, or the widget tree: run the sensors that touch
/// input feeds, then ask for a frame if the result needs drawing. /// input feeds, then ask for a frame if the result needs drawing.
/// Mirrors `default::DefaultApp::window_event`'s tail, split across /// Mirrors `desktop::DesktopApp::window_event`'s tail, split across
/// android-view's several entry points instead of winit's one. /// android-view's several entry points instead of winit's one.
pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) { pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) {
self.run_input_frame(ctx); self.run_input_frame(ctx);
@@ -306,7 +202,11 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
ui_state.cursor.end_frame(); 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); ctx.view.post_frame_callback(&mut ctx.env);
} }
} }
@@ -317,6 +217,42 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
.get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest)) .get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest))
} }
fn generic_motion(&mut self, ctx: &mut CallbackCtx, event: &MotionEvent<'_>) -> bool {
let action = event.action_masked(&mut ctx.env);
let event_time = event.event_time_nanos(&mut ctx.env);
let mut clock = self.device_clock(event_time, event_time);
let at = clock.sample(event_time);
self.device_clock = Some(clock);
let ui = self.state.android_state_mut();
ui.cursor.time = at;
ui.cursor.pos = vec2(event.x(&mut ctx.env), event.y(&mut ctx.env));
ui.cursor.exists = !matches!(action, MotionAction::HoverExit);
let buttons = event.button_state(&mut ctx.env);
ui.cursor.buttons.left.update(buttons.primary());
ui.cursor.buttons.right.update(buttons.secondary());
ui.cursor.buttons.middle.update(buttons.teriary());
match action {
MotionAction::HoverEnter
| MotionAction::HoverMove
| MotionAction::HoverExit
| MotionAction::ButtonPress
| MotionAction::ButtonRelease => {}
MotionAction::Scroll => {
ui.cursor.scroll_delta = vec2(
event.axis(&mut ctx.env, Axis::Hscroll, 0),
event.axis(&mut ctx.env, Axis::Vscroll, 0),
);
}
_ => return false,
}
self.after_input(ctx);
true
}
fn window_size(&self) -> Vec2 { fn window_size(&self) -> Vec2 {
let ui_state = self.state.android_state(); let ui_state = self.state.android_state();
match &ui_state.renderer { match &ui_state.renderer {
@@ -360,11 +296,17 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
"render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}", "render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}",
ui_state.root.is_some(), ui_state.root.is_some(),
self.rsc.widgets().len(), self.rsc.widgets().len(),
self.render.active_widgets(), self.rsc.ui.render_state().get().active_widgets(),
ui_state ui_state
.root .root
.as_ref() .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(), self.window_size(),
); );
} }
@@ -374,12 +316,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
ctx.view.post_frame_callback(&mut ctx.env); ctx.view.post_frame_callback(&mut ctx.env);
} }
let ui_state = self.state.android_state_mut(); 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 ui_state = self.state.android_state_mut();
let Some(renderer) = &mut ui_state.renderer else { let Some(renderer) = &mut ui_state.renderer else {
return; 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 { if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
log::info!( log::info!(
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \ "iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
@@ -400,26 +342,33 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
.android_state_mut() .android_state_mut()
.frame_report .frame_report
.record(now, parts, animating); .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() { if crate::diagnostics::trace_enabled() {
let ui_state = self.state.android_state(); let ui_state = self.state.android_state();
log::debug!( log::debug!(
target: "iris::frame", target: "iris::frame",
"render(): after update active={} root_px={:?}", "render(): after update active={} root_px={:?}",
self.render.active_widgets(), self.rsc.ui.render_state().get().active_widgets(),
ui_state ui_state
.root .root
.as_ref() .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(); let ui_state = self.state.android_state_mut();
if let Some(tree_update) = if let Some(tree_update) = ui_state.access.update(
ui_state self.rsc.widgets(),
.access &self.rsc.ui.render_state().get(),
.update(self.rsc.widgets(), &self.render, &self.rsc) &self.rsc,
{ ) {
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) { if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) {
ctx.push_dynamic_deferred_callback(move |env, view| { ctx.push_dynamic_deferred_callback(move |env, view| {
@@ -467,7 +416,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.after_input(ctx); self.after_input(ctx);
return true; 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 { if handled {
self.after_input(ctx); self.after_input(ctx);
} }
@@ -496,27 +445,14 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
) -> bool { ) -> bool {
self.drain_tasks(); self.drain_tasks();
let action = event.action_masked(&mut ctx.env); let action = event.action_masked(&mut ctx.env);
// Device (physical) pixels, same space layout now uses throughout // MotionEvent and layout both use physical pixels.
// -- see `AndroidUiState::content_scale`'s field comment.
let x = event.x(&mut ctx.env); let x = event.x(&mut ctx.env);
let y = event.y(&mut ctx.env); let y = event.y(&mut ctx.env);
// The event's own clock, converted through the view's one anchor // Use the event clock so batched movement keeps its real timing.
// -- taken on whichever of a touch or a frame callback came first.
// Android reports sample times in the `SystemClock.uptimeMillis()`
// base, which is the same `CLOCK_MONOTONIC` an `Instant` reads, so
// a single `(Instant, nanos)` pair converts every later sample
// exactly. Anchoring **once** rather than per event is what keeps
// the times ordered, and anchoring on the first event's *oldest*
// sample rather than on its own time is what keeps that event's
// batch from collapsing onto one instant -- `sense::DeviceClock`'s
// doc has both, and owns the arithmetic so it can be unit-tested
// off a device (`sense_tests.rs`). See `CursorState::time`.
let event_time = event.event_time_nanos(&mut ctx.env); let event_time = event.event_time_nanos(&mut ctx.env);
let history = event.history_size(&mut ctx.env); let history = event.history_size(&mut ctx.env);
let mut clock = match self.device_clock { let mut clock = match self.device_clock {
Some(clock) => clock, Some(clock) => clock,
// Only the call that anchors needs the batch's oldest sample,
// so the JNI read for it stays off the per-event path.
None => { None => {
let oldest = if history > 0 { let oldest = if history > 0 {
event.historical_event_time_nanos(&mut ctx.env, 0) event.historical_event_time_nanos(&mut ctx.env, 0)
@@ -526,24 +462,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.device_clock(event_time, oldest) self.device_clock(event_time, oldest)
} }
}; };
// `iris::input`'s own doc (`sense::log_input_event`): collected // Avoid allocating trace history when input tracing is disabled.
// only when tracing is on, since this is otherwise a `Vec` per
// `MotionEvent` for a line nobody is reading -- the JNI reads
// themselves (`historical_axis`/`historical_event_time_nanos`
// below) already happen unconditionally, for the replay this
// function does regardless of tracing.
let trace_input = crate::diagnostics::trace_enabled(); let trace_input = crate::diagnostics::trace_enabled();
let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new(); let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new();
if matches!(action, MotionAction::Move) { if matches!(action, MotionAction::Move) {
// Android documents the historical samples as oldest first and // Android orders history oldest-first; `sample` checks monotonicity.
// the event's own sample as the newest of the batch; everything
// downstream (`VelocityTracker`, `DragArbiter`'s long-press
// clock) assumes it, so say so here rather than at each reader.
// `DeviceClock::sample` is what asserts it, and it carries the
// last sample seen *across* events, so the first sample of
// every event is checked against the previous event's last one
// rather than against the anchor.
for pos in 0..history { for pos in 0..history {
let hx = event.historical_axis(&mut ctx.env, Axis::X, 0, pos); let hx = event.historical_axis(&mut ctx.env, Axis::X, 0, pos);
let hy = event.historical_axis(&mut ctx.env, Axis::Y, 0, pos); let hy = event.historical_axis(&mut ctx.env, Axis::Y, 0, pos);
@@ -598,6 +522,38 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
true true
} }
fn on_generic_motion_event<'local>(
&mut self,
ctx: &mut CallbackCtx<'local>,
event: &MotionEvent<'local>,
) -> bool {
self.drain_tasks();
self.generic_motion(ctx, event)
}
fn on_hover_event<'local>(
&mut self,
ctx: &mut CallbackCtx<'local>,
event: &MotionEvent<'local>,
) -> bool {
let action = event.action(&mut ctx.env);
let x = event.x(&mut ctx.env);
let y = event.y(&mut ctx.env);
let ui = self.state.android_state_mut();
if let Some(events) = ui
.access_adapter
.on_hover_event(&mut NullActionHandler, action, x, y)
{
ctx.push_dynamic_deferred_callback(move |env, view| {
raise_if_enabled(env, view, events);
});
true
} else {
self.drain_tasks();
self.generic_motion(ctx, event)
}
}
fn on_focus_changed<'local>( fn on_focus_changed<'local>(
&mut self, &mut self,
ctx: &mut CallbackCtx<'local>, ctx: &mut CallbackCtx<'local>,
@@ -628,32 +584,10 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
height: i32, height: i32,
) { ) {
self.drain_tasks(); self.drain_tasks();
// The layout engine's own notion of the canvas size is separate // The layout canvas and wgpu surface are separate and both use physical pixels.
// from the wgpu surface's -- winit's backend sets it from self.rsc.ui.resize((width as f32, height as f32));
// `WindowEvent::Resized`, and there is no equivalent automatic
// trigger here, so this is the one place android-view's surface
// size has to be told to `UiRenderState` too. Missing this drew
// nothing but the clear colour: the widget tree laid out against
// whatever size `UiRenderState::new` starts at instead of the
// surface's real one.
//
// **Physical pixels, matching `AndroidRenderer`'s own
// `size()`/`resize()`/`new()`** -- `AndroidUiState::content_scale`'s
// field comment. This call sets `UiRenderState::output_size`, which
// every `rel`/`rest` length resolves against and every `abs`
// pixel-region compares to directly; a `dp(56)` height now folds
// in the density at `Len::apply_rest` time instead of this call
// 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));
// `AndroidRenderer::resize` only reconfigures the wgpu surface and // Resizing preserves GPU resources; recreating a destroyed surface does not.
// rewrites the window uniform -- device, atlas, buffers and bind
// groups are untouched, so the glyph cache's coordinates stay
// valid. A genuinely new surface (after `surface_destroyed`, e.g.
// backgrounding) still goes through `AndroidRenderer::new` below,
// since `renderer` is `None` in that case.
let already_live = self.state.android_state().renderer.is_some(); let already_live = self.state.android_state().renderer.is_some();
log::info!( log::info!(
"iris surface: surface_changed {width}x{height} already_live={already_live} \ "iris surface: surface_changed {width}x{height} already_live={already_live} \
@@ -779,9 +713,11 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>, ctx: &mut CallbackCtx<'local>,
virtual_view_id: jint, virtual_view_id: jint,
) -> AccessibilityNodeInfo<'local> { ) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource { let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(), widgets: self.rsc.widgets(),
render: &self.render, render: &render_state,
rsc: &self.rsc, rsc: &self.rsc,
}; };
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
@@ -798,9 +734,11 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>, ctx: &mut CallbackCtx<'local>,
focus_type: jint, focus_type: jint,
) -> AccessibilityNodeInfo<'local> { ) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource { let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(), widgets: self.rsc.widgets(),
render: &self.render, render: &render_state,
rsc: &self.rsc, rsc: &self.rsc,
}; };
let ui_state = self.state.android_state_mut(); let ui_state = self.state.android_state_mut();
@@ -861,26 +799,16 @@ pub fn new_peer<'local, State: AndroidAppState>(
let vm = env.get_java_vm().unwrap(); let vm = env.get_java_vm().unwrap();
let global_view = env.new_global_ref(&view.0).unwrap(); let global_view = env.new_global_ref(&view.0).unwrap();
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view)); let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
let (tasks, task_recv) = Tasks::init(redraw); let (mut rsc, task_recv) = AppRsc::new(redraw);
let mut rsc = AndroidRsc { rsc.ui.set_density(content_scale);
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: PhantomData,
};
rsc.ui.text.density = content_scale;
let shared = Rc::new(RefCell::new(Shared::default())); let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale); let ui_state = AndroidUiState::new(shared.clone(), content_scale);
let mut state = State::new(ui_state, &mut rsc); let mut state = State::new(ui_state, &mut rsc);
let platform_vm = env.get_java_vm().unwrap(); let platform_vm = env.get_java_vm().unwrap();
let platform_view = env.new_global_ref(&view.0).unwrap(); let platform_view = env.new_global_ref(&view.0).unwrap();
state.platform_ready(&mut rsc, platform_vm, platform_view); state.platform_ready(&mut rsc, platform_vm, platform_view);
let mut render = UiRenderState::new();
render.set_density(content_scale);
let peer = IrisViewPeer { let peer = IrisViewPeer {
rsc, rsc,
render,
state, state,
task_recv, task_recv,
device_clock: None, device_clock: None,
File renamed without changes.
File renamed without changes.
@@ -1,21 +1,21 @@
use crate::prelude::*; use crate::prelude::*;
use winit::dpi::{PhysicalPosition, PhysicalSize}; use winit::dpi::{PhysicalPosition, PhysicalSize};
impl<T: HasDefaultUiState> FocusHost for T { impl<T: HasDesktopUiState> FocusHost for T {
fn recent_click(&mut self) -> bool { fn recent_click(&mut self) -> bool {
crate::attr::recent_click(&mut self.default_state_mut().last_click) crate::attr::recent_click(&mut self.desktop_state_mut().last_click)
} }
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) { fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) {
self.default_state_mut().focus = id; self.desktop_state_mut().focus = id;
} }
fn is_focused(&self, id: WeakWidget<TextEdit>) -> bool { fn is_focused(&self, id: WeakWidget<TextEdit>) -> bool {
self.default_state().focus == Some(id) self.desktop_state().focus == Some(id)
} }
fn focus_gained(&mut self, region: Option<PixelRegion>) { fn focus_gained(&mut self, region: Option<PixelRegion>) {
let state = self.default_state_mut(); let state = self.desktop_state_mut();
let Some(region) = region else { return }; let Some(region) = region else { return };
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
@@ -19,7 +19,7 @@ pub struct Input {
impl Input { impl Input {
/// winit's pointer coordinates are physical pixels, which is the /// winit's pointer coordinates are physical pixels, which is the
/// space the whole tree is laid out and hit-tested in -- see /// space the whole tree is laid out and hit-tested in -- see
/// `default::content_scale`. Nothing is converted here; `dp(...)` /// `desktop::content_scale`. Nothing is converted here; `dp(...)`
/// resolves against the density at layout time instead. /// resolves against the density at layout time instead.
pub fn event(&mut self, event: &WindowEvent) -> bool { pub fn event(&mut self, event: &WindowEvent) -> bool {
match event { match event {
@@ -79,10 +79,10 @@ impl Input {
} }
} }
impl DefaultUiState { impl DesktopUiState {
/// Physical pixels, matching `WindowEvent::Resized` (what /// Physical pixels, matching `WindowEvent::Resized` (what
/// `UiRenderState::resize` is given) and the swapchain -- see /// `UiRenderState::resize` is given) and the swapchain -- see
/// `default::content_scale`. /// `desktop::content_scale`.
pub fn window_size(&self) -> Vec2 { pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size(); let size = self.renderer.window().inner_size();
Vec2::new(size.width as f32, size.height as f32) Vec2::new(size.width as f32, size.height as f32)
File renamed without changes.
@@ -1,10 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{ use std::{marker::Sized, sync::Arc, time::Instant};
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -26,19 +22,8 @@ pub use render::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub type Proxy<Event> = EventLoopProxy<Event>;
/// The desktop's `content_scale`: physical pixels per dp, the same /// Physical pixels per dp. Layout and input stay in physical pixels; only
/// quantity Android reads from `DisplayMetrics.density` and feeds to /// `dp(...)` resolves through this scale.
/// `UiRenderState::set_density` (`android::view::AndroidUiState::
/// content_scale`'s field comment). Everything in this backend is
/// physical pixels -- the window size, the pointer, the widget tree --
/// and `dp(...)` is what resolves against this at layout time, exactly
/// as on the phone. That is a correction from an earlier version that
/// divided winit's coordinates into a separate "logical" space instead:
/// it left `UiRenderState::resize` (physical, from `WindowEvent::
/// Resized`) and the window uniform (logical) disagreeing on any
/// display whose scale factor is not 1.0, and it rasterised glyphs at
/// one resolution to display them at another -- the blur the phone's own
/// stopgap produced before `dp` existed.
pub fn content_scale(window: &Window) -> f32 { pub fn content_scale(window: &Window) -> f32 {
match std::env::var("IRIS_SCALE") { match std::env::var("IRIS_SCALE") {
Err(_) => window.scale_factor() as f32, Err(_) => window.scale_factor() as f32,
@@ -52,7 +37,7 @@ pub fn content_scale(window: &Window) -> f32 {
} }
} }
pub struct DefaultUiState { pub struct DesktopUiState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
pub renderer: UiRenderer, pub renderer: UiRenderer,
pub input: Input, pub input: Input,
@@ -65,13 +50,13 @@ pub struct DefaultUiState {
pub access: AccessTree, pub access: AccessTree,
} }
impl<State: 'static> HasRoot<DefaultRsc<State>> for DefaultUiState { impl<State: 'static> HasRoot<DesktopRsc<State>> for DesktopUiState {
fn set_root(&mut self, rsc: &mut DefaultRsc<State>, root: StrongWidget) { fn set_root(&mut self, rsc: &mut DesktopRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root)); self.root = Some(crate::overlay::default_overlay_root(rsc, root));
} }
} }
impl DefaultUiState { impl DesktopUiState {
pub fn new(window: impl Into<Arc<Window>>, access_adapter: accesskit_winit::Adapter) -> Self { pub fn new(window: impl Into<Arc<Window>>, access_adapter: accesskit_winit::Adapter) -> Self {
let window = window.into(); let window = window.into();
Self { Self {
@@ -89,127 +74,35 @@ impl DefaultUiState {
} }
} }
pub trait HasDefaultUiState: Sized + 'static { pub trait HasDesktopUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState; fn desktop_state(&self) -> &DesktopUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState; fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DesktopAppState: HasDesktopUiState {
type Event = (); type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DesktopUiState, rsc: &mut DesktopRsc<Self>, proxy: Proxy<Self::Event>)
-> Self; -> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event( fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc<Self>) {}
&mut self,
event: Self::Event,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DefaultRsc<Self>, render: &mut UiRenderState) {} fn exit(&mut self, rsc: &mut DesktopRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn window_event( fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc<Self>) {}
&mut self,
event: WindowEvent,
rsc: &mut DefaultRsc<Self>,
render: &mut UiRenderState,
) {
}
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
Default::default() Default::default()
} }
} }
pub struct DefaultRsc<State: 'static> { pub type DesktopRsc<State> = AppRsc<State>;
pub ui: UiData,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> DefaultRsc<State> { pub struct DesktopApp<State: DesktopAppState> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) { rsc: DesktopRsc<State>,
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, state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>, task_recv: TaskMsgReceiver<DesktopRsc<State>>,
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DesktopAppState> AppState for DesktopApp<State> {
type Event = State::Event; type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
@@ -224,34 +117,26 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
NullDeactivationHandler, NullDeactivationHandler,
); );
window.set_visible(true); window.set_visible(true);
let default_state = DefaultUiState::new(window, access_adapter); let desktop_state = DesktopUiState::new(window, access_adapter);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); let (mut rsc, task_recv) = AppRsc::new(desktop_state.window.clone());
// Both copies of the density, set before the first widget is // Set before building widgets so the first text shape uses the right density.
// built so text shapes at the right size on the opening frame -- let scale = content_scale(desktop_state.window.as_ref());
// the same pair `android::view::new_peer` sets from rsc.ui.set_density(scale);
// `content_scale`. See `iris_core::TextData::density` for why the let state = State::new(desktop_state, &mut rsc, proxy);
// shaper keeps its own.
let scale = content_scale(default_state.window.as_ref());
rsc.ui.text.density = scale;
let state = State::new(default_state, &mut rsc, proxy);
let mut render = UiRenderState::new();
render.set_density(scale);
Self { Self {
rsc, rsc,
state, state,
render,
task_recv, task_recv,
} }
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render); self.state.event(event, &mut self.rsc);
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self { let Self {
rsc, rsc,
render,
state, state,
task_recv, task_recv,
} = self; } = self;
@@ -260,7 +145,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
update(state, rsc); update(state, rsc);
} }
let ui_state = state.default_state_mut(); let ui_state = state.desktop_state_mut();
// Required by `accesskit_winit` on every window event, not just the // Required by `accesskit_winit` on every window event, not just the
// ones this backend otherwise cares about -- some platform adapters // ones this backend otherwise cares about -- some platform adapters
// rely on it to notice activation (a screen reader turning on). // rely on it to notice activation (a screen reader turning on).
@@ -274,14 +159,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed { if input_changed {
// The winit half of `iris::input` (`sense::log_input_event`'s // Winit delivers one sample at a time, so there is no history batch.
// own doc): no batching here, so `historical` is always empty
// -- winit hands one `WindowEvent` per pointer sample, unlike
// Android's `MotionEvent`. The action is read back off the
// buttons `Input::event` just updated, the same test
// `GestureOutcome`'s callers already use to tell a press from a
// release. Computed only when tracing is on, same reasoning as
// `log_input_event` itself gating on it.
if crate::diagnostics::trace_enabled() { if crate::diagnostics::trace_enabled() {
let action = if cursor_state.buttons.left.is_start() { let action = if cursor_state.buttons.left.is_start() {
"down" "down"
@@ -290,7 +168,11 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
} else { } else {
"move" "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( crate::sense::log_input_event(
action, action,
cursor_state.pos.x, cursor_state.pos.x,
@@ -300,9 +182,12 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
); );
} }
let window_size = ui_state.window_size(); 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(); let ui_state = state.desktop_state_mut();
if old != ui_state.focus if old != ui_state.focus
&& let Some(old) = old && let Some(old) = old
{ {
@@ -311,36 +196,32 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
// Before the draw, so this frame shows this instant's // Advance animations before drawing and keep requesting frames while active.
// position (`UiData::tick_animations`' own doc), and the
// window is asked for another frame while anything is
// still moving -- the winit half of what
// `IrisViewPeer::render`'s `post_frame_callback` does on
// Android. Nothing else in iris moves without an input
// event.
let frame_start = std::time::Instant::now(); let frame_start = std::time::Instant::now();
let animating = rsc.ui_mut().tick_animations(frame_start); let animating = rsc.ui_mut().tick_animations(frame_start);
let ui_state = state.default_state_mut(); let ui_state = state.desktop_state_mut();
if animating { if animating {
ui_state.window.request_redraw(); ui_state.window.request_redraw();
} }
render.update(&ui_state.root, rsc); rsc.draw(&ui_state.root);
ui_state.renderer.update(&mut rsc.ui, render); ui_state.renderer.update(&mut rsc.ui);
let mut parts = ui_state.renderer.draw(); let mut parts = ui_state.renderer.draw();
parts.total = frame_start.elapsed(); parts.total = frame_start.elapsed();
crate::diagnostics::log_frame(render, frame_start, parts, animating); let render_state = rsc.ui.render_state();
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), render, rsc) { 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); ui_state.access_adapter.update_if_active(|| tree_update);
} }
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
render.resize((size.width, size.height)); rsc.ui.resize((size.width, size.height));
ui_state.renderer.resize(size) ui_state.renderer.resize(size)
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
let scale = content_scale(ui_state.window.as_ref()); let scale = content_scale(ui_state.window.as_ref());
rsc.ui.text.density = scale; rsc.ui.set_density(scale);
render.set_density(scale);
ui_state.window.request_redraw(); ui_state.window.request_redraw();
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
@@ -422,29 +303,19 @@ 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(); let ui_state = self.state.desktop_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.renderer.window().request_redraw();
} }
ui_state.input.end_frame(); ui_state.input.end_frame();
} }
fn exit(&mut self) { fn exit(&mut self) {
self.state.exit(&mut self.rsc, &mut self.render); self.state.exit(&mut self.rsc);
}
}
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)
} }
} }
@@ -1,7 +1,7 @@
use crate::platform::OpenUrl; use crate::platform::OpenUrl;
use crate::prelude::HasDefaultUiState; use crate::prelude::HasDesktopUiState;
impl<T: HasDefaultUiState> OpenUrl for T { impl<T: HasDesktopUiState> OpenUrl for T {
fn open_url(&mut self, url: &str) { fn open_url(&mut self, url: &str) {
let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") { let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") {
("open", &[]) ("open", &[])
@@ -1,5 +1,5 @@
use crate::task::RequestRedraw; 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 pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
@@ -26,8 +26,8 @@ pub struct UiRenderer {
} }
impl UiRenderer { impl UiRenderer {
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) { pub fn update(&mut self, ui: &mut Ui) {
self.ui.update(&self.device, &self.queue, ui, render); self.ui.update(&self.device, &self.queue, ui);
} }
/// The two waits, so a desktop frame divides up the same way an /// The two waits, so a desktop frame divides up the same way an
@@ -213,7 +213,7 @@ impl UiRenderer {
// that function's doc comment). // that function's doc comment).
// Physical size, the same units the swapchain, `WindowEvent:: // Physical size, the same units the swapchain, `WindowEvent::
// Resized`, the pointer and the widget tree all use -- see // Resized`, the pointer and the widget tree all use -- see
// `default::content_scale` for why this backend stopped dividing // `desktop::content_scale` for why this backend stopped dividing
// into a separate logical space, and what disagreed while it did. // into a separate logical space, and what disagreed while it did.
let physical_size = Vec2::new(size.width as f32, size.height as f32); let physical_size = Vec2::new(size.width as f32, size.height as f32);
let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size) let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size)
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn trace_enabled() -> bool {
/// One `iris::frame` line, called once per frame from each backend's own /// One `iris::frame` line, called once per frame from each backend's own
/// frame function -- `android::view::IrisViewPeer::render`, /// frame function -- `android::view::IrisViewPeer::render`,
/// `default::DefaultApp::window_event`'s `RedrawRequested` arm, and /// `desktop::DesktopApp::window_event`'s `RedrawRequested` arm, and
/// `harness::Harness::frame` -- after the draw (or, on the harness, where a /// `harness::Harness::frame` -- after the draw (or, on the harness, where a
/// draw would be; `draw` is `Duration::ZERO` there since nothing is /// draw would be; `draw` is `Duration::ZERO` there since nothing is
/// actually submitted to a GPU). /// actually submitted to a GPU).
+10 -88
View File
@@ -1,5 +1,4 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -164,82 +163,12 @@ impl OpenUrl for HarnessState {
} }
} }
/// The harness's `Rsc` -- identical in substance to `DefaultRsc`/ pub type HarnessRsc = AppRsc<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 events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<HarnessState>,
}
impl UiRsc for HarnessRsc {
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 HasState for HarnessRsc {
type State = HarnessState;
}
impl HasEvents for HarnessRsc {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl HasTasks for HarnessRsc {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl HasWidgetState for HarnessRsc {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
impl<I: RscIdx<HarnessRsc>> std::ops::Index<I> for HarnessRsc {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<I: RscIdx<HarnessRsc>> std::ops::IndexMut<I> for HarnessRsc {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
/// A screen running with no window: the widget tree, the frame loop and /// A screen running with no window: the widget tree, the frame loop and
/// the pointer, all advanced by the caller. See the module doc. /// the pointer, all advanced by the caller. See the module doc.
pub struct Harness { pub struct Harness {
pub rsc: HarnessRsc, pub rsc: HarnessRsc,
pub render: UiRenderState,
pub state: HarnessState, pub state: HarnessState,
task_recv: TaskMsgReceiver<HarnessRsc>, task_recv: TaskMsgReceiver<HarnessRsc>,
redraws: Arc<RedrawCounter>, redraws: Arc<RedrawCounter>,
@@ -256,21 +185,11 @@ impl Harness {
/// `PHONE_SCALE`. /// `PHONE_SCALE`.
pub fn new(size: Vec2, density: f32) -> Self { pub fn new(size: Vec2, density: f32) -> Self {
let redraws = Arc::new(RedrawCounter::default()); let redraws = Arc::new(RedrawCounter::default());
let (tasks, task_recv) = Tasks::init(redraws.clone()); let (mut rsc, task_recv) = AppRsc::new(redraws.clone());
let mut rsc = HarnessRsc { rsc.ui.set_density(density);
ui: UiData::default(), rsc.ui.resize(size);
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);
Self { Self {
rsc, rsc,
render,
state: HarnessState::new(), state: HarnessState::new(),
task_recv, task_recv,
redraws, redraws,
@@ -302,15 +221,16 @@ impl Harness {
let now = self.at(t_ms); let now = self.at(t_ms);
let at = Instant::now(); let at = Instant::now();
let animating = self.rsc.ui.tick_animations(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 // No GPU here, so there is nothing to acquire and nothing to
// submit: the frame is all `build`, which is honest rather than // submit: the frame is all `build`, which is honest rather than
// zero-filled (`FrameParts::whole`). `layout`/`redraw`/ // zero-filled (`FrameParts::whole`). `layout`/`redraw`/
// `primitives` are still real, because `render.update` just ran; // `primitives` are still real, because `render.update` just ran;
// see `iris::diagnostics::log_frame`'s own doc for why this reads // see `iris::diagnostics::log_frame`'s own doc for why this reads
// those back rather than timing anything itself. // those back rather than timing anything itself.
let render_state = self.rsc.ui.render_state();
crate::diagnostics::log_frame( crate::diagnostics::log_frame(
&self.render, &render_state.get(),
now, now,
FrameParts::whole(at.elapsed()), FrameParts::whole(at.elapsed()),
animating, animating,
@@ -344,7 +264,9 @@ impl Harness {
} }
crate::sense::log_input_event(action.word(), pos.x, pos.y, t_ms, &[]); crate::sense::log_input_event(action.word(), pos.x, pos.y, t_ms, &[]);
let cursor = self.cursor.clone(); 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); .run_sensors(&mut self.rsc, &mut self.state, cursor, self.size);
self.frame(t_ms); self.frame(t_ms);
self.cursor.end_frame(); self.cursor.end_frame();
+43 -83
View File
@@ -2,14 +2,14 @@ use crate::prelude::*;
use std::{cell::Cell, cell::RefCell, rc::Rc}; use std::{cell::Cell, cell::RefCell, rc::Rc};
pub(crate) struct TestRsc { pub(crate) struct TestRsc {
pub(crate) ui: UiData, pub(crate) ui: Ui,
} }
impl UiRsc for TestRsc { impl UiRsc for TestRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui &mut self.ui
} }
} }
@@ -87,9 +87,7 @@ impl Widget for TracedParent {
#[test] #[test]
fn a_widget_retains_its_entry_layer_not_its_child_cursor() { fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)); let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
let stack = rsc.ui.widgets.add_strong(Stack { let stack = rsc.ui.widgets.add_strong(Stack {
@@ -117,7 +115,7 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None; rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
assert_eq!( assert_eq!(
render.take_counters().0, render.take_counters().draws,
1, 1,
"redrawing the parent should retain the unchanged Stack subtree" "redrawing the parent should retain the unchanged Stack subtree"
); );
@@ -125,9 +123,7 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
#[test] #[test]
fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() { fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let trace = Rc::new(RefCell::new(Vec::new())); let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf { let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0, height: 20.0,
@@ -155,9 +151,7 @@ fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() {
#[test] #[test]
fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() { fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let trace = Rc::new(RefCell::new(Vec::new())); let trace = Rc::new(RefCell::new(Vec::new()));
let child = rsc.ui.widgets.add_strong(TracedLeaf { let child = rsc.ui.widgets.add_strong(TracedLeaf {
height: 20.0, height: 20.0,
@@ -183,9 +177,7 @@ fn a_parent_that_ignores_child_size_is_not_invalidated_with_it() {
#[test] #[test]
fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() { fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let changed_draws = Rc::new(Cell::new(0)); let changed_draws = Rc::new(Cell::new(0));
let sibling_draws = Rc::new(Cell::new(0)); let sibling_draws = Rc::new(Cell::new(0));
let changed = rsc.ui.widgets.add_strong(CountedLeaf { 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] #[test]
fn a_child_coordinate_offset_moves_only_the_child_subtree() { fn a_child_coordinate_offset_moves_only_the_child_subtree() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let child = rsc.ui.widgets.add_strong(FixedRect(40.0)); let child = rsc.ui.widgets.add_strong(FixedRect(40.0));
let child_weak = child.weak(); let child_weak = child.weak();
let parent = rsc.ui.widgets.add_strong(ChildOffset { let parent = rsc.ui.widgets.add_strong(ChildOffset {
@@ -260,7 +250,12 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0; rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0;
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None; rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
let parent_after = render.window_region(&parent_weak, &rsc).unwrap(); let parent_after = render.window_region(&parent_weak, &rsc).unwrap();
let child_after = render.window_region(&child_weak, &rsc).unwrap(); let child_after = render.window_region(&child_weak, &rsc).unwrap();
@@ -274,9 +269,7 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
#[test] #[test]
fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() { fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let first = rsc.ui.widgets.add_strong(FixedRect(40.0)); 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(Rect::new(PaintId::WHITE));
let fill = rsc.ui.widgets.add_strong(Sized { let fill = rsc.ui.widgets.add_strong(Sized {
@@ -295,7 +288,7 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((200.0, 300.0)); render.resize((200.0, 300.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 5); assert_eq!(draws, 5);
assert_eq!(moves, 1); assert_eq!(moves, 1);
@@ -330,9 +323,7 @@ fn scrolled_rects(
#[test] #[test]
fn an_unchanged_frame_draws_and_rewrites_nothing() { fn an_unchanged_frame_draws_and_rewrites_nothing() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (_scroll, root, _rects) = scrolled_rects(&mut rsc, 500); let (_scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((800.0, 20000.0)); render.resize((800.0, 20000.0));
@@ -342,15 +333,18 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() {
render.take_counters(); // discard the first, real draws render.take_counters(); // discard the first, real draws
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
assert_eq!((draws, rewrites, moves), (0, 0, 0)); assert_eq!((draws, rewrites, moves), (0, 0, 0));
} }
#[test] #[test]
fn scrolling_moves_in_o1_without_a_redraw() { fn scrolling_moves_in_o1_without_a_redraw() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, _rects) = scrolled_rects(&mut rsc, 500); let (scroll, root, _rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((800.0, 600.0)); render.resize((800.0, 600.0));
@@ -362,7 +356,7 @@ fn scrolling_moves_in_o1_without_a_redraw() {
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 1, "only Scroll itself should redraw"); assert_eq!(draws, 1, "only Scroll itself should redraw");
assert_eq!(moves, 1, "the scrolled subtree should move in one write"); assert_eq!(moves, 1, "the scrolled subtree should move in one write");
@@ -370,9 +364,7 @@ fn scrolling_moves_in_o1_without_a_redraw() {
#[test] #[test]
fn hit_testing_follows_a_scrolled_widget() { fn hit_testing_follows_a_scrolled_widget() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, rects) = scrolled_rects(&mut rsc, 500); let (scroll, root, rects) = scrolled_rects(&mut rsc, 500);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((800.0, 600.0)); render.resize((800.0, 600.0));
@@ -398,9 +390,7 @@ fn hit_testing_follows_a_scrolled_widget() {
#[test] #[test]
fn redrawing_a_masked_widget_does_not_nest_its_own_mask() { fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8); let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked { let masked = rsc.ui.widgets.add_strong(Masked {
shape: None, shape: None,
@@ -424,9 +414,7 @@ fn redrawing_a_masked_widget_does_not_nest_its_own_mask() {
#[test] #[test]
fn a_mask_stays_put_while_its_scrolled_content_moves() { fn a_mask_stays_put_while_its_scrolled_content_moves() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 500); let (scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 500);
let masked = rsc.ui.widgets.add_strong(Masked { let masked = rsc.ui.widgets.add_strong(Masked {
shape: None, shape: None,
@@ -473,9 +461,7 @@ fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget)
#[test] #[test]
fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() { fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (field, root) = composer_like_tree(&mut rsc); let (field, root) = composer_like_tree(&mut rsc);
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
@@ -515,9 +501,7 @@ fn composing_text_after_a_keyboard_resize_lands_in_the_bars_own_region() {
#[test] #[test]
fn a_scroll_measures_the_box_it_was_offered_not_the_window() { fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized { let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(), inner: rect.any(),
@@ -565,9 +549,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
#[test] #[test]
fn a_panned_widgets_own_hit_box_moves_exactly_once() { fn a_panned_widgets_own_hit_box_moves_exactly_once() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized { let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(), inner: rect.any(),
@@ -601,9 +583,7 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
#[test] #[test]
fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() { fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8); let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let masked = rsc.ui.widgets.add_strong(Masked { let masked = rsc.ui.widgets.add_strong(Masked {
shape: None, shape: None,
@@ -648,9 +628,7 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
#[test] #[test]
fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() { fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let tall = rsc.ui.widgets.add_strong(Sized { let tall = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(), inner: rect.any(),
@@ -690,9 +668,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
#[test] #[test]
fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at() { fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let spacer = rsc.ui.widgets.add_strong(Sized { let spacer = rsc.ui.widgets.add_strong(Sized {
inner: top.any(), inner: top.any(),
@@ -757,9 +733,7 @@ impl Widget for MoveThenPlace {
#[test] #[test]
fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement() { fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child = rsc.ui.widgets.add_strong(Sized { let child = rsc.ui.widgets.add_strong(Sized {
inner: rect.any(), inner: rect.any(),
@@ -839,9 +813,7 @@ fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u3
#[test] #[test]
fn a_masked_child_is_clipped_by_its_container_s_own_corner() { fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (render, mask, _child, slot) = rounded_container(&mut rsc); let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc); let corners = render.primitive_corners(slot, &rsc);
let radius = render let radius = render
@@ -879,9 +851,7 @@ fn a_masked_child_is_clipped_by_its_container_s_own_corner() {
#[test] #[test]
fn a_mask_s_shape_decides_what_can_be_pressed() { fn a_mask_s_shape_decides_what_can_be_pressed() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (render, mask, _child, slot) = rounded_container(&mut rsc); let (render, mask, _child, slot) = rounded_container(&mut rsc);
let corners = render.primitive_corners(slot, &rsc); let corners = render.primitive_corners(slot, &rsc);
@@ -907,9 +877,7 @@ fn a_mask_s_shape_decides_what_can_be_pressed() {
#[test] #[test]
fn nested_masks_multiply_their_coverage() { fn nested_masks_multiply_their_coverage() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let child_id = child.id(); let child_id = child.id();
@@ -970,9 +938,7 @@ fn nested_masks_multiply_their_coverage() {
#[test] #[test]
fn a_plain_mask_still_clips_to_a_square_box() { fn a_plain_mask_still_clips_to_a_square_box() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8); let (_scroll, inner_root, _rects) = scrolled_rects(&mut rsc, 8);
let root = rsc let root = rsc
.ui .ui
@@ -1002,9 +968,7 @@ fn a_plain_mask_still_clips_to_a_square_box() {
#[test] #[test]
fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() { 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)] { for (name, pin, want) in [("read", Pin::Start, 0.0), ("written", Pin::End, 4900.0)] {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any(); let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
let tall = rsc.ui.widgets.add_strong(Sized { let tall = rsc.ui.widgets.add_strong(Sized {
inner: fill, inner: fill,
@@ -1037,9 +1001,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
const PAD: f32 = 4.0; const PAD: f32 = 4.0;
const ROW: f32 = 20.0; const ROW: f32 = 20.0;
const HEADER: f32 = 30.0; const HEADER: f32 = 30.0;
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any(); let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any();
let header_id = header_fill.id(); let header_id = header_fill.id();
let header = rsc.ui.widgets.add_strong(Sized { let header = rsc.ui.widgets.add_strong(Sized {
@@ -1120,9 +1082,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
const SECOND: f32 = 70.0; const SECOND: f32 = 70.0;
const GAP: f32 = 8.0; const GAP: f32 = 8.0;
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
let first_id = first.id(); let first_id = first.id();
let first = rsc.ui.widgets.add_strong(Sized { let first = rsc.ui.widgets.add_strong(Sized {
+5 -3
View File
@@ -1,6 +1,6 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
// Only `default::DefaultAppState::Event`'s default uses this; unused (and // Only `desktop::DesktopAppState::Event`'s default uses this; unused (and
// warned about) on the android target, which has no such default. // warned about) on the android target, which has no such default.
#![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))] #![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))]
#![feature(unsize)] #![feature(unsize)]
@@ -10,7 +10,7 @@
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub mod android; pub mod android;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub mod default; pub mod desktop;
pub mod attr; pub mod attr;
pub mod diagnostics; pub mod diagnostics;
@@ -18,6 +18,7 @@ pub mod event;
pub mod harness; pub mod harness;
pub mod overlay; pub mod overlay;
pub mod platform; pub mod platform;
pub mod runtime;
pub mod sense; pub mod sense;
pub mod state; pub mod state;
pub mod task; pub mod task;
@@ -38,7 +39,7 @@ pub mod prelude {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub use android::*; pub use android::*;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub use default::*; pub use desktop::*;
pub use attr::*; pub use attr::*;
pub use event::*; pub use event::*;
@@ -46,6 +47,7 @@ pub mod prelude {
pub use iris_macro::*; pub use iris_macro::*;
pub use overlay::*; pub use overlay::*;
pub use platform::*; pub use platform::*;
pub use runtime::*;
pub use sense::*; pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
+6 -3
View File
@@ -402,10 +402,11 @@ pub trait OverlayRscExt: HasEvents {
where where
W: WidgetLike<Self, Tag>, W: WidgetLike<Self, Tag>,
{ {
let render_handle = self.ui().render_state();
let (target, path) = self let (target, path) = self
.events() .events()
.controllers .controllers
.path_to::<StackableOverlayController>(origin.id())?; .path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self); clear_singles(&path, self);
let content = modal.add(self); let content = modal.add(self);
@@ -447,10 +448,11 @@ pub trait OverlayRscExt: HasEvents {
{ {
match options.kind { match options.kind {
OverlayKind::Single => { OverlayKind::Single => {
let render_handle = self.ui().render_state();
let target = self let target = self
.events() .events()
.controllers .controllers
.nearest_id::<SingleOverlayController>(origin.id())?; .nearest_id::<SingleOverlayController>(origin.id(), &render_handle.get())?;
let overlay = overlay.add_strong(self).any(); let overlay = overlay.add_strong(self).any();
self.with_controller(target, |single: &mut SingleOverlayController, rsc| { self.with_controller(target, |single: &mut SingleOverlayController, rsc| {
single.open( single.open(
@@ -463,10 +465,11 @@ pub trait OverlayRscExt: HasEvents {
}) })
} }
OverlayKind::Stackable => { OverlayKind::Stackable => {
let render_handle = self.ui().render_state();
let (target, path) = self let (target, path) = self
.events() .events()
.controllers .controllers
.path_to::<StackableOverlayController>(origin.id())?; .path_to::<StackableOverlayController>(origin.id(), &render_handle.get())?;
clear_singles(&path, self); clear_singles(&path, self);
let overlay = overlay.add_strong(self).any(); let overlay = overlay.add_strong(self).any();
open_stackable_at( open_stackable_at(
+98
View File
@@ -0,0 +1,98 @@
use crate::prelude::*;
use std::sync::Arc;
/// Resources shared by every Iris host.
pub struct AppRsc<State: 'static> {
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: std::marker::PhantomData<State>,
}
impl<State> AppRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw);
(
Self {
ui: Ui::default(),
events: EventManager::default(),
tasks,
state: WidgetState::default(),
_state: std::marker::PhantomData,
},
receiver,
)
}
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 AppRsc<State> {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&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> HasState for AppRsc<State> {
type State = State;
}
impl<State> HasEvents for AppRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State> HasTasks for AppRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State> HasWidgetState for AppRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
impl<State, I: RscIdx<AppRsc<State>>> std::ops::Index<I> for AppRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State, I: RscIdx<AppRsc<State>>> std::ops::IndexMut<I> for AppRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
+58 -11
View File
@@ -2,15 +2,15 @@ use crate::prelude::*;
use std::{cell::Cell, rc::Rc, time::Instant}; use std::{cell::Cell, rc::Rc, time::Instant};
struct SenseRsc { struct SenseRsc {
ui: UiData, ui: Ui,
events: EventManager<SenseRsc>, events: EventManager<SenseRsc>,
} }
impl UiRsc for SenseRsc { impl UiRsc for SenseRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui &mut self.ui
} }
fn on_draw(&mut self, active: &ActiveData) { fn on_draw(&mut self, active: &ActiveData) {
@@ -28,6 +28,53 @@ impl HasState for SenseRsc {
type State = (); 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 { impl HasEvents for SenseRsc {
fn events(&self) -> &EventManager<Self> { fn events(&self) -> &EventManager<Self> {
&self.events &self.events
@@ -50,7 +97,7 @@ fn cursor_at(pos: Vec2) -> CursorState {
#[test] #[test]
fn a_button_over_a_list_scrolls_the_list_and_still_clicks() { fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
@@ -121,7 +168,7 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
#[test] #[test]
fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() { fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
@@ -180,7 +227,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
#[test] #[test]
fn capturing_one_widget_starves_every_other_widget_of_events() { fn capturing_one_widget_starves_every_other_widget_of_events() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
@@ -225,7 +272,7 @@ fn capturing_one_widget_starves_every_other_widget_of_events() {
#[test] #[test]
fn a_finger_drag_over_a_scroll_area_pans_it() { fn a_finger_drag_over_a_scroll_area_pans_it() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
@@ -316,7 +363,7 @@ fn the_clock_orders_samples_across_events() {
#[test] #[test]
fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() { fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let scroll_strong = rect(PaintId::WHITE) let scroll_strong = rect(PaintId::WHITE)
@@ -370,7 +417,7 @@ fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
#[test] #[test]
fn taking_the_pointer_cancels_everyone_else_tracking_the_press() { fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
@@ -469,7 +516,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
), ),
] { ] {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let seen = Rc::new(Cell::new(None)); let seen = Rc::new(Cell::new(None));
@@ -526,7 +573,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
#[test] #[test]
fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() { fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
let mut rsc = SenseRsc { let mut rsc = SenseRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
+30 -77
View File
@@ -833,14 +833,14 @@ mod tests {
} }
struct TestRsc { struct TestRsc {
ui: UiData, ui: Ui,
} }
impl UiRsc for TestRsc { impl UiRsc for TestRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui &mut self.ui
} }
} }
@@ -882,9 +882,7 @@ mod tests {
#[test] #[test]
fn a_list_shorter_than_the_viewport_is_drawn_whole_and_stays_at_the_bottom() { fn a_list_shorter_than_the_viewport_is_drawn_whole_and_stays_at_the_bottom() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -912,9 +910,7 @@ mod tests {
#[test] #[test]
fn a_dir_up_span_grows_upward_from_item_zero() { fn a_dir_up_span_grows_upward_from_item_zero() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::UP, Pin::End); let mut list = LazySpan::new(Dir::UP, Pin::End);
let rows = push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0); let rows = push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -949,9 +945,7 @@ mod tests {
#[test] #[test]
fn a_delta_moves_both_directions_the_same_way_on_screen() { fn a_delta_moves_both_directions_the_same_way_on_screen() {
let moved_by = |dir: Dir| { let moved_by = |dir: Dir| {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(dir, Pin::End); let mut list = LazySpan::new(dir, Pin::End);
let keys: Vec<RowKey> = (0..10).collect(); let keys: Vec<RowKey> = (0..10).collect();
let rows = push_rows(&mut rsc, &mut list, &keys, 20.0); let rows = push_rows(&mut rsc, &mut list, &keys, 20.0);
@@ -1003,9 +997,7 @@ mod tests {
#[test] #[test]
fn a_reversed_span_hit_tests_in_screen_space() { fn a_reversed_span_hit_tests_in_screen_space() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::UP, Pin::End); let mut list = LazySpan::new(Dir::UP, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1030,9 +1022,7 @@ mod tests {
#[test] #[test]
fn bottom_anchored_by_default() { fn bottom_anchored_by_default() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); 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() { fn a_row_that_changes_height_draws_its_background_at_the_new_height_immediately() {
for key_to_change in 0..5u64 { for key_to_change in 0..5u64 {
for new_height in [50.0f32, 8.0] { for new_height in [50.0f32, 8.0] {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut rows = Vec::new(); let mut rows = Vec::new();
for key in 0..5u64 { for key in 0..5u64 {
@@ -1118,9 +1106,7 @@ mod tests {
#[test] #[test]
fn a_fill_shaped_background_is_not_left_oversized() { fn a_fill_shaped_background_is_not_left_oversized() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut bg_ids = Vec::new(); let mut bg_ids = Vec::new();
for key in 0..5u64 { for key in 0..5u64 {
@@ -1148,9 +1134,7 @@ mod tests {
#[test] #[test]
fn insert_above_anchor_is_o1_and_does_not_move_visible_rows() { fn insert_above_anchor_is_o1_and_does_not_move_visible_rows() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[10, 11, 12], 20.0); push_rows(&mut rsc, &mut list, &[10, 11, 12], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1171,7 +1155,7 @@ mod tests {
.push_front(LazyItem::new(key, w)); .push_front(LazyItem::new(key, w));
} }
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, _moves, _shapes) = render.take_counters(); let draws = render.take_counters().draws;
let extents_after = rsc.ui.widgets.get(&list_weak).unwrap().extents.clone(); let extents_after = rsc.ui.widgets.get(&list_weak).unwrap().extents.clone();
for key in [11u64, 12] { for key in [11u64, 12] {
@@ -1188,9 +1172,7 @@ mod tests {
#[test] #[test]
fn expanding_a_row_holds_the_edge_nearest_the_tap() { fn expanding_a_row_holds_the_edge_nearest_the_tap() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); 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 rows = push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1234,9 +1216,7 @@ mod tests {
#[test] #[test]
fn expanding_a_row_holds_the_bottom_edge_when_tap_is_lower() { fn expanding_a_row_holds_the_bottom_edge_when_tap_is_lower() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); 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 rows = push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1270,9 +1250,7 @@ mod tests {
#[test] #[test]
fn moves_stay_o1_across_list_size() { fn moves_stay_o1_across_list_size() {
for &n in &[20usize, 200, 2000] { for &n in &[20usize, 200, 2000] {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let keys: Vec<RowKey> = (0..n as u64).collect(); let keys: Vec<RowKey> = (0..n as u64).collect();
push_rows(&mut rsc, &mut list, &keys, 20.0); push_rows(&mut rsc, &mut list, &keys, 20.0);
@@ -1291,7 +1269,8 @@ mod tests {
rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(1.0); rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(1.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let counters = render.take_counters();
let (draws, moves) = (counters.draws, counters.moves);
assert_eq!(draws, 1, "n={n}: only the list should really draw"); assert_eq!(draws, 1, "n={n}: only the list should really draw");
assert_eq!(moves, 1, "n={n}: the whole retained run should move once"); assert_eq!(moves, 1, "n={n}: the whole retained run should move once");
@@ -1300,9 +1279,7 @@ mod tests {
#[test] #[test]
fn a_large_accumulated_offset_rebases_without_moving_the_content() { fn a_large_accumulated_offset_rebases_without_moving_the_content() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let keys: Vec<RowKey> = (0..100).collect(); let keys: Vec<RowKey> = (0..100).collect();
let rows = push_rows(&mut rsc, &mut list, &keys, 1_000.0); let rows = push_rows(&mut rsc, &mut list, &keys, 1_000.0);
@@ -1348,9 +1325,7 @@ mod tests {
#[test] #[test]
fn replacing_the_last_row_stays_pinned_to_the_bottom() { fn replacing_the_last_row_stays_pinned_to_the_bottom() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1392,9 +1367,7 @@ mod tests {
#[test] #[test]
fn replace_back_forgets_the_evicted_keys_own_height() { fn replace_back_forgets_the_evicted_keys_own_height() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1429,9 +1402,7 @@ mod tests {
#[test] #[test]
fn replacing_the_last_row_out_of_view_does_not_move_visible_rows() { fn replacing_the_last_row_out_of_view_does_not_move_visible_rows() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0); push_rows(&mut rsc, &mut list, &[0, 1, 2, 3, 4], 20.0);
let (list_weak, root) = add_list(&mut rsc, list); let (list_weak, root) = add_list(&mut rsc, list);
@@ -1474,9 +1445,7 @@ mod tests {
#[test] #[test]
fn replacing_the_last_row_many_times_does_not_leak_primitives() { fn replacing_the_last_row_many_times_does_not_leak_primitives() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
for key in 0..5u64 { for key in 0..5u64 {
let (_bg_id, row) = background_styled_row(&mut rsc, 20.0); let (_bg_id, row) = background_styled_row(&mut rsc, 20.0);
@@ -1511,9 +1480,7 @@ mod tests {
#[test] #[test]
fn an_ancestor_redrawing_a_dirty_row_leaves_no_stale_copy() { fn an_ancestor_redrawing_a_dirty_row_leaves_no_stale_copy() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
let mut rows = Vec::new(); let mut rows = Vec::new();
for key in 0..5u64 { for key in 0..5u64 {
@@ -1574,9 +1541,7 @@ mod tests {
#[test] #[test]
fn a_fling_moves_the_list_and_then_settles() { fn a_fling_moves_the_list_and_then_settles() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
@@ -1602,9 +1567,7 @@ mod tests {
#[test] #[test]
fn a_registered_fling_is_driven_by_tick_animations_and_then_unregisters() { fn a_registered_fling_is_driven_by_tick_animations_and_then_unregisters() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap()); let before = scroll_position(rsc.ui.widgets.get(&list_weak).unwrap());
@@ -1637,9 +1600,7 @@ mod tests {
#[test] #[test]
fn a_negative_delta_moves_toward_the_end() { fn a_negative_delta_moves_toward_the_end() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(2000.0); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(2000.0);
@@ -1661,9 +1622,7 @@ mod tests {
#[test] #[test]
fn amt_counts_only_what_the_child_could_take() { fn amt_counts_only_what_the_child_could_take() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100.0); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
@@ -1684,9 +1643,7 @@ mod tests {
#[test] #[test]
fn a_fling_stops_at_the_first_row() { fn a_fling_stops_at_the_first_row() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().fling(50_000.0); rsc.ui.widgets.get_mut(&scroll).unwrap().fling(50_000.0);
@@ -1710,9 +1667,7 @@ mod tests {
#[test] #[test]
fn scrolling_past_the_start_lands_on_it_in_the_same_frame() { fn scrolling_past_the_start_lands_on_it_in_the_same_frame() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (scroll, root, mut render) = build_flingable_list(&mut rsc); let (scroll, root, mut render) = build_flingable_list(&mut rsc);
let list_weak = scroll; let list_weak = scroll;
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100_000.0); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(100_000.0);
@@ -1734,9 +1689,7 @@ mod tests {
#[test] #[test]
fn anchor_position_display_reports_slot_and_offset() { fn anchor_position_display_reports_slot_and_offset() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let (list_weak, root, mut render) = build_flingable_list(&mut rsc); let (list_weak, root, mut render) = build_flingable_list(&mut rsc);
let _ = (&root, &mut render); let _ = (&root, &mut render);
let list_ref = rsc.ui.widgets.get(&list_weak).unwrap(); let list_ref = rsc.ui.widgets.get(&list_weak).unwrap();
+1 -4
View File
@@ -87,7 +87,6 @@ mod tests {
use super::*; use super::*;
use crate::layout_tests::TestRsc; use crate::layout_tests::TestRsc;
use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests}; use crate::sense::{CursorButton, DRAG_SLOP, PointerRequests};
use iris_core::UiData;
use std::time::Duration; use std::time::Duration;
fn area() -> (Fixture, WidgetId) { fn area() -> (Fixture, WidgetId) {
@@ -95,9 +94,7 @@ mod tests {
} }
fn area_on(axis: Axis) -> (Fixture, WidgetId) { fn area_on(axis: Axis) -> (Fixture, WidgetId) {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any(); let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
let id = fill.id(); let id = fill.id();
let long = Some(Len::abs(1000.0)); 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 { 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 UiRsc) -> TextEditCtx<'a> {
let ui = ui.ui_mut(); let ui: &mut UiData = ui.ui_mut();
TextEditCtx { TextEditCtx {
text: ui.widgets.get_mut(self).unwrap(), text: ui.widgets.get_mut(self).unwrap(),
data: &mut ui.text, data: &mut ui.text,
+2 -6
View File
@@ -222,9 +222,7 @@ mod tests {
use crate::prelude::*; use crate::prelude::*;
fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, StrongWidget) { fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, StrongWidget) {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let text = wtext(content).add_strong(&mut rsc); let text = wtext(content).add_strong(&mut rsc);
let id = text.weak(); let id = text.weak();
let root = text.any(); let root = text.any();
@@ -306,9 +304,7 @@ mod tests {
#[test] #[test]
fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() { fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() {
let mut rsc = TestRsc { let mut rsc = TestRsc { ui: Ui::default() };
ui: UiData::default(),
};
let root = wtext("hello there") let root = wtext("hello there")
.size(18) .size(18)
.color(PaintId::WHITE) .color(PaintId::WHITE)
+9 -9
View File
@@ -161,7 +161,7 @@ pub trait TextSelectable {
impl<I: IdLike<Widget = Text>> TextSelectable for I { impl<I: IdLike<Widget = Text>> TextSelectable for I {
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> { fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> {
let ui = ui.ui_mut(); let ui: &mut UiData = ui.ui_mut();
TextSelectionCtx { TextSelectionCtx {
view: &mut ui.widgets.get_mut(self).unwrap().view, view: &mut ui.widgets.get_mut(self).unwrap().view,
data: &mut ui.text, data: &mut ui.text,
@@ -250,7 +250,7 @@ impl SelectionController {
id: WidgetId, id: WidgetId,
f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T, f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T,
) -> Option<T> { ) -> Option<T> {
let ui = rsc.ui_mut(); let ui: &mut UiData = rsc.ui_mut();
let text = ui let text = ui
.widgets .widgets
.get_dyn_mut(id)? .get_dyn_mut(id)?
@@ -481,16 +481,16 @@ mod controller_tests {
use super::*; use super::*;
struct TestRsc { struct TestRsc {
ui: UiData, ui: Ui,
events: EventManager<TestRsc>, events: EventManager<TestRsc>,
} }
impl UiRsc for TestRsc { impl UiRsc for TestRsc {
fn ui(&self) -> &UiData { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
fn ui_mut(&mut self) -> &mut UiData { fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui &mut self.ui
} }
@@ -532,7 +532,7 @@ mod controller_tests {
StrongWidget, StrongWidget,
) { ) {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let first = wtext("first").add(&mut rsc); let first = wtext("first").add(&mut rsc);
@@ -571,7 +571,7 @@ mod controller_tests {
#[test] #[test]
fn a_widget_without_an_order_override_keeps_draw_order() { fn a_widget_without_an_order_override_keeps_draw_order() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let first = wtext("back").add(&mut rsc); let first = wtext("back").add(&mut rsc);
@@ -604,7 +604,7 @@ mod controller_tests {
#[test] #[test]
fn nearest_controller_prefers_the_inner_scope() { fn nearest_controller_prefers_the_inner_scope() {
let mut rsc = TestRsc { let mut rsc = TestRsc {
ui: UiData::default(), ui: Ui::default(),
events: EventManager::default(), events: EventManager::default(),
}; };
let leaf = wtext("leaf").add(&mut rsc); let leaf = wtext("leaf").add(&mut rsc);
@@ -624,7 +624,7 @@ mod controller_tests {
let found = rsc let found = rsc
.events() .events()
.controllers .controllers
.nearest_id::<SelectionController>(leaf.id()) .nearest_id::<SelectionController>(leaf.id(), &render)
.unwrap(); .unwrap();
assert_eq!(found.host(), inner.id()); 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] { fn render(gpu: &Gpu, renderer: &mut UiRenderNode, harness: &mut Harness) -> [[u8; 4]; 2] {
renderer.update( renderer.update(&gpu.device, &gpu.queue, &mut harness.rsc.ui);
&gpu.device,
&gpu.queue,
&mut harness.rsc.ui,
&mut harness.render,
);
let texture = gpu.device.create_texture(&wgpu::TextureDescriptor { let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Iris colour-space target"), label: Some("Iris colour-space target"),
size: wgpu::Extent3d { size: wgpu::Extent3d {