From e137f38a5dd92605cfc3be2fde549e3d16a904e9 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 11 Sep 2026 02:18:34 -0400 Subject: [PATCH] Return the tabs demo to one example --- AGENTS.md | 2 +- app/Cargo.lock | 8 - app/Cargo.toml | 5 +- app/build-apk.sh | 6 +- app/run-bench.sh | 2 +- app/src/android/devlog.rs | 25 --- app/src/android/mod.rs | 62 +----- docs/RUST.md | 2 +- iris/Cargo.lock | 8 - iris/Cargo.toml | 2 - .../tabs}/assets/sungals.png | Bin iris/examples/tabs/main.rs | 210 +++++++++++++++++- iris/tabs-ui/Cargo.toml | 9 - iris/tabs-ui/src/lib.rs | 207 ----------------- 14 files changed, 220 insertions(+), 328 deletions(-) rename iris/{tabs-ui => examples/tabs}/assets/sungals.png (100%) delete mode 100644 iris/tabs-ui/Cargo.toml delete mode 100644 iris/tabs-ui/src/lib.rs diff --git a/AGENTS.md b/AGENTS.md index 8312459..82d4385 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ The release signing key lives at `~/.config/ai-app/release.jks`, never in the checkout. `build-apk.sh` creates it once. Normal builds use application id `com.example.aiapp`; benchmark builds add `.bench` and are built explicitly: - ./build-apk.sh release --features "transcript-screen bench" + ./build-apk.sh release --features "screens bench" ## Running the server diff --git a/app/Cargo.lock b/app/Cargo.lock index 39eaae2..28b49dc 100644 --- a/app/Cargo.lock +++ b/app/Cargo.lock @@ -180,7 +180,6 @@ dependencies = [ "pulldown-cmark", "serde", "serde_json", - "tabs-ui", "tempfile", "tokio", "ureq", @@ -3611,13 +3610,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "tabs-ui" -version = "0.1.0" -dependencies = [ - "iris", -] - [[package]] name = "tempfile" version = "3.27.0" diff --git a/app/Cargo.toml b/app/Cargo.toml index 3efc076..bd3804d 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -34,7 +34,6 @@ log = { version = "0.4.34", features = ["std"] } # UI dependencies stay optional so client-only tests do not link the renderer. iris = { path = "../iris", optional = true } -tabs-ui = { path = "../iris/tabs-ui", optional = true } libc = { version = "0.2.189", optional = true } tokio = { version = "1.53.1", features = ["rt", "time"], optional = true } @@ -51,9 +50,7 @@ default = ["screens", "fixture"] screens = ["dep:iris"] # Default-on for tests; APK builds opt in so ordinary APKs omit the 1.9 MB fixture. fixture = ["screens"] -transcript-screen = ["screens"] -tabs-screen = ["screens", "dep:tabs-ui"] -bench = ["transcript-screen", "fixture", "dep:libc", "dep:tokio"] +bench = ["screens", "fixture", "dep:libc", "dep:tokio"] force-gles = ["screens", "iris/force-gles"] [dev-dependencies] diff --git a/app/build-apk.sh b/app/build-apk.sh index 1fb7355..9b031a5 100755 --- a/app/build-apk.sh +++ b/app/build-apk.sh @@ -7,15 +7,15 @@ # emulator stays on debug" rule -- pass `release` explicitly for a phone # build). --abi defaults to arm64-v8a (a phone/real device); pass # x86_64 for this checkout's own AVD. --features defaults to -# "transcript-screen". Pass "transcript-screen bench" for the retained -# benchmark app. Never force GLES for a phone or emulator build; Iris's +# "screens". Pass "screens bench" for the retained benchmark app. Never +# force GLES for a phone or emulator build; Iris's # runtime selects the available hardware backend. set -eu cd "$(dirname "$0")" BUILD_TYPE="debug" ABI="arm64-v8a" -FEATURES="transcript-screen" +FEATURES="screens" case "${1:-}" in debug|release) BUILD_TYPE="$1"; shift ;; esac diff --git a/app/run-bench.sh b/app/run-bench.sh index b489478..113eed8 100755 --- a/app/run-bench.sh +++ b/app/run-bench.sh @@ -9,7 +9,7 @@ # (android-project/app/build/outputs/apk/release/app-release.apk) if it # exists, else the # debug one. Build it first with: -# ./build-apk.sh release --features "transcript-screen bench" +# ./build-apk.sh release --features "screens bench" set -eu cd "$(dirname "$0")" diff --git a/app/src/android/devlog.rs b/app/src/android/devlog.rs index 420e817..26fb6d9 100644 --- a/app/src/android/devlog.rs +++ b/app/src/android/devlog.rs @@ -4,14 +4,9 @@ use android_view::jni::JNIEnv; use android_view::jni::objects::{JClass, JObject, JString}; use android_view::jni::sys::{jlong, jobjectArray}; -#[cfg(feature = "transcript-screen")] use std::path::Path; use std::{ptr, sync::OnceLock}; -/// 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 -/// build (`iris-android-app` without `transcript-screen`). -#[cfg(feature = "transcript-screen")] const FIELDS_PER_LINE: usize = 5; /// The authority the provider registered itself under, once it has been @@ -43,12 +38,9 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeReady( authority: JString, files_dir: JString, ) { - #[cfg(feature = "transcript-screen")] if let Some(dir) = string_arg(&mut env, &files_dir) { crate::android::app_log::set_crash_dir(Path::new(&dir)); } - #[cfg(not(feature = "transcript-screen"))] - let _ = &files_dir; let Some(authority) = string_arg(&mut env, &authority) else { return; }; @@ -95,7 +87,6 @@ pub extern "system" fn Java_dev_iris_android_demo_DevLogProvider_nativeLinesSinc string_array(&mut env, &line_fields(since.max(0) as u64)) } -#[cfg(feature = "transcript-screen")] fn status_fields() -> Vec { let ring = crate::client::log_ring::process_ring(); vec![ @@ -105,17 +96,6 @@ fn status_fields() -> Vec { ] } -/// The tabs demo links no `client-core` and keeps no ring, so it holds -/// nothing and has never dropped anything -- which is the truth, not a -/// stand-in. The natives are still exported there, because a `native` -/// method Java declares and the library does not is an -/// `UnsatisfiedLinkError` the moment the class loads. -#[cfg(not(feature = "transcript-screen"))] -fn status_fields() -> Vec { - vec!["0".to_string(), "0".to_string(), "-1".to_string()] -} - -#[cfg(feature = "transcript-screen")] fn line_fields(since: u64) -> Vec { let (lines, _next) = crate::client::log_ring::process_ring().since(since); let mut fields = Vec::with_capacity(lines.len() * FIELDS_PER_LINE); @@ -129,11 +109,6 @@ fn line_fields(since: u64) -> Vec { fields } -#[cfg(not(feature = "transcript-screen"))] -fn line_fields(_since: u64) -> Vec { - Vec::new() -} - /// Null rather than a panic across the JNI boundary: `DevLogProvider` /// reads it as "the provider could not answer" and returns no cursor, /// which Dev Updater already draws as a distinct state. Taking the app diff --git a/app/src/android/mod.rs b/app/src/android/mod.rs index d570a0b..997fcf2 100644 --- a/app/src/android/mod.rs +++ b/app/src/android/mod.rs @@ -7,61 +7,26 @@ use android_view::{ }, register_view_class, }; -#[cfg(not(feature = "transcript-screen"))] -use iris::{ - android::{AndroidAppState, AndroidUiState}, - macros::AndroidUiState, - rsc::StdRsc, -}; use log::LevelFilter; use std::ffi::c_void; -#[cfg(feature = "transcript-screen")] use std::path::{Path, PathBuf}; -/// 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 -/// tabs demo keeps `android_logger` alone, as it always had. -#[cfg(feature = "transcript-screen")] +/// The app's own log ring and its upload. mod app_log; #[cfg(feature = "bench")] mod bench_client; #[cfg(feature = "bench")] mod bench_jni; mod devlog; -#[cfg(feature = "transcript-screen")] mod enrollment; -#[cfg(all(feature = "transcript-screen", not(feature = "bench")))] +#[cfg(not(feature = "bench"))] mod transcript_client; /// The app's `View` subclass, matching the Java side's package -- /// `app/src/main/java/dev/iris/android/demo/IrisView.java`. const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView"; -#[cfg(not(feature = "transcript-screen"))] -#[derive(AndroidUiState)] -pub struct Client { - ui_state: AndroidUiState, -} - -#[cfg(not(feature = "transcript-screen"))] -impl AndroidAppState for Client { - fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { - // `widgets.info` is the winit example's frame-debug readout, kept - // current from `DesktopAppState::window_event` -- android-view has - // no per-frame hook to drive the equivalent from here yet, so it - // is left at its built "" text rather than wired to nothing. - let _ = tabs_ui::build(rsc, &mut ui_state); - Self { ui_state } - } - - fn back_pressed(&mut self, _rsc: &mut StdRsc) -> bool { - false - } -} - -#[cfg(not(feature = "transcript-screen"))] -type ActiveClient = Client; -#[cfg(all(feature = "transcript-screen", not(feature = "bench")))] +#[cfg(not(feature = "bench"))] type ActiveClient = transcript_client::TranscriptClient; #[cfg(feature = "bench")] type ActiveClient = bench_client::BenchClient; @@ -79,18 +44,7 @@ extern "system" fn new_view_peer<'local>( /// mirrors android-view's own demo, which carries the same comment. #[unsafe(no_mangle)] pub unsafe extern "system" fn JNI_OnLoad(vm: *mut RawJavaVM, _: *mut c_void) -> jint { - // The ring in front of `android_logger` where there is one (see - // `app_log`), and `android_logger` alone otherwise. Both install the - // same tag and level, so `logcat` cannot tell the two builds apart -- - // the ring only adds a second reader. - #[cfg(feature = "transcript-screen")] app_log::install(LevelFilter::Debug); - #[cfg(not(feature = "transcript-screen"))] - android_logger::init_once( - android_logger::Config::default() - .with_max_level(LevelFilter::Debug) - .with_tag("iris-android-app"), - ); let vm = unsafe { JavaVM::from_raw(vm) }.unwrap(); let mut env = vm.get_env().unwrap(); register_view_class(&mut env, VIEW_CLASS, new_view_peer); @@ -118,11 +72,8 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeSetFilesDir let Some(dir) = jstring(&mut env, dir) else { return; }; - #[cfg(feature = "transcript-screen")] - { - app_log::set_crash_dir(Path::new(&dir)); - enrollment::set_files_dir(PathBuf::from(&dir)); - } + app_log::set_crash_dir(Path::new(&dir)); + enrollment::set_files_dir(PathBuf::from(&dir)); log::debug!("iris app: files directory is {dir}"); } @@ -137,15 +88,12 @@ pub extern "system" fn Java_dev_iris_android_demo_MainActivity_nativeEnroll( let Some(uri) = jstring(&mut env, uri) else { return; }; - #[cfg(feature = "transcript-screen")] match enrollment::apply_link(&uri) { // Never the token: `wg-app-link`'s enroll module forbids logging // it, and this line would otherwise be the one place it leaked. Ok(server) => log::info!("iris app: enrolled with {}:{}", server.host, server.port), Err(error) => log::warn!("iris app: that enrolment link was refused -- {error}"), } - #[cfg(not(feature = "transcript-screen"))] - log::warn!("iris app: {uri} arrived, but this build has no server to enrol with"); } fn jstring(env: &mut JNIEnv, value: JString) -> Option { diff --git a/docs/RUST.md b/docs/RUST.md index 200a43f..6681a05 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -26,7 +26,7 @@ open work and measured constraints that are expensive to rediscover. The application crate has an rlib for tests and desktop and a cdylib for Android. Features select optional surfaces: `screens` links Iris, `fixture` embeds the synthetic transcript, and `bench` adds the benchmark runner. An -ordinary APK uses `transcript-screen`, not `bench`. +ordinary APK uses `screens`; only a benchmark build adds `bench`. ## Current state diff --git a/iris/Cargo.lock b/iris/Cargo.lock index 9a47f77..f22b561 100644 --- a/iris/Cargo.lock +++ b/iris/Cargo.lock @@ -1550,7 +1550,6 @@ dependencies = [ "pollster", "send_wrapper", "swash", - "tabs-ui", "tokio", "wgpu", "winit", @@ -3316,13 +3315,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "tabs-ui" -version = "0.1.0" -dependencies = [ - "iris", -] - [[package]] name = "tempfile" version = "3.25.0" diff --git a/iris/Cargo.toml b/iris/Cargo.toml index 763ac68..6c63e9b 100644 --- a/iris/Cargo.toml +++ b/iris/Cargo.toml @@ -36,7 +36,6 @@ force-gles = [] [dev-dependencies] tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } -tabs-ui = { path = "tabs-ui" } bytemuck = { workspace = true } [[bench]] @@ -47,7 +46,6 @@ harness = false members = [ "core", "macro", - "tabs-ui", "rig-input", ] diff --git a/iris/tabs-ui/assets/sungals.png b/iris/examples/tabs/assets/sungals.png similarity index 100% rename from iris/tabs-ui/assets/sungals.png rename to iris/examples/tabs/assets/sungals.png diff --git a/iris/examples/tabs/main.rs b/iris/examples/tabs/main.rs index 151168c..13ce661 100644 --- a/iris/examples/tabs/main.rs +++ b/iris/examples/tabs/main.rs @@ -1,4 +1,5 @@ use iris::prelude::*; +use std::{cell::RefCell, rc::Rc}; use winit::event::WindowEvent; fn main() { @@ -6,14 +7,14 @@ fn main() { } #[derive(DesktopUiState)] -pub struct Client { +struct Client { ui_state: DesktopUiState, info: WeakWidget, } impl DesktopAppState for Client { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { - let widgets = tabs_ui::build(rsc, &mut ui_state); + let widgets = build(rsc, &mut ui_state); Self { ui_state, info: widgets.info, @@ -33,3 +34,208 @@ impl DesktopAppState for Client { } } } + +struct ClientWidgets { + info: WeakWidget, +} + +fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets +where + Rsc::State: FocusHost, +{ + let rrect = rect(PaintId::WHITE).radius(20); + let pad_test = ( + rrect.clone().color(PaintId::BLUE), + ( + rrect + .clone() + .color(PaintId::RED) + .sized((100, 100)) + .center() + .width(rest(2)), + ( + rrect.clone().color(PaintId::ORANGE), + rrect.clone().color(PaintId::LIME).pad(10.0), + ) + .span(Dir::RIGHT) + .width(rest(2)), + rrect.clone().color(PaintId::YELLOW), + ) + .span(Dir::RIGHT) + .pad(10) + .width(rest(3)), + ) + .span(Dir::RIGHT) + .add(rsc); + + let span_test = ( + rrect.clone().color(PaintId::GREEN).width(100), + rrect.clone().color(PaintId::ORANGE), + rrect.clone().color(PaintId::CYAN), + rrect.clone().color(PaintId::BLUE).width(rel(0.5)), + rrect.clone().color(PaintId::MAGENTA).width(100), + rrect.color(PaintId::RED).width(100), + ) + .span(Dir::LEFT) + .add(rsc); + + let span_add = Span::empty(Dir::RIGHT).add(rsc); + + let add_button = rect(PaintId::LIME) + .radius(30) + .on(CursorSense::click(), move |_, rsc| { + let child = image(include_bytes!("assets/sungals.png")) + .center() + .add_strong(rsc); + span_add(rsc).push(child); + }) + .sized((150, 150)) + .align(Align::BOT_RIGHT); + + let del_button = rect(PaintId::RED) + .radius(30) + .on(CursorSense::click(), move |_, rsc| { + span_add(rsc).pop(); + }) + .sized((150, 150)) + .align(Align::BOT_LEFT); + + let span_add_test = (span_add, add_button, del_button).stack().add(rsc); + + let btext = |content| wtext(content).size(30); + + let text_test = ( + btext("this is a").align(Align::LEFT), + btext("teeeeeeeest").align(Align::RIGHT), + btext("okkk\nokkkkkk!").align(Align::LEFT), + btext("hmm"), + btext("a"), + ( + btext("'").family(Family::Monospace).align(Align::TOP), + btext("'").family(Family::Monospace), + btext(":gamer mode").family(Family::Monospace), + rect(PaintId::CYAN).sized((10, 10)).center(), + rect(PaintId::RED).sized((100, 100)).center(), + rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP), + ) + .span(Dir::RIGHT) + .center(), + wtext("pretty cool right?").size(50), + ) + .span(Dir::DOWN) + .add(rsc); + + let texts = Span::empty(Dir::DOWN).gap(10).add(rsc); + let msg_area = texts + .scrollable(Axis::Y, Pin::Start) + .masked() + .background(rect(PaintId::SKY)); + let add_text = wtext("add") + .editable(EditMode::MultiLine) + .text_align(Align::LEFT) + .size(30) + .attr::(()) + .on(Submit, move |ctx, rsc: &mut Rsc| { + let w = ctx.widget; + let content = w.edit(rsc).take(); + let text = wtext(content) + .editable(EditMode::MultiLine) + .size(30) + .text_align(Align::LEFT) + .wrap(true) + .attr::(()); + let fill = rsc + .ui_mut() + .paints + .add(Srgba8::WHITE.to_linear().darker(0.5)); + let msg_box = text.background(rect(fill)).add_strong(rsc); + texts(rsc).push(msg_box); + }) + .add(rsc); + + let text_edit_scroll = ( + msg_area.height(rest(1)), + ( + Rect::new( + rsc.ui_mut() + .paints + .add(Srgba8::WHITE.to_linear().darker(0.9)), + ), + ( + add_text.width(rest(1)), + Rect::new(PaintId::GREEN) + .on(CursorSense::click(), move |ctx, rsc: &mut Rsc| { + rsc.run_event::(add_text, (), ctx.state); + }) + .sized((40, 40)), + ) + .span(Dir::RIGHT) + .pad(10), + ) + .stack() + .size(StackSize::Child(1)) + .layer_offset(1) + .align(Align::BOT), + ) + .span(Dir::DOWN) + .add(rsc); + + let main = WidgetPtr::new().add(rsc); + + let vals = Rc::new(RefCell::new((0, Vec::new()))); + let mut switch_button = |solid: Srgba8, to: WeakWidget, label| { + let value = solid.to_linear(); + let paint = rsc.ui_mut().paints.add(value); + let to = to.upgrade(rsc); + let vec = &mut vals.borrow_mut().1; + let i = vec.len(); + if vec.is_empty() { + vec.push(None); + main(rsc).set(to); + } else { + vec.push(Some(to)); + } + let vals = vals.clone(); + let pressed = paint.clone(); + let hovered = paint.clone(); + let normal = paint.clone(); + let rect = rect(paint) + .on(CursorSense::click(), move |_ctx, rsc: &mut Rsc| { + let (prev, vec) = &mut *vals.borrow_mut(); + if let Some(h) = vec[i].take() { + vec[*prev] = main(rsc).replace(h); + *prev = i; + } + rsc.ui_mut().paints.set(&pressed, value.darker(0.3)); + }) + .on( + CursorSense::HoverStart | CursorSense::unclick(), + move |_ctx, rsc: &mut Rsc| { + rsc.ui_mut().paints.set(&hovered, value.brighter(0.2)); + }, + ) + .on(CursorSense::HoverEnd, move |_ctx, rsc: &mut Rsc| { + rsc.ui_mut().paints.set(&normal, value); + }) + .label(label); + (rect, wtext(label).size(30).text_align(Align::CENTER)).stack() + }; + + let tabs = ( + switch_button(Srgba8::RED, pad_test, "pad"), + switch_button(Srgba8::GREEN, span_test, "span"), + switch_button(Srgba8::BLUE, span_add_test, "image span"), + switch_button(Srgba8::MAGENTA, text_test, "text layout"), + switch_button(Srgba8::YELLOW, text_edit_scroll, "text edit scroll"), + ) + .span(Dir::RIGHT); + + let info = wtext("").add(rsc); + let info_sect = info.pad(10).align(Align::RIGHT); + + ((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect) + .stack() + .set_root(rsc, ui_state); + + ClientWidgets { info } +} diff --git a/iris/tabs-ui/Cargo.toml b/iris/tabs-ui/Cargo.toml deleted file mode 100644 index a032c7b..0000000 --- a/iris/tabs-ui/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "tabs-ui" -version.workspace = true -edition.workspace = true - -# Shared demo content for the desktop and Android example entry points. - -[dependencies] -iris = { path = ".." } diff --git a/iris/tabs-ui/src/lib.rs b/iris/tabs-ui/src/lib.rs deleted file mode 100644 index 0e92873..0000000 --- a/iris/tabs-ui/src/lib.rs +++ /dev/null @@ -1,207 +0,0 @@ -use iris::prelude::*; -use std::{cell::RefCell, rc::Rc}; - -pub struct ClientWidgets { - pub info: WeakWidget, -} - -pub fn build(rsc: &mut Rsc, ui_state: &mut impl HasRoot) -> ClientWidgets -where - Rsc::State: FocusHost, -{ - let rrect = rect(PaintId::WHITE).radius(20); - let pad_test = ( - rrect.clone().color(PaintId::BLUE), - ( - rrect - .clone() - .color(PaintId::RED) - .sized((100, 100)) - .center() - .width(rest(2)), - ( - rrect.clone().color(PaintId::ORANGE), - rrect.clone().color(PaintId::LIME).pad(10.0), - ) - .span(Dir::RIGHT) - .width(rest(2)), - rrect.clone().color(PaintId::YELLOW), - ) - .span(Dir::RIGHT) - .pad(10) - .width(rest(3)), - ) - .span(Dir::RIGHT) - .add(rsc); - - let span_test = ( - rrect.clone().color(PaintId::GREEN).width(100), - rrect.clone().color(PaintId::ORANGE), - rrect.clone().color(PaintId::CYAN), - rrect.clone().color(PaintId::BLUE).width(rel(0.5)), - rrect.clone().color(PaintId::MAGENTA).width(100), - rrect.color(PaintId::RED).width(100), - ) - .span(Dir::LEFT) - .add(rsc); - - let span_add = Span::empty(Dir::RIGHT).add(rsc); - - let add_button = rect(PaintId::LIME) - .radius(30) - .on(CursorSense::click(), move |_, rsc| { - let child = image(include_bytes!("../assets/sungals.png")) - .center() - .add_strong(rsc); - span_add(rsc).push(child); - }) - .sized((150, 150)) - .align(Align::BOT_RIGHT); - - let del_button = rect(PaintId::RED) - .radius(30) - .on(CursorSense::click(), move |_, rsc| { - span_add(rsc).pop(); - }) - .sized((150, 150)) - .align(Align::BOT_LEFT); - - let span_add_test = (span_add, add_button, del_button).stack().add(rsc); - - let btext = |content| wtext(content).size(30); - - let text_test = ( - btext("this is a").align(Align::LEFT), - btext("teeeeeeeest").align(Align::RIGHT), - btext("okkk\nokkkkkk!").align(Align::LEFT), - btext("hmm"), - btext("a"), - ( - btext("'").family(Family::Monospace).align(Align::TOP), - btext("'").family(Family::Monospace), - btext(":gamer mode").family(Family::Monospace), - rect(PaintId::CYAN).sized((10, 10)).center(), - rect(PaintId::RED).sized((100, 100)).center(), - rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP), - ) - .span(Dir::RIGHT) - .center(), - wtext("pretty cool right?").size(50), - ) - .span(Dir::DOWN) - .add(rsc); - - let texts = Span::empty(Dir::DOWN).gap(10).add(rsc); - let msg_area = texts - .scrollable(Axis::Y, Pin::Start) - .masked() - .background(rect(PaintId::SKY)); - let add_text = wtext("add") - .editable(EditMode::MultiLine) - .text_align(Align::LEFT) - .size(30) - .attr::(()) - .on(Submit, move |ctx, rsc: &mut Rsc| { - let w = ctx.widget; - let content = w.edit(rsc).take(); - let text = wtext(content) - .editable(EditMode::MultiLine) - .size(30) - .text_align(Align::LEFT) - .wrap(true) - .attr::(()); - let fill = rsc - .ui_mut() - .paints - .add(Srgba8::WHITE.to_linear().darker(0.5)); - let msg_box = text.background(rect(fill)).add_strong(rsc); - texts(rsc).push(msg_box); - }) - .add(rsc); - - let text_edit_scroll = ( - msg_area.height(rest(1)), - ( - Rect::new( - rsc.ui_mut() - .paints - .add(Srgba8::WHITE.to_linear().darker(0.9)), - ), - ( - add_text.width(rest(1)), - Rect::new(PaintId::GREEN) - .on(CursorSense::click(), move |ctx, rsc: &mut Rsc| { - rsc.run_event::(add_text, (), ctx.state); - }) - .sized((40, 40)), - ) - .span(Dir::RIGHT) - .pad(10), - ) - .stack() - .size(StackSize::Child(1)) - .layer_offset(1) - .align(Align::BOT), - ) - .span(Dir::DOWN) - .add(rsc); - - let main = WidgetPtr::new().add(rsc); - - let vals = Rc::new(RefCell::new((0, Vec::new()))); - let mut switch_button = |solid: Srgba8, to: WeakWidget, label| { - let value = solid.to_linear(); - let paint = rsc.ui_mut().paints.add(value); - let to = to.upgrade(rsc); - let vec = &mut vals.borrow_mut().1; - let i = vec.len(); - if vec.is_empty() { - vec.push(None); - main(rsc).set(to); - } else { - vec.push(Some(to)); - } - let vals = vals.clone(); - let pressed = paint.clone(); - let hovered = paint.clone(); - let normal = paint.clone(); - let rect = rect(paint) - .on(CursorSense::click(), move |_ctx, rsc: &mut Rsc| { - let (prev, vec) = &mut *vals.borrow_mut(); - if let Some(h) = vec[i].take() { - vec[*prev] = main(rsc).replace(h); - *prev = i; - } - rsc.ui_mut().paints.set(&pressed, value.darker(0.3)); - }) - .on( - CursorSense::HoverStart | CursorSense::unclick(), - move |_ctx, rsc: &mut Rsc| { - rsc.ui_mut().paints.set(&hovered, value.brighter(0.2)); - }, - ) - .on(CursorSense::HoverEnd, move |_ctx, rsc: &mut Rsc| { - rsc.ui_mut().paints.set(&normal, value); - }) - .label(label); - (rect, wtext(label).size(30).text_align(Align::CENTER)).stack() - }; - - let tabs = ( - switch_button(Srgba8::RED, pad_test, "pad"), - switch_button(Srgba8::GREEN, span_test, "span"), - switch_button(Srgba8::BLUE, span_add_test, "image span"), - switch_button(Srgba8::MAGENTA, text_test, "text layout"), - switch_button(Srgba8::YELLOW, text_edit_scroll, "text edit scroll"), - ) - .span(Dir::RIGHT); - - let info = wtext("").add(rsc); - let info_sect = info.pad(10).align(Align::RIGHT); - - ((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect) - .stack() - .set_root(rsc, ui_state); - - ClientWidgets { info } -}