From 8218e84b629075349281bdfce19d1e1b187f1304 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 11 Sep 2026 11:33:04 -0400 Subject: [PATCH] Run Iris examples on desktop and Android --- docs/PLAN.md | 4 + iris/Cargo.toml | 27 ++- iris/cargo-iris/src/package.rs | 201 +++++++++++++++--- iris/examples/bench_images/android.rs | 20 ++ .../desktop.rs} | 29 +-- iris/examples/bench_images/lib.rs | 24 +++ iris/examples/message_list/android.rs | 20 ++ iris/examples/message_list/desktop.rs | 26 +++ .../{message_list.rs => message_list/lib.rs} | 44 ++-- iris/examples/minimal/android.rs | 20 ++ .../{minimal.rs => minimal/desktop.rs} | 12 +- iris/examples/minimal/lib.rs | 5 + iris/examples/tabs/android.rs | 34 +++ iris/examples/tabs/desktop.rs | 31 +++ iris/examples/tabs/{main.rs => lib.rs} | 49 ++--- iris/examples/task.rs | 30 --- iris/examples/task/android.rs | 20 ++ iris/examples/task/desktop.rs | 21 ++ iris/examples/task/lib.rs | 21 ++ iris/examples/view.rs | 49 ----- iris/examples/view/android.rs | 20 ++ iris/examples/view/desktop.rs | 21 ++ iris/examples/view/lib.rs | 36 ++++ iris/readme.md | 16 +- iris/src/lib.rs | 1 + iris/src/rsc/task.rs | 5 + 26 files changed, 581 insertions(+), 205 deletions(-) create mode 100644 iris/examples/bench_images/android.rs rename iris/examples/{bench_images.rs => bench_images/desktop.rs} (61%) create mode 100644 iris/examples/bench_images/lib.rs create mode 100644 iris/examples/message_list/android.rs create mode 100644 iris/examples/message_list/desktop.rs rename iris/examples/{message_list.rs => message_list/lib.rs} (57%) create mode 100644 iris/examples/minimal/android.rs rename iris/examples/{minimal.rs => minimal/desktop.rs} (80%) create mode 100644 iris/examples/minimal/lib.rs create mode 100644 iris/examples/tabs/android.rs create mode 100644 iris/examples/tabs/desktop.rs rename iris/examples/tabs/{main.rs => lib.rs} (86%) delete mode 100644 iris/examples/task.rs create mode 100644 iris/examples/task/android.rs create mode 100644 iris/examples/task/desktop.rs create mode 100644 iris/examples/task/lib.rs delete mode 100644 iris/examples/view.rs create mode 100644 iris/examples/view/android.rs create mode 100644 iris/examples/view/desktop.rs create mode 100644 iris/examples/view/lib.rs diff --git a/docs/PLAN.md b/docs/PLAN.md index 6fe5efa..ef490a2 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -809,6 +809,10 @@ javac, d8, aapt2, jar, zipalign and apksigner. Gradle is not in the ordinary path because Iris's fixed Java view host has no Maven/AAR dependency or variant graph for it to manage. An application that later embeds Iris in a larger Gradle project can use that project as the packaging authority instead. +`--example NAME` packages the example's sibling `android.rs` as the Android +`cdylib`; `desktop.rs` remains Cargo's example target and both import their +shared `lib.rs`. This keeps the same widget tree runnable on both platforms +without adding another crate or making shared content select its host. The tool discovers and validates prerequisites but never installs an SDK, NDK, JDK, system image or emulator. `cargo iris run` requires an explicit diff --git a/iris/Cargo.toml b/iris/Cargo.toml index 06f0fa5..5ddcbd7 100644 --- a/iris/Cargo.toml +++ b/iris/Cargo.toml @@ -12,7 +12,7 @@ pollster = { workspace = true } wgpu = { workspace = true } image = { workspace = true } accesskit = { workspace = true } -tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] } +tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } # The embedding app installs the logger. log = "0.4.34" @@ -35,9 +35,32 @@ send_wrapper = "0.6.0" force-gles = [] [dev-dependencies] -tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } bytemuck = { workspace = true } +[[example]] +name = "bench_images" +path = "examples/bench_images/desktop.rs" + +[[example]] +name = "message_list" +path = "examples/message_list/desktop.rs" + +[[example]] +name = "minimal" +path = "examples/minimal/desktop.rs" + +[[example]] +name = "tabs" +path = "examples/tabs/desktop.rs" + +[[example]] +name = "task" +path = "examples/task/desktop.rs" + +[[example]] +name = "view" +path = "examples/view/desktop.rs" + [[bench]] name = "message_list" harness = false diff --git a/iris/cargo-iris/src/package.rs b/iris/cargo-iris/src/package.rs index 5eab202..979cbd7 100644 --- a/iris/cargo-iris/src/package.rs +++ b/iris/cargo-iris/src/package.rs @@ -1,4 +1,4 @@ -use cargo_metadata::{CrateType, MetadataCommand, Package}; +use cargo_metadata::{CrateType, MetadataCommand, Package, Target, TargetKind}; use std::{ env, ffi::OsStr, @@ -36,9 +36,9 @@ pub fn run(mut args: Vec) -> Result<(), String> { fn print_help() { println!( - "Build an Iris library as an installable Android APK.\n\n\ + "Build an Iris application or example as an installable Android APK.\n\n\ Usage:\n cargo iris apk [OPTIONS]\n cargo iris run --device SERIAL [OPTIONS]\n\n\ - Options:\n --manifest-path PATH\n --package NAME\n --abi arm64-v8a|x86_64\n --release\n\ + Options:\n --manifest-path PATH\n --package NAME\n --example NAME\n --abi arm64-v8a|x86_64\n --release\n\ \x20 --application-id ID\n --label TEXT\n --keystore PATH --key-alias ALIAS\n\n\ Release signing passwords come from IRIS_KEYSTORE_PASSWORD and, when different,\n\ IRIS_KEY_PASSWORD. Iris uses the Android SDK and emulator/device supplied by you." @@ -49,6 +49,7 @@ fn print_help() { struct Options { manifest_path: Option, package: Option, + example: Option, abi: String, release: bool, application_id: Option, @@ -77,6 +78,7 @@ impl Options { options.manifest_path = Some(value("--manifest-path", &mut i)?.into()) } "--package" => options.package = Some(value("--package", &mut i)?), + "--example" => options.example = Some(value("--example", &mut i)?), "--abi" => options.abi = value("--abi", &mut i)?, "--application-id" => { options.application_id = Some(value("--application-id", &mut i)?) @@ -128,43 +130,64 @@ fn build(options: &Options) -> Result { metadata.root_package(), options.package.as_deref(), )?; - let target = package - .targets - .iter() - .find(|target| { - target - .crate_types - .iter() - .any(|kind| kind == &CrateType::CDyLib) - }) - .ok_or_else(|| { - format!( - "package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}", - package.name, package.manifest_path - ) - })?; + let target = select_target(package, options.example.as_deref())?; let sdk = Sdk::find()?; let application_id = options .application_id .clone() - .or_else(|| metadata_string(package, "application-id")) - .unwrap_or_else(|| default_application_id(&package.name)); + .or_else(|| { + options + .example + .is_none() + .then(|| metadata_string(package, "application-id")) + .flatten() + }) + .unwrap_or_else(|| default_application_id(&package.name, options.example.as_deref())); validate_application_id(&application_id)?; let label = options .label .clone() - .or_else(|| metadata_string(package, "label")) - .unwrap_or_else(|| package.name.to_string()); + .or_else(|| { + options + .example + .is_none() + .then(|| metadata_string(package, "label")) + .flatten() + }) + .unwrap_or_else(|| { + options + .example + .clone() + .unwrap_or_else(|| package.name.to_string()) + }); let variant = if options.release { "release" } else { "debug" }; + let artifact = options.example.as_ref().map_or_else( + || package.name.to_string(), + |example| format!("{}-{example}", package.name), + ); let output = metadata .target_directory .as_std_path() .join("iris-android") - .join(package.name.as_str()) + .join(&artifact) .join(variant) .join(&options.abi); recreate(&output)?; let native = output.join("native"); + let (build_manifest, library_name) = if options.example.is_some() { + let wrapper = metadata + .target_directory + .as_std_path() + .join("iris-android") + .join("example-wrappers") + .join(&artifact); + materialize_example_wrapper(&metadata.packages, package, target, &wrapper)? + } else { + ( + package.manifest_path.as_std_path().to_path_buf(), + target.name.clone(), + ) + }; let mut cargo = Command::new("cargo"); cargo @@ -173,7 +196,8 @@ fn build(options: &Options) -> Result { .arg("build") .arg("--lib") .arg("--manifest-path") - .arg(package.manifest_path.as_std_path()); + .arg(&build_manifest) + .env("CARGO_TARGET_DIR", metadata.target_directory.as_std_path()); if options.release { cargo.arg("--release"); } @@ -185,13 +209,13 @@ fn build(options: &Options) -> Result { let library = native .join(&options.abi) - .join(format!("lib{}.so", target.name)); + .join(format!("lib{library_name}.so")); if !library.is_file() { return Err(format!("cargo-ndk did not produce {}", library.display())); } let classes = output.join("classes"); fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?; - let sources = materialize_host(&output, &target.name)?; + let sources = materialize_host(&output, &library_name)?; let java_files = files_with_extension(&sources, "java")?; let mut javac = Command::new("javac"); javac @@ -229,7 +253,7 @@ fn build(options: &Options) -> Result { let manifest = output.join("AndroidManifest.xml"); fs::write( &manifest, - manifest_xml(&application_id, &label, &target.name, sdk.api), + manifest_xml(&application_id, &label, &library_name, sdk.api), ) .map_err(io_error("write Android manifest", &manifest))?; let unsigned = output.join("unsigned.apk"); @@ -258,7 +282,7 @@ fn build(options: &Options) -> Result { &dex.join("classes.dex"), &library, &options.abi, - &target.name, + &library_name, )?; let aligned = output.join("aligned.apk"); @@ -272,7 +296,7 @@ fn build(options: &Options) -> Result { "APK alignment", "install Android SDK Build Tools", )?; - let apk = output.join(format!("{}-{variant}.apk", package.name)); + let apk = output.join(format!("{artifact}-{variant}.apk")); sign(&sdk, options, &aligned, &apk)?; verify(&sdk, &apk)?; Ok(Built { @@ -298,6 +322,106 @@ fn select_package<'a>( }) } +fn select_target<'a>(package: &'a Package, example: Option<&str>) -> Result<&'a Target, String> { + if let Some(example) = example { + return package + .targets + .iter() + .find(|target| { + target.name == example + && target.kind.iter().any(|kind| kind == &TargetKind::Example) + }) + .ok_or_else(|| { + format!( + "package {} has no example named {example:?}; use one of: {}", + package.name, + package + .targets + .iter() + .filter(|target| target + .kind + .iter() + .any(|kind| kind == &TargetKind::Example)) + .map(|target| target.name.as_str()) + .collect::>() + .join(", ") + ) + }); + } + package + .targets + .iter() + .find(|target| { + target + .crate_types + .iter() + .any(|kind| kind == &CrateType::CDyLib) + }) + .ok_or_else(|| { + format!( + "package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}", + package.name, package.manifest_path + ) + }) +} + +fn materialize_example_wrapper( + packages: &[Package], + package: &Package, + example: &Target, + wrapper: &Path, +) -> Result<(PathBuf, String), String> { + let android_source = example + .src_path + .as_std_path() + .parent() + .unwrap() + .join("android.rs"); + if !android_source.is_file() { + return Err(format!( + "example {:?} has no Android entry point at {}; put shared code in lib.rs and add sibling desktop.rs and android.rs entries", + example.name, + android_source.display() + )); + } + let iris = packages + .iter() + .find(|dependency| dependency.name == "iris") + .ok_or_else(|| { + format!( + "example {:?} does not depend on Iris; add `iris` to {}", + example.name, package.manifest_path + ) + })?; + let source_dir = wrapper.join("src"); + fs::create_dir_all(&source_dir) + .map_err(io_error("create Android example wrapper", &source_dir))?; + let library_name = format!( + "iris_android_{}_{}", + identifier_segment(&package.name), + identifier_segment(&example.name) + ); + let iris_root = iris.manifest_path.parent().unwrap(); + let manifest = format!( + "[package]\nname = {name:?}\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\ + [lib]\nname = {library_name:?}\ncrate-type = [\"cdylib\", \"rlib\"]\n\n\ + [dependencies]\niris = {{ path = {iris_root:?} }}\n\n[workspace]\n", + name = format!("iris-android-{}-{}", package.name, example.name), + iris_root = iris_root.as_str(), + ); + let manifest_path = wrapper.join("Cargo.toml"); + fs::write(&manifest_path, manifest) + .map_err(io_error("write Android example manifest", &manifest_path))?; + let source = format!( + "#[path = {:?}]\nmod example;\n", + android_source.to_string_lossy() + ); + let source_path = source_dir.join("lib.rs"); + fs::write(&source_path, source) + .map_err(io_error("write Android example wrapper", &source_path))?; + Ok((manifest_path, library_name)) +} + fn metadata_string(package: &Package, key: &str) -> Option { package .metadata @@ -308,8 +432,8 @@ fn metadata_string(package: &Package, key: &str) -> Option { .map(str::to_owned) } -fn default_application_id(package: &str) -> String { - let segment: String = package +fn identifier_segment(value: &str) -> String { + value .chars() .map(|c| { if c.is_ascii_alphanumeric() { @@ -318,8 +442,15 @@ fn default_application_id(package: &str) -> String { '_' } }) - .collect(); - format!("dev.iris.app.{segment}") + .collect() +} + +fn default_application_id(package: &str, example: Option<&str>) -> String { + let package = identifier_segment(package); + match example { + Some(example) => format!("dev.iris.example.{package}.{}", identifier_segment(example)), + None => format!("dev.iris.app.{package}"), + } } fn validate_application_id(id: &str) -> Result<(), String> { @@ -699,6 +830,10 @@ mod tests { assert!(validate_application_id("one").is_err()); assert!(validate_application_id("dev.2demo").is_err()); assert!(validate_application_id("dev.iris.bad-name").is_err()); + assert_eq!( + default_application_id("demo-app", Some("color-picker")), + "dev.iris.example.demo_app.color_picker" + ); } #[test] diff --git a/iris/examples/bench_images/android.rs b/iris/examples/bench_images/android.rs new file mode 100644 index 0000000..fd0c88b --- /dev/null +++ b/iris/examples/bench_images/android.rs @@ -0,0 +1,20 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct State { + ui_state: AndroidUiState, +} + +impl AndroidAppState for State { + type Resources = StdRsc; +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> State { + let _ = build(rsc, &mut ui_state); + State { ui_state } +} diff --git a/iris/examples/bench_images.rs b/iris/examples/bench_images/desktop.rs similarity index 61% rename from iris/examples/bench_images.rs rename to iris/examples/bench_images/desktop.rs index 4754fe2..57fcfc6 100644 --- a/iris/examples/bench_images.rs +++ b/iris/examples/bench_images/desktop.rs @@ -1,6 +1,10 @@ use iris::prelude::*; +use winit::event::WindowEvent; + +#[path = "lib.rs"] +mod app; +use app::*; -const ROWS: usize = 1000; const SETTLE_FRAMES: usize = 4; const FRAMES: usize = 6; @@ -14,30 +18,17 @@ struct State { impl DesktopAppState for State { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let mut span = Span::empty(Dir::DOWN); - for _ in 0..ROWS { - let img = image::DynamicImage::new_rgba8(32, 32); - let widget = image::>(img)(rsc); - let widget = rsc.ui.widgets.add_strong(widget); - span.push(widget.any()); - } - let span = rsc.ui.widgets.add_strong(span); - let span_weak = span.weak(); - let root = rsc - .ui - .widgets - .add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End)); - ui_state.set_root(rsc, root.any()); + let span = build(rsc, &mut ui_state); Self { ui_state, - span: span_weak, + span, frame: 0, appended: false, } } - fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut StdRsc) { - if !matches!(event, winit::event::WindowEvent::RedrawRequested) { + fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc) { + if !matches!(event, WindowEvent::RedrawRequested) { return; } self.frame += 1; @@ -48,7 +39,7 @@ impl DesktopAppState for State { ); if self.frame == SETTLE_FRAMES && !self.appended { self.appended = true; - let img = image::DynamicImage::new_rgba8(32, 32); + let img = iris::image::DynamicImage::new_rgba8(32, 32); let widget = image::>(img)(rsc); let widget = rsc.ui.widgets.add_strong(widget); rsc.ui diff --git a/iris/examples/bench_images/lib.rs b/iris/examples/bench_images/lib.rs new file mode 100644 index 0000000..15d3e25 --- /dev/null +++ b/iris/examples/bench_images/lib.rs @@ -0,0 +1,24 @@ +use iris::prelude::*; + +const ROWS: usize = 1000; + +pub(crate) fn build( + rsc: &mut Rsc, + ui_state: &mut impl HasRoot, +) -> WeakWidget { + let mut span = Span::empty(Dir::DOWN); + for _ in 0..ROWS { + let img = iris::image::DynamicImage::new_rgba8(32, 32); + let widget = image::(img)(rsc); + let widget = rsc.ui_mut().widgets.add_strong(widget); + span.push(widget.any()); + } + let span = rsc.ui_mut().widgets.add_strong(span); + let span_weak = span.weak(); + let root = rsc + .ui_mut() + .widgets + .add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End)); + ui_state.set_root(rsc, root.any()); + span_weak +} diff --git a/iris/examples/message_list/android.rs b/iris/examples/message_list/android.rs new file mode 100644 index 0000000..63b7e7b --- /dev/null +++ b/iris/examples/message_list/android.rs @@ -0,0 +1,20 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct State { + ui_state: AndroidUiState, +} + +impl AndroidAppState for State { + type Resources = StdRsc; +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> State { + build(rsc, &mut ui_state); + State { ui_state } +} diff --git a/iris/examples/message_list/desktop.rs b/iris/examples/message_list/desktop.rs new file mode 100644 index 0000000..4657509 --- /dev/null +++ b/iris/examples/message_list/desktop.rs @@ -0,0 +1,26 @@ +use iris::prelude::*; +use winit::{dpi::LogicalSize, window::WindowAttributes}; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(DesktopUiState)] +struct State { + ui_state: DesktopUiState, +} + +impl DesktopAppState for State { + fn window_attributes() -> WindowAttributes { + WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) + } + + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { + build(rsc, &mut ui_state); + Self { ui_state } + } +} + +fn main() { + DesktopApp::::run(); +} diff --git a/iris/examples/message_list.rs b/iris/examples/message_list/lib.rs similarity index 57% rename from iris/examples/message_list.rs rename to iris/examples/message_list/lib.rs index 0a6470b..0038bb6 100644 --- a/iris/examples/message_list.rs +++ b/iris/examples/message_list/lib.rs @@ -1,14 +1,4 @@ use iris::prelude::*; -use winit::{dpi::LogicalSize, window::WindowAttributes}; - -fn main() { - DesktopApp::::run(); -} - -#[derive(DesktopUiState)] -struct State { - ui_state: DesktopUiState, -} const ROWS: usize = 800; const IMAGE_EVERY: usize = 12; @@ -20,9 +10,9 @@ fn row_text(i: usize) -> String { format!("Message {i}: {}", SENTENCE.repeat(repeats)) } -fn row_image(i: usize) -> image::DynamicImage { +fn row_image(i: usize) -> iris::image::DynamicImage { let hue = ((i * 47) % 255) as u8; - image::RgbaImage::from_pixel(48, 48, image::Rgba([hue, 128, 255 - hue, 255])).into() + iris::image::RgbaImage::from_pixel(48, 48, iris::image::Rgba([hue, 128, 255 - hue, 255])).into() } fn build_row(rsc: &mut Rsc, i: usize) -> StrongWidget { @@ -58,25 +48,17 @@ fn build_row(rsc: &mut Rsc, i: usize) -> StrongWidget { } } -impl DesktopAppState for State { - fn window_attributes() -> WindowAttributes { - WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) +pub(crate) fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) { + let mut list = LazySpan::new(Dir::DOWN, Pin::End); + for i in 0..ROWS { + let row = build_row(rsc, i); + list.push_back(LazyItem::new(i as u64, row)); } - fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let mut list = LazySpan::new(Dir::DOWN, Pin::End); - for i in 0..ROWS { - let row = build_row(rsc, i); - list.push_back(LazyItem::new(i as u64, row)); - } - - let root = list - .scrollable() - .masked() - .background(rect(PaintId::WHITE)) - .add_strong(rsc); - ui_state.set_root(rsc, root.any()); - - Self { ui_state } - } + let root = list + .scrollable() + .masked() + .background(rect(PaintId::WHITE)) + .add_strong(rsc); + ui_state.set_root(rsc, root.any()); } diff --git a/iris/examples/minimal/android.rs b/iris/examples/minimal/android.rs new file mode 100644 index 0000000..63b7e7b --- /dev/null +++ b/iris/examples/minimal/android.rs @@ -0,0 +1,20 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct State { + ui_state: AndroidUiState, +} + +impl AndroidAppState for State { + type Resources = StdRsc; +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> State { + build(rsc, &mut ui_state); + State { ui_state } +} diff --git a/iris/examples/minimal.rs b/iris/examples/minimal/desktop.rs similarity index 80% rename from iris/examples/minimal.rs rename to iris/examples/minimal/desktop.rs index 579ae5a..2a2dd10 100644 --- a/iris/examples/minimal.rs +++ b/iris/examples/minimal/desktop.rs @@ -1,8 +1,8 @@ use iris::prelude::*; -fn main() { - DesktopApp::::run(); -} +#[path = "lib.rs"] +mod app; +use app::*; #[derive(DesktopUiState)] struct State { @@ -11,7 +11,11 @@ struct State { impl DesktopAppState for State { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - rect(PaintId::RED).set_root(rsc, &mut ui_state); + build(rsc, &mut ui_state); Self { ui_state } } } + +fn main() { + DesktopApp::::run(); +} diff --git a/iris/examples/minimal/lib.rs b/iris/examples/minimal/lib.rs new file mode 100644 index 0000000..334a542 --- /dev/null +++ b/iris/examples/minimal/lib.rs @@ -0,0 +1,5 @@ +use iris::prelude::*; + +pub(crate) fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) { + rect(PaintId::RED).set_root(rsc, ui_state); +} diff --git a/iris/examples/tabs/android.rs b/iris/examples/tabs/android.rs new file mode 100644 index 0000000..39a839e --- /dev/null +++ b/iris/examples/tabs/android.rs @@ -0,0 +1,34 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct Client { + ui_state: AndroidUiState, + info: WeakWidget, +} + +impl AndroidAppState for Client { + type Resources = StdRsc; + + fn on_insets_changed(&mut self, rsc: &mut Self::Resources, _: WindowInsets) { + let views = self + .ui_state + .renderer + .as_ref() + .map_or(0, |renderer| renderer.ui.view_count()); + update_info(rsc, self.info, views); + } +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Client { + let widgets = build(rsc, &mut ui_state); + update_info(rsc, widgets.info, 0); + Client { + ui_state, + info: widgets.info, + } +} diff --git a/iris/examples/tabs/desktop.rs b/iris/examples/tabs/desktop.rs new file mode 100644 index 0000000..8379d6f --- /dev/null +++ b/iris/examples/tabs/desktop.rs @@ -0,0 +1,31 @@ +use iris::prelude::*; +use winit::event::WindowEvent; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(DesktopUiState)] +struct Client { + ui_state: DesktopUiState, + info: WeakWidget, +} + +impl DesktopAppState for Client { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { + let widgets = build(rsc, &mut ui_state); + update_info(rsc, widgets.info, 0); + Self { + ui_state, + info: widgets.info, + } + } + + fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc) { + update_info(rsc, self.info, self.ui_state.renderer.ui.view_count()); + } +} + +fn main() { + DesktopApp::::run(); +} diff --git a/iris/examples/tabs/main.rs b/iris/examples/tabs/lib.rs similarity index 86% rename from iris/examples/tabs/main.rs rename to iris/examples/tabs/lib.rs index 13ce661..6d762d8 100644 --- a/iris/examples/tabs/main.rs +++ b/iris/examples/tabs/lib.rs @@ -1,45 +1,26 @@ use iris::prelude::*; use std::{cell::RefCell, rc::Rc}; -use winit::event::WindowEvent; -fn main() { - DesktopApp::::run(); -} - -#[derive(DesktopUiState)] -struct Client { - ui_state: DesktopUiState, - info: WeakWidget, -} - -impl DesktopAppState for Client { - fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let widgets = build(rsc, &mut ui_state); - Self { - ui_state, - info: widgets.info, - } - } - - fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc) { - let render_state = rsc.ui.render_state(); - let new = format!( - "widgets: {}\nactive: {}\nviews: {}", - rsc.widgets().len(), - render_state.get().active_widgets(), - self.ui_state.renderer.ui.view_count(), - ); - if new != *rsc.widgets()[self.info].content { - *rsc.widgets_mut()[self.info].content = new; - } +pub(crate) fn update_info(rsc: &mut Rsc, info: WeakWidget, views: usize) { + let render_state = rsc.ui().render_state(); + let new = format!( + "widgets: {}\nactive: {}\nviews: {views}", + rsc.widgets().len(), + render_state.get().active_widgets(), + ); + if new != *rsc.widgets()[info].content { + *rsc.widgets_mut()[info].content = new; } } -struct ClientWidgets { - info: WeakWidget, +pub(crate) struct ClientWidgets { + pub(crate) info: WeakWidget, } -fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets +pub(crate) fn build( + rsc: &mut Rsc, + ui_state: &mut impl HasRoot, +) -> ClientWidgets where Rsc::State: FocusHost, { diff --git a/iris/examples/task.rs b/iris/examples/task.rs deleted file mode 100644 index f0d0a3c..0000000 --- a/iris/examples/task.rs +++ /dev/null @@ -1,30 +0,0 @@ -use iris::prelude::*; -use std::time::Duration; - -fn main() { - DesktopApp::::run(); -} - -#[derive(DesktopUiState)] -struct State { - ui_state: DesktopUiState, -} - -impl DesktopAppState for State { - fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let rect = rect(PaintId::RED).add(rsc); - rect.task_on(CursorSense::click(), async move |mut ctx| { - tokio::time::sleep(Duration::from_secs(1)).await; - ctx.update(move |_, rsc| { - let rect = rect(rsc); - if rect.is_paint(&PaintId::RED) { - rect.set_paint(PaintId::BLUE); - } else { - rect.set_paint(PaintId::RED); - } - }); - }) - .set_root(rsc, &mut ui_state); - Self { ui_state } - } -} diff --git a/iris/examples/task/android.rs b/iris/examples/task/android.rs new file mode 100644 index 0000000..63b7e7b --- /dev/null +++ b/iris/examples/task/android.rs @@ -0,0 +1,20 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct State { + ui_state: AndroidUiState, +} + +impl AndroidAppState for State { + type Resources = StdRsc; +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> State { + build(rsc, &mut ui_state); + State { ui_state } +} diff --git a/iris/examples/task/desktop.rs b/iris/examples/task/desktop.rs new file mode 100644 index 0000000..2a2dd10 --- /dev/null +++ b/iris/examples/task/desktop.rs @@ -0,0 +1,21 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(DesktopUiState)] +struct State { + ui_state: DesktopUiState, +} + +impl DesktopAppState for State { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { + build(rsc, &mut ui_state); + Self { ui_state } + } +} + +fn main() { + DesktopApp::::run(); +} diff --git a/iris/examples/task/lib.rs b/iris/examples/task/lib.rs new file mode 100644 index 0000000..d4dbe0a --- /dev/null +++ b/iris/examples/task/lib.rs @@ -0,0 +1,21 @@ +use iris::prelude::*; +use std::time::Duration; + +pub(crate) fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) +where + Rsc::State: FocusHost, +{ + let rect = rect(PaintId::RED).add(rsc); + rect.task_on(CursorSense::click(), async move |mut ctx| { + iris::task::sleep(Duration::from_secs(1)).await; + ctx.update(move |_, rsc| { + let rect = rect(rsc); + if rect.is_paint(&PaintId::RED) { + rect.set_paint(PaintId::BLUE); + } else { + rect.set_paint(PaintId::RED); + } + }); + }) + .set_root(rsc, ui_state); +} diff --git a/iris/examples/view.rs b/iris/examples/view.rs deleted file mode 100644 index 396344c..0000000 --- a/iris/examples/view.rs +++ /dev/null @@ -1,49 +0,0 @@ -use iris::prelude::*; - -fn main() { - DesktopApp::::run(); -} - -#[derive(DesktopUiState)] -struct State { - ui_state: DesktopUiState, -} - -type Rsc = StdRsc; - -#[derive(Clone, Copy, WidgetView)] -struct Test { - #[root] - root: WeakWidget, - cur: WeakState, -} - -impl Test { - pub fn new(rsc: &mut Rsc) -> Self { - let root = rect(PaintId::RED).add(rsc); - let cur = rsc.create_state(root, false); - Self { root, cur } - } - pub fn toggle(&self, rsc: &mut Rsc) { - let cur = &mut rsc[self.cur]; - *cur = !*cur; - if *cur { - rsc[self.root].set_paint(PaintId::BLUE); - } else { - rsc[self.root].set_paint(PaintId::RED); - } - } -} - -impl DesktopAppState for State { - fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let test = Test::new(rsc); - - test.on(CursorSense::click(), move |_, rsc| { - test.toggle(rsc); - }) - .set_root(rsc, &mut ui_state); - - Self { ui_state } - } -} diff --git a/iris/examples/view/android.rs b/iris/examples/view/android.rs new file mode 100644 index 0000000..63b7e7b --- /dev/null +++ b/iris/examples/view/android.rs @@ -0,0 +1,20 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(AndroidUiState)] +struct State { + ui_state: AndroidUiState, +} + +impl AndroidAppState for State { + type Resources = StdRsc; +} + +#[iris::app_init] +fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> State { + build(rsc, &mut ui_state); + State { ui_state } +} diff --git a/iris/examples/view/desktop.rs b/iris/examples/view/desktop.rs new file mode 100644 index 0000000..2a2dd10 --- /dev/null +++ b/iris/examples/view/desktop.rs @@ -0,0 +1,21 @@ +use iris::prelude::*; + +#[path = "lib.rs"] +mod app; +use app::*; + +#[derive(DesktopUiState)] +struct State { + ui_state: DesktopUiState, +} + +impl DesktopAppState for State { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { + build(rsc, &mut ui_state); + Self { ui_state } + } +} + +fn main() { + DesktopApp::::run(); +} diff --git a/iris/examples/view/lib.rs b/iris/examples/view/lib.rs new file mode 100644 index 0000000..16f4846 --- /dev/null +++ b/iris/examples/view/lib.rs @@ -0,0 +1,36 @@ +use iris::prelude::*; + +#[derive(Clone, Copy, WidgetView)] +struct Test { + #[root] + root: WeakWidget, + cur: WeakState, +} + +impl Test { + pub fn new(rsc: &mut StdRsc) -> Self { + let root = rect(PaintId::RED).add(rsc); + let cur = rsc.create_state(root, false); + Self { root, cur } + } + pub fn toggle(&self, rsc: &mut StdRsc) { + let cur = &mut rsc[self.cur]; + *cur = !*cur; + if *cur { + rsc[self.root].set_paint(PaintId::BLUE); + } else { + rsc[self.root].set_paint(PaintId::RED); + } + } +} + +pub(crate) fn build(rsc: &mut StdRsc, ui_state: &mut impl HasRoot>) +where + State: FocusHost, +{ + let test = Test::new(rsc); + test.on(CursorSense::click(), move |_, rsc| { + test.toggle(rsc); + }) + .set_root(rsc, ui_state); +} diff --git a/iris/readme.md b/iris/readme.md index 3f17f4d..76a771b 100644 --- a/iris/readme.md +++ b/iris/readme.md @@ -4,7 +4,9 @@ My experimental attempt at a rust ui library (also my first ui library). It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not. -Examples are in `examples`, eg. `cargo run --example tabs`. +Examples are in `examples`, eg. `cargo run --example tabs`. Each example keeps +its widget tree in `lib.rs` and its small desktop and Android hosts in +`desktop.rs` and `android.rs`. ## Android applications @@ -57,6 +59,14 @@ cargo iris apk --abi arm64-v8a cargo iris run --abi x86_64 --device emulator-5554 ``` +Package examples use the same command with `--example`: + +```sh +cargo run --example tabs +cargo iris apk --example tabs --abi arm64-v8a +cargo iris run --example tabs --abi x86_64 --device emulator-5554 +``` + `cargo iris` packages directly with `cargo-ndk`, `javac`, `d8`, `aapt2`, `jar`, `zipalign`, and `apksigner`; it does not require Gradle. The caller provides a JDK, Android SDK and NDK, and any emulator or physical device. Set @@ -73,8 +83,8 @@ IRIS_KEYSTORE_PASSWORD=... IRIS_KEY_PASSWORD=... \ ``` APK staging and output live under -`target/iris-android////`; the command's final -line is the verified APK's absolute path. +`target/iris-android/[-]///`; the +command's final line is the verified APK's absolute path. Goals, in general order: 1. does what I want it to (text, images, video, animations) diff --git a/iris/src/lib.rs b/iris/src/lib.rs index cb89a73..b840090 100644 --- a/iris/src/lib.rs +++ b/iris/src/lib.rs @@ -27,6 +27,7 @@ mod access_tests; #[cfg(test)] mod layout_tests; +pub use image; pub use iris_core as core; pub use iris_macro as macros; pub use iris_macro::app_init; diff --git a/iris/src/rsc/task.rs b/iris/src/rsc/task.rs index ff9ffbd..1cd6a29 100644 --- a/iris/src/rsc/task.rs +++ b/iris/src/rsc/task.rs @@ -14,6 +14,11 @@ use tokio::{ }, }; +/// Waits without blocking the task runtime's worker thread. +pub async fn sleep(duration: std::time::Duration) { + tokio::time::sleep(duration).await; +} + /// What a completed task nudges when it wants its result drawn. Shared /// between backends rather than typed as `winit::window::Window` directly: /// android-view has no `Window` at all, and the redraw request there is a