Run Iris examples on desktop and Android

This commit is contained in:
iris committed 2026-09-11 11:33:04 -04:00
1 parent df1290904b
commit 8218e84b62
26 files changed
+581 -205

No files matched your search

+4
View File
@@ -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 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 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. 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, The tool discovers and validates prerequisites but never installs an SDK,
NDK, JDK, system image or emulator. `cargo iris run` requires an explicit NDK, JDK, system image or emulator. `cargo iris run` requires an explicit
+25 -2
View File
@@ -12,7 +12,7 @@ pollster = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
image = { workspace = true } image = { workspace = true }
accesskit = { 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. # The embedding app installs the logger.
log = "0.4.34" log = "0.4.34"
@@ -35,9 +35,32 @@ send_wrapper = "0.6.0"
force-gles = [] force-gles = []
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
bytemuck = { workspace = true } 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]] [[bench]]
name = "message_list" name = "message_list"
harness = false harness = false
+168 -33
View File
@@ -1,4 +1,4 @@
use cargo_metadata::{CrateType, MetadataCommand, Package}; use cargo_metadata::{CrateType, MetadataCommand, Package, Target, TargetKind};
use std::{ use std::{
env, env,
ffi::OsStr, ffi::OsStr,
@@ -36,9 +36,9 @@ pub fn run(mut args: Vec<String>) -> Result<(), String> {
fn print_help() { fn print_help() {
println!( 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\ 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\ \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\ 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." IRIS_KEY_PASSWORD. Iris uses the Android SDK and emulator/device supplied by you."
@@ -49,6 +49,7 @@ fn print_help() {
struct Options { struct Options {
manifest_path: Option<PathBuf>, manifest_path: Option<PathBuf>,
package: Option<String>, package: Option<String>,
example: Option<String>,
abi: String, abi: String,
release: bool, release: bool,
application_id: Option<String>, application_id: Option<String>,
@@ -77,6 +78,7 @@ impl Options {
options.manifest_path = Some(value("--manifest-path", &mut i)?.into()) options.manifest_path = Some(value("--manifest-path", &mut i)?.into())
} }
"--package" => options.package = Some(value("--package", &mut i)?), "--package" => options.package = Some(value("--package", &mut i)?),
"--example" => options.example = Some(value("--example", &mut i)?),
"--abi" => options.abi = value("--abi", &mut i)?, "--abi" => options.abi = value("--abi", &mut i)?,
"--application-id" => { "--application-id" => {
options.application_id = Some(value("--application-id", &mut i)?) options.application_id = Some(value("--application-id", &mut i)?)
@@ -128,43 +130,64 @@ fn build(options: &Options) -> Result<Built, String> {
metadata.root_package(), metadata.root_package(),
options.package.as_deref(), options.package.as_deref(),
)?; )?;
let target = package let target = select_target(package, options.example.as_deref())?;
.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 sdk = Sdk::find()?; let sdk = Sdk::find()?;
let application_id = options let application_id = options
.application_id .application_id
.clone() .clone()
.or_else(|| metadata_string(package, "application-id")) .or_else(|| {
.unwrap_or_else(|| default_application_id(&package.name)); 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)?; validate_application_id(&application_id)?;
let label = options let label = options
.label .label
.clone() .clone()
.or_else(|| metadata_string(package, "label")) .or_else(|| {
.unwrap_or_else(|| package.name.to_string()); 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 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 let output = metadata
.target_directory .target_directory
.as_std_path() .as_std_path()
.join("iris-android") .join("iris-android")
.join(package.name.as_str()) .join(&artifact)
.join(variant) .join(variant)
.join(&options.abi); .join(&options.abi);
recreate(&output)?; recreate(&output)?;
let native = output.join("native"); 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"); let mut cargo = Command::new("cargo");
cargo cargo
@@ -173,7 +196,8 @@ fn build(options: &Options) -> Result<Built, String> {
.arg("build") .arg("build")
.arg("--lib") .arg("--lib")
.arg("--manifest-path") .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 { if options.release {
cargo.arg("--release"); cargo.arg("--release");
} }
@@ -185,13 +209,13 @@ fn build(options: &Options) -> Result<Built, String> {
let library = native let library = native
.join(&options.abi) .join(&options.abi)
.join(format!("lib{}.so", target.name)); .join(format!("lib{library_name}.so"));
if !library.is_file() { if !library.is_file() {
return Err(format!("cargo-ndk did not produce {}", library.display())); return Err(format!("cargo-ndk did not produce {}", library.display()));
} }
let classes = output.join("classes"); let classes = output.join("classes");
fs::create_dir_all(&classes).map_err(io_error("create Java output", &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 java_files = files_with_extension(&sources, "java")?;
let mut javac = Command::new("javac"); let mut javac = Command::new("javac");
javac javac
@@ -229,7 +253,7 @@ fn build(options: &Options) -> Result<Built, String> {
let manifest = output.join("AndroidManifest.xml"); let manifest = output.join("AndroidManifest.xml");
fs::write( fs::write(
&manifest, &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))?; .map_err(io_error("write Android manifest", &manifest))?;
let unsigned = output.join("unsigned.apk"); let unsigned = output.join("unsigned.apk");
@@ -258,7 +282,7 @@ fn build(options: &Options) -> Result<Built, String> {
&dex.join("classes.dex"), &dex.join("classes.dex"),
&library, &library,
&options.abi, &options.abi,
&target.name, &library_name,
)?; )?;
let aligned = output.join("aligned.apk"); let aligned = output.join("aligned.apk");
@@ -272,7 +296,7 @@ fn build(options: &Options) -> Result<Built, String> {
"APK alignment", "APK alignment",
"install Android SDK Build Tools", "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)?; sign(&sdk, options, &aligned, &apk)?;
verify(&sdk, &apk)?; verify(&sdk, &apk)?;
Ok(Built { 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::<Vec<_>>()
.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<String> { fn metadata_string(package: &Package, key: &str) -> Option<String> {
package package
.metadata .metadata
@@ -308,8 +432,8 @@ fn metadata_string(package: &Package, key: &str) -> Option<String> {
.map(str::to_owned) .map(str::to_owned)
} }
fn default_application_id(package: &str) -> String { fn identifier_segment(value: &str) -> String {
let segment: String = package value
.chars() .chars()
.map(|c| { .map(|c| {
if c.is_ascii_alphanumeric() { if c.is_ascii_alphanumeric() {
@@ -318,8 +442,15 @@ fn default_application_id(package: &str) -> String {
'_' '_'
} }
}) })
.collect(); .collect()
format!("dev.iris.app.{segment}") }
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> { 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("one").is_err());
assert!(validate_application_id("dev.2demo").is_err()); assert!(validate_application_id("dev.2demo").is_err());
assert!(validate_application_id("dev.iris.bad-name").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] #[test]
+20
View File
@@ -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<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
let _ = build(rsc, &mut ui_state);
State { ui_state }
}
@@ -1,6 +1,10 @@
use iris::prelude::*; use iris::prelude::*;
use winit::event::WindowEvent;
#[path = "lib.rs"]
mod app;
use app::*;
const ROWS: usize = 1000;
const SETTLE_FRAMES: usize = 4; const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6; const FRAMES: usize = 6;
@@ -14,30 +18,17 @@ struct State {
impl DesktopAppState for State { impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
let mut span = Span::empty(Dir::DOWN); let span = build(rsc, &mut ui_state);
for _ in 0..ROWS {
let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<StdRsc<Self>>(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());
Self { Self {
ui_state, ui_state,
span: span_weak, span,
frame: 0, frame: 0,
appended: false, appended: false,
} }
} }
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut StdRsc<Self>) { fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc<Self>) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) { if !matches!(event, WindowEvent::RedrawRequested) {
return; return;
} }
self.frame += 1; self.frame += 1;
@@ -48,7 +39,7 @@ impl DesktopAppState 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 = iris::image::DynamicImage::new_rgba8(32, 32);
let widget = image::<StdRsc<Self>>(img)(rsc); let widget = image::<StdRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget); let widget = rsc.ui.widgets.add_strong(widget);
rsc.ui rsc.ui
+24
View File
@@ -0,0 +1,24 @@
use iris::prelude::*;
const ROWS: usize = 1000;
pub(crate) fn build<Rsc: UiRsc>(
rsc: &mut Rsc,
ui_state: &mut impl HasRoot<Rsc>,
) -> WeakWidget<Span> {
let mut span = Span::empty(Dir::DOWN);
for _ in 0..ROWS {
let img = iris::image::DynamicImage::new_rgba8(32, 32);
let widget = image::<Rsc>(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
}
+20
View File
@@ -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<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
}
+26
View File
@@ -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<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() {
DesktopApp::<State>::run();
}
@@ -1,14 +1,4 @@
use iris::prelude::*; use iris::prelude::*;
use winit::{dpi::LogicalSize, window::WindowAttributes};
fn main() {
DesktopApp::<State>::run();
}
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
const ROWS: usize = 800; const ROWS: usize = 800;
const IMAGE_EVERY: usize = 12; const IMAGE_EVERY: usize = 12;
@@ -20,9 +10,9 @@ fn row_text(i: usize) -> String {
format!("Message {i}: {}", SENTENCE.repeat(repeats)) 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; 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: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget { fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
@@ -58,25 +48,17 @@ fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
} }
} }
impl DesktopAppState for State { pub(crate) fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) {
fn window_attributes() -> WindowAttributes { let mut list = LazySpan::new(Dir::DOWN, Pin::End);
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) 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<Self>, _: Proxy<Self::Event>) -> Self { let root = list
let mut list = LazySpan::new(Dir::DOWN, Pin::End); .scrollable()
for i in 0..ROWS { .masked()
let row = build_row(rsc, i); .background(rect(PaintId::WHITE))
list.push_back(LazyItem::new(i as u64, row)); .add_strong(rsc);
} ui_state.set_root(rsc, root.any());
let root = list
.scrollable()
.masked()
.background(rect(PaintId::WHITE))
.add_strong(rsc);
ui_state.set_root(rsc, root.any());
Self { ui_state }
}
} }
+20
View File
@@ -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<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
}
@@ -1,8 +1,8 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { #[path = "lib.rs"]
DesktopApp::<State>::run(); mod app;
} use app::*;
#[derive(DesktopUiState)] #[derive(DesktopUiState)]
struct State { struct State {
@@ -11,7 +11,11 @@ struct State {
impl DesktopAppState for State { impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
rect(PaintId::RED).set_root(rsc, &mut ui_state); build(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
} }
fn main() {
DesktopApp::<State>::run();
}
+5
View File
@@ -0,0 +1,5 @@
use iris::prelude::*;
pub(crate) fn build<Rsc: UiRsc>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) {
rect(PaintId::RED).set_root(rsc, ui_state);
}
+34
View File
@@ -0,0 +1,34 @@
use iris::prelude::*;
#[path = "lib.rs"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct Client {
ui_state: AndroidUiState,
info: WeakWidget<Text>,
}
impl AndroidAppState for Client {
type Resources = StdRsc<Self>;
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>) -> Client {
let widgets = build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0);
Client {
ui_state,
info: widgets.info,
}
}
+31
View File
@@ -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<Text>,
}
impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> 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<Self>) {
update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
}
}
fn main() {
DesktopApp::<Client>::run();
}
@@ -1,45 +1,26 @@
use iris::prelude::*; use iris::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use winit::event::WindowEvent;
fn main() { pub(crate) fn update_info<Rsc: UiRsc>(rsc: &mut Rsc, info: WeakWidget<Text>, views: usize) {
DesktopApp::<Client>::run(); let render_state = rsc.ui().render_state();
} let new = format!(
"widgets: {}\nactive: {}\nviews: {views}",
#[derive(DesktopUiState)] rsc.widgets().len(),
struct Client { render_state.get().active_widgets(),
ui_state: DesktopUiState, );
info: WeakWidget<Text>, if new != *rsc.widgets()[info].content {
} *rsc.widgets_mut()[info].content = new;
impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
let widgets = build(rsc, &mut ui_state);
Self {
ui_state,
info: widgets.info,
}
}
fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) {
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;
}
} }
} }
struct ClientWidgets { pub(crate) struct ClientWidgets {
info: WeakWidget<Text>, pub(crate) info: WeakWidget<Text>,
} }
fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) -> ClientWidgets pub(crate) fn build<Rsc: HasEvents>(
rsc: &mut Rsc,
ui_state: &mut impl HasRoot<Rsc>,
) -> ClientWidgets
where where
Rsc::State: FocusHost, Rsc::State: FocusHost,
{ {
-30
View File
@@ -1,30 +0,0 @@
use iris::prelude::*;
use std::time::Duration;
fn main() {
DesktopApp::<State>::run();
}
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> 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 }
}
}
+20
View File
@@ -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<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
}
+21
View File
@@ -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<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() {
DesktopApp::<State>::run();
}
+21
View File
@@ -0,0 +1,21 @@
use iris::prelude::*;
use std::time::Duration;
pub(crate) fn build<Rsc: HasEvents + HasTasks>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>)
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);
}
-49
View File
@@ -1,49 +0,0 @@
use iris::prelude::*;
fn main() {
DesktopApp::<State>::run();
}
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
type Rsc = StdRsc<State>;
#[derive(Clone, Copy, WidgetView)]
struct Test {
#[root]
root: WeakWidget<Rect>,
cur: WeakState<bool>,
}
impl Test {
pub fn new(rsc: &mut Rsc) -> Self {
let root = rect(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<Self>, _: Proxy<Self::Event>) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
test.toggle(rsc);
})
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
+20
View File
@@ -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<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
}
+21
View File
@@ -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<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() {
DesktopApp::<State>::run();
}
+36
View File
@@ -0,0 +1,36 @@
use iris::prelude::*;
#[derive(Clone, Copy, WidgetView)]
struct Test {
#[root]
root: WeakWidget<Rect>,
cur: WeakState<bool>,
}
impl Test {
pub fn new<State>(rsc: &mut StdRsc<State>) -> Self {
let root = rect(PaintId::RED).add(rsc);
let cur = rsc.create_state(root, false);
Self { root, cur }
}
pub fn toggle<State>(&self, rsc: &mut StdRsc<State>) {
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<State>(rsc: &mut StdRsc<State>, ui_state: &mut impl HasRoot<StdRsc<State>>)
where
State: FocusHost,
{
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {
test.toggle(rsc);
})
.set_root(rsc, ui_state);
}
+13 -3
View File
@@ -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. 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 ## Android applications
@@ -57,6 +59,14 @@ cargo iris apk --abi arm64-v8a
cargo iris run --abi x86_64 --device emulator-5554 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`, `cargo iris` packages directly with `cargo-ndk`, `javac`, `d8`, `aapt2`,
`jar`, `zipalign`, and `apksigner`; it does not require Gradle. The caller `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 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 APK staging and output live under
`target/iris-android/<package>/<debug|release>/<abi>/`; the command's final `target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; the
line is the verified APK's absolute path. command's final line is the verified APK's absolute path.
Goals, in general order: Goals, in general order:
1. does what I want it to (text, images, video, animations) 1. does what I want it to (text, images, video, animations)
+1
View File
@@ -27,6 +27,7 @@ mod access_tests;
#[cfg(test)] #[cfg(test)]
mod layout_tests; mod layout_tests;
pub use image;
pub use iris_core as core; pub use iris_core as core;
pub use iris_macro as macros; pub use iris_macro as macros;
pub use iris_macro::app_init; pub use iris_macro::app_init;
+5
View File
@@ -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 /// What a completed task nudges when it wants its result drawn. Shared
/// between backends rather than typed as `winit::window::Window` directly: /// between backends rather than typed as `winit::window::Window` directly:
/// android-view has no `Window` at all, and the redraw request there is a /// android-view has no `Window` at all, and the redraw request there is a