diff --git a/iris/Cargo.lock b/iris/Cargo.lock index a61b344..a84b717 100644 --- a/iris/Cargo.lock +++ b/iris/Cargo.lock @@ -1185,6 +1185,7 @@ dependencies = [ "pollster", "send_wrapper", "swash", + "tabs-ui", "tokio", "wgpu", "winit", @@ -2655,6 +2656,13 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "tabs-ui" +version = "0.1.0" +dependencies = [ + "iris", +] + [[package]] name = "termcolor" version = "1.4.1" diff --git a/iris/Cargo.toml b/iris/Cargo.toml index 683c4fd..6d307f9 100644 --- a/iris/Cargo.toml +++ b/iris/Cargo.toml @@ -42,9 +42,13 @@ send_wrapper = "0.6.0" [dev-dependencies] tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } +# The tabs example's widget tree. A dev-dependency cycle back to this +# package is fine -- cargo excludes dev-dependencies from the graph used +# to build the library itself, so this only matters for `--examples`. +tabs-ui = { path = "tabs-ui" } [workspace] -members = ["core", "macro"] +members = ["core", "macro", "tabs-ui"] [workspace.package] version = "0.1.0" diff --git a/iris/examples/tabs/main.rs b/iris/examples/tabs/main.rs index 6316924..1860264 100644 --- a/iris/examples/tabs/main.rs +++ b/iris/examples/tabs/main.rs @@ -1,13 +1,14 @@ -use std::{cell::RefCell, rc::Rc}; -use winit::event::WindowEvent; - use iris::prelude::*; -type ClientRsc = DefaultRsc; +use winit::event::WindowEvent; fn main() { DefaultApp::::run(); } +/// The tabs example: five demo panes plus a message composer, built by +/// `tabs_ui::build` and driven here through the winit backend. The same +/// widget tree also runs on the android-view backend, through +/// `iris-android-app` -- see RUST.md's I2. #[derive(DefaultUiState)] pub struct Client { ui_state: DefaultUiState, @@ -20,189 +21,11 @@ impl DefaultAppState for Client { rsc: &mut DefaultRsc, _: Proxy, ) -> Self { - let rrect = rect(Color::WHITE).radius(20); - let pad_test = ( - rrect.color(Color::BLUE), - ( - rrect - .color(Color::RED) - .sized((100, 100)) - .center() - .width(rest(2)), - ( - rrect.color(Color::ORANGE), - rrect.color(Color::LIME).pad(10.0), - ) - .span(Dir::RIGHT) - .width(rest(2)), - rrect.color(Color::YELLOW), - ) - .span(Dir::RIGHT) - .pad(10) - .width(rest(3)), - ) - .span(Dir::RIGHT) - .add(rsc); - - let span_test = ( - rrect.color(Color::GREEN).width(100), - rrect.color(Color::ORANGE), - rrect.color(Color::CYAN), - rrect.color(Color::BLUE).width(rel(0.5)), - rrect.color(Color::MAGENTA).width(100), - rrect.color(Color::RED).width(100), - ) - .span(Dir::LEFT) - .add(rsc); - - let span_add = Span::empty(Dir::RIGHT).add(rsc); - - let add_button = rect(Color::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(Color::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(Color::CYAN).sized((10, 10)).center(), - rect(Color::RED).sized((100, 100)).center(), - rect(Color::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().masked().background(rect(Color::SKY)); - let add_text = wtext("add") - .editable(EditMode::MultiLine) - .text_align(Align::LEFT) - .size(30) - .attr::(()) - .on(Submit, move |ctx, 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 msg_box = text - .background(rect(Color::WHITE.darker(0.5))) - .add_strong(rsc); - texts(rsc).push(msg_box); - }) - .add(rsc); - - let text_edit_scroll = ( - msg_area.height(rest(1)), - ( - Rect::new(Color::WHITE.darker(0.9)), - ( - add_text.width(rest(1)), - Rect::new(Color::GREEN) - .on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| { - 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 = |color, to: WeakWidget, label| { - 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 rect = rect(color) - .on(CursorSense::click(), move |ctx, rsc| { - let (prev, vec) = &mut *vals.borrow_mut(); - if let Some(h) = vec[i].take() { - vec[*prev] = main(rsc).replace(h); - *prev = i; - } - ctx.widget(rsc).color = color.darker(0.3); - }) - .on( - CursorSense::HoverStart | CursorSense::unclick(), - move |ctx, rsc| { - ctx.widget(rsc).color = color.brighter(0.2); - }, - ) - .on(CursorSense::HoverEnd, move |ctx, rsc| { - ctx.widget(rsc).color = color; - }); - (rect, wtext(label).size(30).text_align(Align::CENTER)).stack() - }; - - let tabs = ( - switch_button(Color::RED, pad_test, "pad"), - switch_button(Color::GREEN, span_test, "span"), - switch_button(Color::BLUE, span_add_test, "image span"), - switch_button(Color::MAGENTA, text_test, "text layout"), - switch_button( - Color::YELLOW.mul_rgb(0.5), - 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, &mut ui_state); - - Self { ui_state, info } + let widgets = tabs_ui::build(rsc, &mut ui_state); + Self { + ui_state, + info: widgets.info, + } } fn window_event( diff --git a/iris/tabs-ui/Cargo.toml b/iris/tabs-ui/Cargo.toml new file mode 100644 index 0000000..21805cb --- /dev/null +++ b/iris/tabs-ui/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tabs-ui" +version.workspace = true +edition.workspace = true + +# The tabs example's widget tree, factored out of iris/examples/tabs/main.rs +# so it can be built once and driven by either backend: the winit example +# binary, and iris/android-app's cdylib. Its own crate rather than a pub +# module of `iris` because it is demo content, not library surface -- see +# RUST.md's I2. + +[dependencies] +iris = { path = ".." } diff --git a/iris/examples/tabs/assets/sungals.png b/iris/tabs-ui/assets/sungals.png similarity index 100% rename from iris/examples/tabs/assets/sungals.png rename to iris/tabs-ui/assets/sungals.png diff --git a/iris/tabs-ui/src/lib.rs b/iris/tabs-ui/src/lib.rs new file mode 100644 index 0000000..11d3d6d --- /dev/null +++ b/iris/tabs-ui/src/lib.rs @@ -0,0 +1,205 @@ +//! The tabs example's widget tree -- the five demo panes plus the message +//! composer that exercises `TextEdit`. Factored out of +//! `iris/examples/tabs/main.rs` (I2, RUST.md) so the same UI runs under +//! both backends: the winit example binary calls `build` from +//! `DefaultAppState::new`, and `iris-android-app`'s cdylib calls it from +//! `AndroidAppState::new`. Nothing here mentions either backend by name -- +//! it only needs `Rsc: HasEvents` (for `.on(...)`) and `Rsc::State: +//! FocusHost` (for `.attr::(())`), both of which every backend +//! implements. + +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(Color::WHITE).radius(20); + let pad_test = ( + rrect.color(Color::BLUE), + ( + rrect + .color(Color::RED) + .sized((100, 100)) + .center() + .width(rest(2)), + ( + rrect.color(Color::ORANGE), + rrect.color(Color::LIME).pad(10.0), + ) + .span(Dir::RIGHT) + .width(rest(2)), + rrect.color(Color::YELLOW), + ) + .span(Dir::RIGHT) + .pad(10) + .width(rest(3)), + ) + .span(Dir::RIGHT) + .add(rsc); + + let span_test = ( + rrect.color(Color::GREEN).width(100), + rrect.color(Color::ORANGE), + rrect.color(Color::CYAN), + rrect.color(Color::BLUE).width(rel(0.5)), + rrect.color(Color::MAGENTA).width(100), + rrect.color(Color::RED).width(100), + ) + .span(Dir::LEFT) + .add(rsc); + + let span_add = Span::empty(Dir::RIGHT).add(rsc); + + let add_button = rect(Color::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(Color::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(Color::CYAN).sized((10, 10)).center(), + rect(Color::RED).sized((100, 100)).center(), + rect(Color::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().masked().background(rect(Color::SKY)); + let add_text = wtext("add") + .editable(EditMode::MultiLine) + .text_align(Align::LEFT) + .size(30) + .attr::(()) + .on(Submit, move |ctx, 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 msg_box = text + .background(rect(Color::WHITE.darker(0.5))) + .add_strong(rsc); + texts(rsc).push(msg_box); + }) + .add(rsc); + + let text_edit_scroll = ( + msg_area.height(rest(1)), + ( + Rect::new(Color::WHITE.darker(0.9)), + ( + add_text.width(rest(1)), + Rect::new(Color::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 = |color, to: WeakWidget, label| { + 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 rect = rect(color) + .on(CursorSense::click(), move |ctx, rsc| { + let (prev, vec) = &mut *vals.borrow_mut(); + if let Some(h) = vec[i].take() { + vec[*prev] = main(rsc).replace(h); + *prev = i; + } + ctx.widget(rsc).color = color.darker(0.3); + }) + .on( + CursorSense::HoverStart | CursorSense::unclick(), + move |ctx, rsc| { + ctx.widget(rsc).color = color.brighter(0.2); + }, + ) + .on(CursorSense::HoverEnd, move |ctx, rsc| { + ctx.widget(rsc).color = color; + }); + (rect, wtext(label).size(30).text_align(Align::CENTER)).stack() + }; + + let tabs = ( + switch_button(Color::RED, pad_test, "pad"), + switch_button(Color::GREEN, span_test, "span"), + switch_button(Color::BLUE, span_add_test, "image span"), + switch_button(Color::MAGENTA, text_test, "text layout"), + switch_button( + Color::YELLOW.mul_rgb(0.5), + 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 } +}