Clean up shared UI runtime state
This commit is contained in:
1 parent
f49b284b46
commit
a8602c1626
43 files changed
+642
-834
No files matched your search
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ 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};
|
||||||
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
|
||||||
@@ -50,7 +53,7 @@ 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);
|
||||||
@@ -123,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}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
@@ -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, &[]);
|
||||||
|
|||||||
@@ -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| {
|
||||||
|
|||||||
@@ -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
@@ -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>) {
|
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)
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
+66
-82
@@ -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>>,
|
||||||
@@ -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);
|
||||||
@@ -605,10 +604,7 @@ mod apply_tests {
|
|||||||
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");
|
||||||
@@ -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(¶graphs(crate::client::text_cap::MESSAGE_LINES * 4));
|
let capped = shapes_for_message(¶graphs(MESSAGE_LINES * 4));
|
||||||
let bigger = shapes_for_message(¶graphs(crate::client::text_cap::MESSAGE_LINES * 40));
|
let bigger = shapes_for_message(¶graphs(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"
|
||||||
@@ -695,7 +686,7 @@ mod apply_tests {
|
|||||||
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]
|
||||||
@@ -732,10 +722,7 @@ mod apply_tests {
|
|||||||
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);
|
||||||
@@ -767,7 +754,7 @@ mod apply_tests {
|
|||||||
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);
|
||||||
@@ -787,10 +774,7 @@ mod apply_tests {
|
|||||||
};
|
};
|
||||||
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
@@ -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)]
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -49,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);
|
||||||
|
|
||||||
@@ -119,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);
|
||||||
|
|
||||||
|
|||||||
@@ -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\
|
||||||
|
|||||||
@@ -68,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,
|
||||||
@@ -97,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;
|
||||||
@@ -155,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;
|
||||||
@@ -200,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;
|
||||||
@@ -270,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;
|
||||||
@@ -314,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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,7 +40,7 @@ impl DefaultAppState for State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DefaultRsc<Self>) {
|
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc<Self>) {
|
||||||
if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
|
if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,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
|
||||||
@@ -71,5 +71,5 @@ impl DefaultAppState for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
DefaultApp::<State>::run();
|
DesktopApp::<State>::run();
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,7 +24,7 @@ impl DefaultAppState for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, _: WindowEvent, rsc: &mut DefaultRsc<Self>) {
|
fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc<Self>) {
|
||||||
let render_state = rsc.ui.render_state();
|
let render_state = rsc.ui.render_state();
|
||||||
let new = format!(
|
let new = format!(
|
||||||
"widgets: {}\nactive: {}\nviews: {}",
|
"widgets: {}\nactive: {}\nviews: {}",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+85
-176
@@ -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,
|
||||||
@@ -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,66 +140,7 @@ impl WindowInsets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AndroidRsc<State: 'static> {
|
pub type AndroidRsc<State> = AppRsc<State>;
|
||||||
pub ui: Ui,
|
|
||||||
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) -> &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: '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
|
||||||
@@ -234,30 +149,10 @@ pub struct IrisViewPeer<State: AndroidAppState> {
|
|||||||
pub(super) rsc: AndroidRsc<State>,
|
pub(super) rsc: AndroidRsc<State>,
|
||||||
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() {
|
||||||
@@ -298,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);
|
||||||
@@ -322,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 {
|
||||||
@@ -514,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)
|
||||||
@@ -544,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);
|
||||||
@@ -616,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>,
|
||||||
@@ -646,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
|
|
||||||
// `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.rsc.ui.resize((width as f32, height as f32));
|
self.rsc.ui.resize((width as f32, height as f32));
|
||||||
|
|
||||||
// `AndroidRenderer::resize` only reconfigures the wgpu surface and
|
// 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} \
|
||||||
@@ -883,14 +799,7 @@ 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 {
|
|
||||||
ui: Default::default(),
|
|
||||||
events: Default::default(),
|
|
||||||
tasks,
|
|
||||||
state: Default::default(),
|
|
||||||
_state: PhantomData,
|
|
||||||
};
|
|
||||||
rsc.ui.set_density(content_scale);
|
rsc.ui.set_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);
|
||||||
|
|||||||
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,114 +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(&mut self, event: Self::Event, rsc: &mut DefaultRsc<Self>) {}
|
fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc<Self>) {}
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn exit(&mut self, rsc: &mut DefaultRsc<Self>) {}
|
fn exit(&mut self, rsc: &mut DesktopRsc<Self>) {}
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn window_event(&mut self, event: WindowEvent, rsc: &mut DefaultRsc<Self>) {}
|
fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc<Self>) {}
|
||||||
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: Ui,
|
|
||||||
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) -> &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: '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>,
|
|
||||||
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 {
|
||||||
@@ -211,16 +117,12 @@ 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
|
|
||||||
// `content_scale`. See `iris_core::TextData::density` for why the
|
|
||||||
// shaper keeps its own.
|
|
||||||
let scale = content_scale(default_state.window.as_ref());
|
|
||||||
rsc.ui.set_density(scale);
|
rsc.ui.set_density(scale);
|
||||||
let state = State::new(default_state, &mut rsc, proxy);
|
let state = State::new(desktop_state, &mut rsc, proxy);
|
||||||
Self {
|
Self {
|
||||||
rsc,
|
rsc,
|
||||||
state,
|
state,
|
||||||
@@ -243,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).
|
||||||
@@ -257,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"
|
||||||
@@ -292,7 +187,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
.get()
|
.get()
|
||||||
.run_sensors(rsc, state, cursor_state, window_size);
|
.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
|
||||||
{
|
{
|
||||||
@@ -301,16 +196,10 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -415,7 +304,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
state.window_event(event, rsc);
|
state.window_event(event, rsc);
|
||||||
let ui_state = self.state.default_state_mut();
|
let ui_state = self.state.desktop_state_mut();
|
||||||
let render_state = rsc.ui.render_state();
|
let render_state = rsc.ui.render_state();
|
||||||
if render_state
|
if render_state
|
||||||
.get()
|
.get()
|
||||||
@@ -430,17 +319,3 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
self.state.exit(&mut self.rsc);
|
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", &[])
|
||||||
@@ -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)
|
||||||
@@ -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).
|
||||||
|
|||||||
+2
-79
@@ -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,76 +163,7 @@ 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: Ui,
|
|
||||||
pub events: EventManager<Self>,
|
|
||||||
pub tasks: Tasks<Self>,
|
|
||||||
pub state: WidgetState,
|
|
||||||
_state: PhantomData<HarnessState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiRsc for HarnessRsc {
|
|
||||||
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 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.
|
||||||
@@ -255,14 +185,7 @@ 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 {
|
|
||||||
ui: Ui::default(),
|
|
||||||
events: EventManager::default(),
|
|
||||||
tasks,
|
|
||||||
state: WidgetState::default(),
|
|
||||||
_state: PhantomData,
|
|
||||||
};
|
|
||||||
rsc.ui.set_density(density);
|
rsc.ui.set_density(density);
|
||||||
rsc.ui.resize(size);
|
rsc.ui.resize(size);
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -115,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"
|
||||||
);
|
);
|
||||||
@@ -250,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();
|
||||||
|
|
||||||
@@ -283,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);
|
||||||
@@ -328,7 +333,12 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,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");
|
||||||
|
|||||||
+5
-3
@@ -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::*;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1155,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] {
|
||||||
@@ -1269,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");
|
||||||
|
|||||||
Reference in new issue
Block a user