use std::any::Any;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use skia_safe::Color;
use crate::animators::{self, AnimationId, FrameTick, ValueAnimator, easing};
use crate::control::{Control, GestureCx, Handled, Has, part_mut};
use crate::controls::backdrop::{BackdropBuild, SkiaBackdrop};
use crate::controls::button::{ButtonBuild, ButtonSet, SkiaButton};
use crate::controls::drawer::{DrawerBuild, DrawerDirection, DrawerSet, SkiaDrawer};
use crate::controls::label::{LabelBuild, SkiaLabel, TextAlignment};
use crate::controls::layout::{GridLength, LayoutBuild, SkiaGrid, SkiaLayer, SkiaLayout};
use crate::controls::rich_label::SkiaRichLabel;
use crate::controls::shape::{ShapeBuild, SkiaShape};
use crate::controls::snapping_layout::{SnappingBuild, SnappingSet};
use crate::gestures::{Gesture, GestureKind};
use crate::props;
use crate::tree::{Build, ControlId, Cx, Detached, Handle, IntoChildren, Mut, Raw, Tree, wrong_state};
use crate::types::{CacheType, IntoProp, LayoutOptions, Thickness};
use crate::ui::{Aria, HistoryOp};
const fn rgb(v: u32) -> Color {
Color::new(0xFF00_0000 | v)
}
const PAGE: Color = rgb(0x212529);
const ACCENT: Color = rgb(0x6EA8FE);
const LINE: Color = rgb(0x343A40);
fn tabs_easing(x: f32) -> f32 {
(x - 1.0) * (x - 1.0) * ((0.55 + 1.0) * (x - 1.0) + 0.55) + 1.0
}
const TOAST_IN_MS: f32 = 300.0;
const TOAST_OUT_MS: f32 = 250.0;
const REQUESTS_PER_FRAME: usize = 32;
pub type ShellArguments = Vec<(String, String)>;
pub fn split_route(route: &str) -> (&str, ShellArguments) {
let Some((name, query)) = route.split_once('?') else { return (route, Vec::new()) };
let mut arguments = Vec::new();
for pair in query.split('&').filter(|p| !p.is_empty()) {
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
set_argument(&mut arguments, decode(key, true), decode(value, true));
}
(name, arguments)
}
pub fn build_route(name: &str, arguments: &[(String, String)]) -> String {
let mut route = name.to_owned();
for (i, (key, value)) in arguments.iter().enumerate() {
route.push(if i == 0 { '?' } else { '&' });
encode(&mut route, key, true);
route.push('=');
encode(&mut route, value, true);
}
route
}
fn set_argument(arguments: &mut ShellArguments, key: String, value: String) {
match arguments.iter_mut().find(|(k, _)| *k == key) {
Some((_, v)) => *v = value,
None => arguments.push((key, value)),
}
}
fn encode(out: &mut String, text: &str, form: bool) {
for b in text.bytes() {
match b {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_' => out.push(b as char),
b'!' | b'~' | b'\'' | b'(' | b')' if !form => out.push(b as char),
b' ' if form => out.push('+'),
_ => out.push_str(&format!("%{b:02X}")),
}
}
}
fn decode(text: &str, form: bool) -> String {
let (bytes, mut out, mut i) = (text.as_bytes(), Vec::with_capacity(text.len()), 0);
while i < bytes.len() {
let hex = |at: usize| bytes.get(at).and_then(|b| (*b as char).to_digit(16));
match bytes[i] {
b'+' if form => out.push(b' '),
b'%' if hex(i + 1).is_some() && hex(i + 2).is_some() => {
out.push((hex(i + 1).unwrap_or(0) * 16 + hex(i + 2).unwrap_or(0)) as u8);
i += 2;
}
b => out.push(b),
}
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PopupOptions {
pub animated: bool,
pub close_when_background_tapped: bool,
pub show_overlay: bool,
pub background_color: Option<Color>,
}
impl Default for PopupOptions {
fn default() -> Self {
Self { animated: true, close_when_background_tapped: true, show_overlay: true, background_color: None }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ModalOptions {
pub use_gestures: bool,
pub animated: bool,
pub freeze_background: bool,
}
impl Default for ModalOptions {
fn default() -> Self {
Self { use_gestures: false, animated: true, freeze_background: true }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavigationSource {
Push,
Pop,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShellNavigatingArgs {
pub route: String,
pub source: NavigationSource,
pub cancel: bool,
pub view: Option<ControlId>,
pub previous: Option<ControlId>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShellNavigatedArgs {
pub route: String,
pub source: NavigationSource,
pub view: Option<ControlId>,
}
type Factory = Rc<dyn Fn(&mut dyn Any, &ShellArguments) -> Detached>;
type NavigatingHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &mut ShellNavigatingArgs)>;
type NavigatedHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &ShellNavigatedArgs)>;
type RouteChangedHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>, &str)>;
type ChangedHandler = Box<dyn FnMut(Raw<'_>, &mut dyn Any, &mut Cx<'_>)>;
props!(ShellProps, ShellBuild, ShellSet {
nav_bar_height / set_nav_bar_height: f32 = 56.0, NONE;
nav_bar_color / set_nav_bar_color: Color = PAGE, NONE;
pages_animation_speed / set_pages_animation_speed: f32 = 200.0, NONE;
page_background_color / set_page_background_color: Color = PAGE, NONE;
tab_bar_height / set_tab_bar_height: f32 = 56.0, NONE;
tab_bar_color / set_tab_bar_color: Color = PAGE, NONE;
animate_tabs / set_animate_tabs: bool = false, NONE;
tabs_animation_speed / set_tabs_animation_speed: f32 = 150.0, NONE;
insets / set_insets: Option<Thickness> = None, APPLY;
use_browser_history / set_use_browser_history: bool = true, NONE;
});
impl IntoProp<Option<Thickness>> for Thickness {
fn into_prop(self) -> Option<Thickness> {
Some(self)
}
}
#[derive(Clone, Copy)]
struct Parts {
pages: ControlId,
modals: ControlId,
popups: ControlId,
toasts: ControlId,
}
struct Stack {
layer: ControlId,
root: ControlId,
pages: Vec<Page>,
}
struct Page {
route: String,
arguments: ShellArguments,
host: ControlId,
slide: Option<AnimationId>,
popping: bool,
}
struct Overlay {
wrapper: ControlId,
content: ControlId,
animation: Option<AnimationId>,
closing: bool,
animated: bool,
history: bool,
}
enum Request {
Start,
GoTo { route: String, animated: bool, arguments: ShellArguments },
GoBack { animated: bool },
PopToRoot,
PopTabToRoot,
SelectTab(usize),
OpenPopup { content: Detached, options: PopupOptions },
ClosePopup { wrapper: Option<ControlId>, animated: bool },
CloseAllPopups,
PushModal { content: Detached, options: ModalOptions },
PopModal { animated: bool },
ShowToast { content: Detached, ms: f32 },
CloseAllToasts,
HistoryMoved { depth: u32, hash: String },
Done(Done),
}
#[derive(Clone, Copy)]
enum Done {
Pushed(ControlId),
Popped(ControlId),
TabShown(usize),
PopupOpened(ControlId),
PopupClosed(ControlId),
ModalOpened(ControlId),
ModalClosed(ControlId),
ToastDue(ControlId),
ToastClosed(ControlId),
}
pub struct SkiaShell {
layout: SkiaLayout,
pub p: ShellProps,
me: ControlId,
routes: Vec<(String, Factory)>,
titles: Vec<(String, String)>,
tabs: Vec<(String, String)>,
root: Vec<Detached>,
parts: Option<Parts>,
stacks: Vec<Option<Stack>>,
selected: usize,
tab_switch: Option<(AnimationId, usize)>,
tab_bar: Vec<(Handle<SkiaButton>, ControlId)>,
popups: Vec<Overlay>,
modals: Vec<Overlay>,
toasts: Vec<Overlay>,
requests: VecDeque<Request>,
ticking: bool,
route_seen: Option<String>,
history_on: bool,
history: Vec<HistoryKind>,
applied_insets: Option<Thickness>,
tab_bar_id: Option<ControlId>,
on_navigating: Option<NavigatingHandler>,
on_navigated: Option<NavigatedHandler>,
on_route_changed: Option<RouteChangedHandler>,
on_changed: Option<ChangedHandler>,
}
impl SkiaShell {
pub const TOAST_BACKGROUND_COLOR: Color = Color::new(0xCC00_0000);
pub const TOAST_TEXT_COLOR: Color = Color::WHITE;
pub const TOAST_TEXT_SIZE: f32 = 16.0;
pub const TOAST_TEXT_MARGINS: f32 = 24.0;
pub const POPUP_BACKGROUND_COLOR: Color = Color::new(0x6600_0000);
pub const POPUPS_BACKGROUND_BLUR: f32 = 6.0;
pub const POPUPS_ANIMATION_SPEED: f32 = 250.0;
pub const TAB_SELECTED_COLOR: Color = ACCENT;
pub const TAB_COLOR: Color = rgb(0xADB5BD);
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Build<SkiaShell> {
let shell = SkiaShell {
layout: SkiaLayout::default(),
p: ShellProps::default(),
me: Handle::<SkiaShell>::default().id(),
routes: Vec::new(),
titles: Vec::new(),
tabs: Vec::new(),
root: Vec::new(),
parts: None,
stacks: Vec::new(),
selected: 0,
tab_switch: None,
tab_bar: Vec::new(),
popups: Vec::new(),
modals: Vec::new(),
toasts: Vec::new(),
requests: VecDeque::new(),
ticking: false,
route_seen: None,
history_on: false,
history: Vec::new(),
applied_insets: None,
tab_bar_id: None,
on_navigating: None,
on_navigated: None,
on_route_changed: None,
on_changed: None,
};
let mut build = Build::new(shell).fill();
let id = build.id();
build.control_mut().me = id;
build
}
fn stack(&self) -> Option<&Stack> {
self.stacks.get(self.selected)?.as_ref()
}
pub fn route(&self) -> &str {
self.stack().and_then(|s| s.pages.last()).map_or("", |p| &p.route)
}
pub fn arguments(&self) -> &[(String, String)] {
self.stack().and_then(|s| s.pages.last()).map_or(&[], |p| &p.arguments)
}
pub fn navigation_stack(&self) -> impl Iterator<Item = &str> {
self.stack().into_iter().flat_map(|s| s.pages.iter().map(|p| p.route.as_str()))
}
pub fn can_go_back(&self) -> bool {
self.stack().is_some_and(|s| !s.pages.is_empty()) || !self.popups.is_empty() || !self.modals.is_empty()
}
pub fn popups_count(&self) -> usize {
self.popups.len()
}
pub fn modals_count(&self) -> usize {
self.modals.len()
}
pub fn toasts_count(&self) -> usize {
self.toasts.len()
}
pub fn selected_tab(&self) -> usize {
self.selected
}
fn title(&self, name: &str) -> String {
self.titles.iter().find(|(r, _)| r == name).map_or(name, |(_, t)| t.as_str()).to_owned()
}
fn factory(&self, name: &str) -> Option<Factory> {
self.routes.iter().find(|(r, _)| r == name).map(|(_, f)| f.clone())
}
fn mount_parts(&mut self, cx: &mut Cx) {
let me = self.me;
let insets = self.p.insets.unwrap_or(cx.content_insets());
self.applied_insets = Some(insets);
let pages = cx.add_child(me, SkiaLayer::new().fill());
if !self.tabs.is_empty() {
let bar = self.tab_bar_build(insets);
self.tab_bar_id = Some(cx.add_child(me, bar));
}
let overlays = || SkiaLayer::new().fill().margin(insets);
let (modals, popups, toasts) = (cx.add_child(me, overlays()), cx.add_child(me, overlays()), cx.add_child(me, overlays()));
self.parts = Some(Parts { pages, modals, popups, toasts });
self.stacks = (0..self.tabs.len().max(1)).map(|_| None).collect();
let root = std::mem::take(&mut self.root);
self.stacks[0] = Some(mount_stack(cx, pages, root, true, self.tab_look(insets), insets));
self.history_on = self.p.use_browser_history && cx.has_history();
if self.history_on {
cx.listen_history(me, true);
}
if !self.tabs.is_empty() || self.on_route_changed.is_some() || self.history_on {
self.requests.push_front(Request::Start);
}
}
fn tab_look(&self, insets: Thickness) -> Option<(f32, Color)> {
(!self.tabs.is_empty()).then(|| (self.p.tab_bar_height + insets.bottom, self.p.page_background_color))
}
fn apply_insets(&mut self, cx: &mut Cx) {
let insets = self.p.insets.unwrap_or(cx.content_insets());
if self.applied_insets == Some(insets) {
return;
}
self.applied_insets = Some(insets);
let (bar, nav_top) = (self.p.tab_bar_height + insets.bottom, self.p.nav_bar_height + insets.top);
if let Some(mut tab_bar) = self.tab_bar_id.and_then(|id| cx.any_mut(id)) {
tab_bar.set_height_request(bar);
tab_bar.set_padding((0.0, 0.0, 0.0, insets.bottom));
}
let tabs = !self.tabs.is_empty();
let (root_margin, content_margin, nav_padding) = self.page_insets(insets);
for stack in self.stacks.iter().flatten() {
if tabs && let Some(mut layer) = cx.any_mut(stack.layer) {
layer.set_margin((0.0, 0.0, 0.0, bar));
}
if let Some(mut root) = cx.any_mut(stack.root) {
root.set_margin(root_margin);
}
for page in &stack.pages {
let [content, nav, ..] = *cx.tree.children(page.host) else { continue };
if let Some(mut content) = cx.any_mut(content) {
content.set_margin(content_margin);
}
if let Some(mut nav) = cx.any_mut(nav) {
nav.set_height_request(nav_top);
nav.set_padding(nav_padding);
}
}
}
if let Some(parts) = self.parts {
for layer in [parts.modals, parts.popups, parts.toasts] {
if let Some(mut layer) = cx.any_mut(layer) {
layer.set_margin(insets);
}
}
}
}
fn page_insets(&self, insets: Thickness) -> (Thickness, Thickness, Thickness) {
let bottom = if self.tabs.is_empty() { insets.bottom } else { 0.0 };
let nav_top = self.p.nav_bar_height + insets.top;
(
Thickness::new(insets.left, insets.top, insets.right, bottom),
Thickness::new(insets.left, nav_top, insets.right, bottom),
Thickness::new(insets.left, insets.top, insets.right, 0.0),
)
}
fn tab_bar_build(&mut self, insets: Thickness) -> Build<SkiaLayout> {
let me = self.me;
let mut cells = Vec::with_capacity(self.tabs.len());
for (i, (_, title)) in self.tabs.iter().enumerate() {
let selected = i == self.selected;
let mut button = Handle::default();
let indicator = SkiaShape::new()
.height_request(3)
.width_request(36)
.corner_radius(2)
.background_color(Self::TAB_SELECTED_COLOR)
.horizontal_options(LayoutOptions::Center)
.vertical_options(LayoutOptions::End)
.input_transparent(true)
.is_visible(selected);
let indicator_id = indicator.id();
let caption = SkiaButton::new(title.as_str())
.assign(&mut button)
.fill()
.background_color(Color::TRANSPARENT)
.corner_radius(0)
.font_size(13)
.font_family(if selected { "FontTextBold" } else { "" })
.text_color(if selected { Self::TAB_SELECTED_COLOR } else { Self::TAB_COLOR })
.accessibility_role(Aria::BUTTON)
.accessibility_label(title.as_str());
self.tab_bar.push((button, indicator_id));
cells.push(SkiaLayer::new().column(i as i32).fill().children((tapped(caption, move |cx| cx.select_tab(me, i)), indicator)));
}
let p = &self.p;
SkiaLayer::new()
.height_request(p.tab_bar_height + insets.bottom)
.vertical_options(LayoutOptions::End)
.background_color(p.tab_bar_color)
.block_gestures_below(true)
.padding((0.0, 0.0, 0.0, insets.bottom))
.children((
SkiaLayer::new().height_request(1).background_color(LINE),
SkiaGrid::new().column_definitions(vec![GridLength::STAR; self.tabs.len()]).fill().children(cells),
))
}
fn page_host(&self, title: &str, page: Detached, insets: Thickness) -> Build<SkiaLayout> {
let (me, p) = (self.me, &self.p);
let nav_top = p.nav_bar_height + insets.top;
let (_, content_margin, nav_padding) = self.page_insets(insets);
let nav_button = |text: &str| {
SkiaButton::new(text)
.background_color(Color::TRANSPARENT)
.text_color(ACCENT)
.font_size(16)
.vertical_options(LayoutOptions::Center)
.accessibility_role(Aria::BUTTON)
.accessibility_label(text.trim_start_matches(['‹', ' ']))
};
SkiaLayer::new().fill().background_color(p.page_background_color).block_gestures_below(true).children((
SkiaLayer::new().fill().margin(content_margin).children(page),
SkiaLayer::new()
.height_request(nav_top)
.background_color(p.nav_bar_color)
.padding(nav_padding)
.use_cache(CacheType::Image)
.children((
tapped(nav_button("‹ Back").margin((8, 0)), move |cx| cx.go_back(me, true)),
SkiaLabel::new(title)
.font_size(18)
.font_family("FontTextBold")
.text_color(Color::WHITE)
.fill_x()
.horizontal_text_alignment(TextAlignment::Center)
.vertical_options(LayoutOptions::Center)
.max_lines(1)
.margin((96, 0))
.accessibility_role(Aria::HEADING),
tapped(nav_button("Home").horizontal_options(LayoutOptions::End).margin((0, 0, 8, 0)), move |cx| {
cx.pop_to_root(me)
}),
SkiaLayer::new().height_request(1).vertical_options(LayoutOptions::End).background_color(LINE),
)),
))
}
}
impl Has<ShellProps> for SkiaShell {
fn part(&self) -> &ShellProps {
&self.p
}
fn part_mut(&mut self) -> &mut ShellProps {
&mut self.p
}
}
impl Control for SkiaShell {
fn inner(&self) -> Option<&dyn Control> {
Some(&self.layout)
}
fn inner_mut(&mut self) -> Option<&mut dyn Control> {
Some(&mut self.layout)
}
fn on_props_changed(&mut self, cx: &mut Cx) {
if self.parts.is_none() {
self.mount_parts(cx);
} else {
self.apply_insets(cx);
}
if !self.requests.is_empty() && !std::mem::replace(&mut self.ticking, true) {
animators::start_frame(cx.tree, self.me, tick);
}
}
fn on_history(&mut self, cx: &mut GestureCx, depth: u32, hash: &str) {
self.requests.push_back(Request::HistoryMoved { depth, hash: hash.to_owned() });
if !std::mem::replace(&mut self.ticking, true) {
animators::start_frame(cx.tree, self.me, tick);
}
}
}
impl Build<SkiaShell> {
pub fn route<S: Any, B: Into<Detached>>(
mut self,
name: &str,
factory: impl Fn(&mut S, &ShellArguments) -> B + 'static,
) -> Self {
let factory: Factory = Rc::new(move |state, arguments| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
factory(state, arguments).into()
});
self.control_mut().routes.push((name.to_owned(), factory));
self
}
pub fn titles<'a>(mut self, titles: impl IntoIterator<Item = (&'a str, &'a str)>) -> Self {
let own = &mut self.control_mut().titles;
own.extend(titles.into_iter().map(|(r, t)| (r.to_owned(), t.to_owned())));
self
}
pub fn tabs<'a>(mut self, tabs: impl IntoIterator<Item = (&'a str, &'a str)>) -> Self {
let own = &mut self.control_mut().tabs;
own.extend(tabs.into_iter().map(|(r, t)| (r.to_owned(), t.to_owned())));
self
}
pub fn root(mut self, content: impl IntoChildren) -> Self {
content.push_into(&mut self.control_mut().root);
self
}
pub fn on_navigating<S: Any>(
mut self,
mut f: impl FnMut(&mut Mut<'_, SkiaShell>, &mut S, &mut Cx<'_>, &mut ShellNavigatingArgs) + 'static,
) -> Self {
self.control_mut().on_navigating = Some(Box::new(move |me, state, cx, e| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx, e)
}));
self
}
pub fn on_navigated<S: Any>(
mut self,
mut f: impl FnMut(&mut Mut<'_, SkiaShell>, &mut S, &mut Cx<'_>, &ShellNavigatedArgs) + 'static,
) -> Self {
self.control_mut().on_navigated = Some(Box::new(move |me, state, cx, e| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx, e)
}));
self
}
pub fn on_route_changed<S: Any>(
mut self,
mut f: impl FnMut(&mut Mut<'_, SkiaShell>, &mut S, &mut Cx<'_>, &str) + 'static,
) -> Self {
self.control_mut().on_route_changed = Some(Box::new(move |me, state, cx, route| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx, route)
}));
self
}
}
impl Build<SkiaShell> {
pub fn on_changed<S: Any>(mut self, mut f: impl FnMut(&mut Mut<'_, SkiaShell>, &mut S, &mut Cx<'_>) + 'static) -> Self {
self.control_mut().on_changed = Some(Box::new(move |me, state, cx| {
let state = state.downcast_mut::<S>().unwrap_or_else(|| wrong_state::<S>());
f(&mut me.typed(), state, cx)
}));
self
}
}
fn tapped<T: Control>(mut build: Build<T>, mut run: impl FnMut(&mut Cx) + 'static) -> Build<T> {
build.handlers.tapped = Some(Box::new(move |_, _, cx| run(cx)));
build
}
struct PopupWrapper {
layout: SkiaLayout,
content: ControlId,
outside: bool,
}
impl Control for PopupWrapper {
fn inner(&self) -> Option<&dyn Control> {
Some(&self.layout)
}
fn inner_mut(&mut self) -> Option<&mut dyn Control> {
Some(&mut self.layout)
}
fn on_gesture(&mut self, cx: &mut GestureCx, gesture: &Gesture) -> Handled {
if gesture.kind == GestureKind::Tapped {
let tree = &*cx.tree;
let matrix = tree.render.get(self.content.index as usize).and_then(|r| r.matrix).and_then(|m| m.invert());
let local = matrix.map_or(cx.point, |m| m.map_point(cx.point));
let r = tree.base(self.content).map(|b| b.rect).unwrap_or_default();
self.outside = !(local.x >= r.left && local.x < r.right && local.y >= r.top && local.y < r.bottom);
}
Handled::No
}
}
impl Cx<'_> {
pub fn go_to(&mut self, shell: impl Into<ControlId>, route: &str, animated: bool) {
self.go_to_with(shell, route, animated, Vec::new())
}
pub fn go_to_with(&mut self, shell: impl Into<ControlId>, route: &str, animated: bool, arguments: ShellArguments) {
request(self, shell.into(), Request::GoTo { route: route.to_owned(), animated, arguments });
}
pub fn go_back(&mut self, shell: impl Into<ControlId>, animated: bool) {
request(self, shell.into(), Request::GoBack { animated });
}
pub fn pop_to_root(&mut self, shell: impl Into<ControlId>) {
request(self, shell.into(), Request::PopToRoot);
}
pub fn pop_tab_to_root(&mut self, shell: impl Into<ControlId>) {
request(self, shell.into(), Request::PopTabToRoot);
}
pub fn select_tab(&mut self, shell: impl Into<ControlId>, index: usize) {
request(self, shell.into(), Request::SelectTab(index));
}
pub fn open_popup(&mut self, shell: impl Into<ControlId>, content: impl Into<Detached>, options: PopupOptions) {
request(self, shell.into(), Request::OpenPopup { content: content.into(), options });
}
pub fn close_popup(&mut self, shell: impl Into<ControlId>, animated: bool) {
request(self, shell.into(), Request::ClosePopup { wrapper: None, animated });
}
pub fn close_all_popups(&mut self, shell: impl Into<ControlId>) {
request(self, shell.into(), Request::CloseAllPopups);
}
pub fn push_modal(&mut self, shell: impl Into<ControlId>, content: impl Into<Detached>, options: ModalOptions) {
request(self, shell.into(), Request::PushModal { content: content.into(), options });
}
pub fn pop_modal(&mut self, shell: impl Into<ControlId>, animated: bool) {
request(self, shell.into(), Request::PopModal { animated });
}
pub fn show_toast(&mut self, shell: impl Into<ControlId>, text: &str, ms: impl IntoProp<f32>) {
let label = SkiaRichLabel::new(text)
.text_color(SkiaShell::TOAST_TEXT_COLOR)
.font_size(SkiaShell::TOAST_TEXT_SIZE)
.margin(SkiaShell::TOAST_TEXT_MARGINS)
.fill_x();
self.show_toast_content(shell, label, ms);
}
pub fn show_toast_content(&mut self, shell: impl Into<ControlId>, content: impl Into<Detached>, ms: impl IntoProp<f32>) {
request(self, shell.into(), Request::ShowToast { content: content.into(), ms: ms.into_prop() });
}
pub fn close_all_toasts(&mut self, shell: impl Into<ControlId>) {
request(self, shell.into(), Request::CloseAllToasts);
}
}
thread_local! {
static FIRING: RefCell<(Option<ControlId>, Vec<Request>)> = const { RefCell::new((None, Vec::new())) };
}
fn shell(tree: &mut Tree, id: ControlId) -> Option<&mut SkiaShell> {
part_mut(tree.node_mut(id)?.kind.as_deref_mut()?)
}
fn request(cx: &mut Cx, id: ControlId, request: Request) {
let Some(s) = shell(cx.tree, id) else {
FIRING.with_borrow_mut(|(firing, pending)| {
if *firing == Some(id) {
pending.push(request);
}
});
return;
};
s.requests.push_back(request);
wake(cx.tree, id);
}
fn post(cx: &mut Cx, id: ControlId, done: Done) {
request(cx, id, Request::Done(done));
}
fn wake(tree: &mut Tree, id: ControlId) {
if let Some(s) = shell(tree, id)
&& !std::mem::replace(&mut s.ticking, true)
{
animators::start_frame(tree, id, tick);
}
}
fn tick(id: ControlId, _time_ms: f64, state: &mut dyn Any, cx: &mut Cx<'_>) -> FrameTick {
let mut ran = false;
for _ in 0..REQUESTS_PER_FRAME {
let Some(request) = shell(cx.tree, id).and_then(|s| s.requests.pop_front()) else { break };
run(cx, id, state, request);
ran = true;
}
if ran {
fire(cx, id, |s| &mut s.on_changed, |h, me, cx| h(me, state, cx));
}
let keep = shell(cx.tree, id).is_some_and(|s| {
s.ticking = !s.requests.is_empty();
s.ticking
});
FrameTick { keep, state_touched: ran }
}
fn run(cx: &mut Cx, id: ControlId, state: &mut dyn Any, request: Request) {
match request {
Request::Start => start(cx, id, state),
Request::GoTo { route, animated, arguments } => go_to(cx, id, state, &route, animated, arguments),
Request::GoBack { animated } => go_back(cx, id, state, animated),
Request::PopToRoot => pop_to_root(cx, id, state, true),
Request::PopTabToRoot => pop_to_root(cx, id, state, false),
Request::SelectTab(index) => select_tab(cx, id, state, index),
Request::OpenPopup { content, options } => open_popup(cx, id, state, content, options),
Request::ClosePopup { wrapper, animated } => {
let top = shell(cx.tree, id).and_then(|s| s.popups.last()).map(|p| p.wrapper);
if let Some(wrapper) = wrapper.or(top) {
close_popup(cx, id, state, wrapper, animated);
}
}
Request::CloseAllPopups => {
let count = shell(cx.tree, id).map_or(0, |s| s.popups.len());
for i in (0..count).rev() {
if let Some(wrapper) = shell(cx.tree, id).and_then(|s| s.popups.get(i)).map(|p| p.wrapper) {
close_popup(cx, id, state, wrapper, false);
}
}
}
Request::PushModal { content, options } => push_modal(cx, id, state, content, options),
Request::PopModal { animated } => pop_modal(cx, id, state, animated),
Request::ShowToast { content, ms } => show_toast(cx, id, content, ms),
Request::CloseAllToasts => close_all_toasts(cx, id),
Request::HistoryMoved { depth, hash } => history_moved(cx, id, state, depth, &hash),
Request::Done(done) => finish(cx, id, state, done),
}
}
fn fire<H: ?Sized>(
cx: &mut Cx,
id: ControlId,
slot: fn(&mut SkiaShell) -> &mut Option<Box<H>>,
call: impl FnOnce(&mut H, Raw<'_>, &mut Cx<'_>),
) {
let Some(mut handler) = shell(cx.tree, id).and_then(|s| slot(s).take()) else { return };
let Some(mut node) = cx.tree.take(id) else { return };
let outer = FIRING.with_borrow_mut(|(firing, _)| firing.replace(id));
let mut queue = Vec::new();
if let Some(control) = node.kind.as_deref_mut() {
call(&mut *handler, Raw { id, control, base: &mut node.base, queue: &mut queue }, cx);
}
cx.tree.put_back(node);
cx.tree.queue.append(&mut queue);
let asked = FIRING.with_borrow_mut(|(firing, pending)| {
*firing = outer;
std::mem::take(pending)
});
if let Some(s) = shell(cx.tree, id) {
slot(s).get_or_insert(handler);
s.requests.extend(asked);
}
}
fn navigating(
cx: &mut Cx,
id: ControlId,
state: &mut dyn Any,
route: &str,
source: NavigationSource,
view: Option<ControlId>,
previous: Option<ControlId>,
) -> bool {
let mut e = ShellNavigatingArgs { route: route.to_owned(), source, cancel: false, view, previous };
fire(cx, id, |s| &mut s.on_navigating, |h, me, cx| h(me, state, cx, &mut e));
!e.cancel
}
fn navigated(cx: &mut Cx, id: ControlId, state: &mut dyn Any, route: &str, source: NavigationSource, view: Option<ControlId>) {
if shell(cx.tree, id).is_some_and(|s| s.on_navigated.is_some()) {
let e = ShellNavigatedArgs { route: route.to_owned(), source, view };
fire(cx, id, |s| &mut s.on_navigated, |h, me, cx| h(me, state, cx, &e));
}
}
fn route_changed(cx: &mut Cx, id: ControlId, state: &mut dyn Any) {
let Some(s) = shell(cx.tree, id) else { return };
if s.route_seen.as_deref() == Some(s.route()) {
return;
}
let route = s.route().to_owned();
s.route_seen = Some(route.clone());
fire(cx, id, |s| &mut s.on_route_changed, |h, me, cx| h(me, state, cx, &route));
}
fn current_route(tree: &mut Tree, id: ControlId) -> String {
shell(tree, id).map_or_else(String::new, |s| s.route().to_owned())
}
fn set_visible(tree: &mut Tree, id: ControlId, visible: bool) {
if let Some(mut c) = tree.any_mut(id) {
c.set_is_visible(visible);
}
}
fn mount_stack(cx: &mut Cx, pages: ControlId, content: Vec<Detached>, visible: bool, tab: Option<(f32, Color)>, insets: Thickness) -> Stack {
let bottom_inset = if tab.is_some() { 0.0 } else { insets.bottom };
let mut root = SkiaLayer::new().fill().margin((insets.left, insets.top, insets.right, bottom_inset)).children(content);
if let Some((_, background)) = tab {
root = root.background_color(background);
}
let root_id = root.id();
let bottom = tab.map_or(0.0, |(bottom, _)| bottom);
let layer = SkiaLayer::new().fill().margin((0.0, 0.0, 0.0, bottom)).is_visible(visible).children(root);
Stack { layer: cx.add_child(pages, layer), root: root_id, pages: Vec::new() }
}
fn lerp(from: f32, to: f32, v: f32) -> f32 {
if v == 1.0 { to } else { from + (to - from) * v }
}
fn width(tree: &Tree, id: ControlId) -> f32 {
tree.base(id).map_or(0.0, |b| b.rect.width() / b.scale.max(0.01))
}
fn show_pages(tree: &mut Tree, id: ControlId, tab: usize) {
let Some(stack) = shell(tree, id).and_then(|s| s.stacks.get(tab)?.as_ref()) else { return };
let moving = stack.pages.iter().any(|p| p.slide.is_some());
let (root, count) = (stack.root, stack.pages.len());
set_visible(tree, root, moving || count == 0);
for i in 0..count {
let Some(host) = shell(tree, id).and_then(|s| Some(s.stacks.get(tab)?.as_ref()?.pages.get(i)?.host)) else { break };
set_visible(tree, host, moving || i + 1 == count);
}
}
fn slide(cx: &mut Cx, id: ControlId, host: ControlId, from: f32, to: f32, ms: f32, done: Done) -> AnimationId {
cx.animate(host, ms, easing::linear, move |v, cx| {
if let Some(mut c) = cx.any_mut(host) {
c.set_translation_x(lerp(from, to, v));
}
if v == 1.0 {
post(cx, id, done);
}
})
}
fn start(cx: &mut Cx, id: ControlId, state: &mut dyn Any) {
let hash = cx.location_hash().to_owned();
let Some(s) = shell(cx.tree, id) else { return };
if let (Some((route, _)), Some(root)) = (s.tabs.first(), s.stack().map(|st| st.root))
&& let Some(factory) = s.factory(route)
{
let content = factory(state, &Vec::new());
cx.add_child(root, content);
}
if let Some(s) = shell(cx.tree, id)
&& s.history_on
{
let initial = parse_hash(s, &hash);
if !initial.is_empty() {
s.history = vec![HistoryKind::Page; initial.len()];
let op = HistoryOp::Replace { depth: initial.len() as u32, hash: Some(hash_for(initial.iter().map(String::as_str))) };
cx.history(op);
rebuild(cx, id, state, &initial);
}
}
route_changed(cx, id, state);
}
fn push_page(
cx: &mut Cx,
id: ControlId,
state: &mut dyn Any,
name: &str,
route: String,
arguments: ShellArguments,
animated: bool,
) -> Option<(ControlId, bool)> {
let factory = shell(cx.tree, id)?.factory(name)?;
let page = factory(state, &arguments);
let distance = if animated { width(cx.tree, id) } else { 0.0 };
let s = shell(cx.tree, id)?;
let (tab, speed, insets) = (s.selected, s.p.pages_animation_speed, s.applied_insets.unwrap_or(Thickness::ZERO));
let layer = s.stack()?.layer;
let build = s.page_host(&s.title(name), page, insets).translation_x(distance);
let host = cx.add_child(layer, build);
let slide = (distance > 0.0).then(|| slide(cx, id, host, distance, 0.0, speed, Done::Pushed(host)));
shell(cx.tree, id)?.stacks.get_mut(tab)?.as_mut()?.pages.push(Page { route, arguments, host, slide, popping: false });
show_pages(cx.tree, id, tab);
Some((host, slide.is_some()))
}
fn go_to(cx: &mut Cx, id: ControlId, state: &mut dyn Any, route: &str, animated: bool, arguments: ShellArguments) {
let Some(s) = shell(cx.tree, id) else { return };
let (name, mut merged) = split_route(route);
if s.factory(name).is_none() {
panic!("DrawnUi: route '{name}' is not registered in SkiaShell routes");
}
for (key, value) in arguments {
set_argument(&mut merged, key, value);
}
let full = build_route(name, &merged);
let previous = s.stack().and_then(|st| st.pages.last()).map(|p| p.host);
if !navigating(cx, id, state, &full, NavigationSource::Push, None, previous) {
return;
}
let Some((host, slides)) = push_page(cx, id, state, name, full.clone(), merged, animated) else { return };
push_history(cx, id, HistoryKind::Page);
route_changed(cx, id, state);
if !slides {
navigated(cx, id, state, &full, NavigationSource::Push, Some(host));
}
}
fn go_back(cx: &mut Cx, id: ControlId, state: &mut dyn Any, animated: bool) {
let Some(s) = shell(cx.tree, id) else { return };
if let Some(top) = s.popups.last() {
let wrapper = top.wrapper;
return close_popup(cx, id, state, wrapper, animated);
}
if !s.modals.is_empty() {
return pop_modal(cx, id, state, animated);
}
if !s.stack().is_some_and(|st| st.pages.iter().any(|p| !p.popping)) || back_through_history(cx, id, HistoryKind::Page) {
return;
}
pop_page(cx, id, state, animated);
}
fn below(stack: &Stack, i: usize) -> (String, Option<ControlId>) {
match i.checked_sub(1).and_then(|b| stack.pages.get(b)) {
Some(p) => (p.route.clone(), Some(p.host)),
None => (String::new(), None),
}
}
fn pop_page(cx: &mut Cx, id: ControlId, state: &mut dyn Any, animated: bool) -> bool {
let Some(s) = shell(cx.tree, id) else { return true };
let (tab, speed) = (s.selected, s.p.pages_animation_speed);
let Some(stack) = s.stack() else { return true };
let Some(i) = stack.pages.iter().rposition(|p| !p.popping) else { return true };
let (host, (below_route, below_host)) = (stack.pages[i].host, below(stack, i));
if !navigating(cx, id, state, &below_route, NavigationSource::Pop, Some(host), Some(host)) {
return false;
}
let distance = if animated { width(cx.tree, id) } else { 0.0 };
let from = cx.base(host).map_or(0.0, |b| b.p.translation_x);
let Some(page) = shell(cx.tree, id).and_then(|s| s.stacks.get_mut(tab)?.as_mut()?.pages.iter_mut().find(|p| p.host == host)) else {
return true;
};
let running = page.slide.take();
if let Some(running) = running {
cx.stop_animation(running);
}
if distance > 0.0 {
let slide = slide(cx, id, host, from, distance, speed, Done::Popped(host));
if let Some(page) = shell(cx.tree, id).and_then(|s| s.stacks.get_mut(tab)?.as_mut()?.pages.iter_mut().find(|p| p.host == host)) {
(page.slide, page.popping) = (Some(slide), true);
}
show_pages(cx.tree, id, tab);
return true;
}
if let Some(stack) = shell(cx.tree, id).and_then(|s| s.stacks.get_mut(tab)?.as_mut()) {
stack.pages.retain(|p| p.host != host);
}
cx.remove(host);
show_pages(cx.tree, id, tab);
route_changed(cx, id, state);
navigated(cx, id, state, &below_route, NavigationSource::Pop, below_host);
true
}
fn pop_to_root(cx: &mut Cx, id: ControlId, state: &mut dyn Any, every_tab: bool) {
let Some(s) = shell(cx.tree, id) else { return };
if every_tab && s.history_on {
let pages = s.history.iter().filter(|k| **k == HistoryKind::Page).count();
s.history.retain(|k| *k != HistoryKind::Page);
let depth = s.history.len() as u32;
if pages > 0 {
cx.history(HistoryOp::Replace { depth, hash: Some(String::new()) });
}
}
let Some(s) = shell(cx.tree, id) else { return };
let tabs = if every_tab { 0..s.stacks.len() } else { s.selected..s.selected + 1 };
for tab in tabs {
clear_stack(cx, id, tab);
}
route_changed(cx, id, state);
}
fn clear_stack(cx: &mut Cx, id: ControlId, tab: usize) {
let hosts: Vec<ControlId> = shell(cx.tree, id)
.and_then(|s| s.stacks.get_mut(tab)?.as_mut())
.map(|st| st.pages.drain(..).map(|p| p.host).collect())
.unwrap_or_default();
for host in hosts {
cx.remove(host);
}
show_pages(cx.tree, id, tab);
}
fn rebuild(cx: &mut Cx, id: ControlId, state: &mut dyn Any, routes: &[String]) {
let Some(tab) = shell(cx.tree, id).map(|s| s.selected) else { return };
clear_stack(cx, id, tab);
for route in routes {
let (name, arguments) = split_route(route);
push_page(cx, id, state, name, route.clone(), arguments, false);
}
route_changed(cx, id, state);
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum HistoryKind {
Page,
Popup,
Modal,
}
fn hash_for<'a>(routes: impl Iterator<Item = &'a str>) -> String {
let mut hash = String::new();
for route in routes {
hash.push_str(if hash.is_empty() { "#/" } else { "/" });
encode(&mut hash, route, false);
}
hash
}
fn parse_hash(s: &SkiaShell, hash: &str) -> Vec<String> {
let path = hash.strip_prefix('#').map_or(hash, |h| h.strip_prefix('/').unwrap_or(h));
let routes = path.split('/').filter(|r| !r.is_empty()).map(|r| decode(r, false));
routes.filter(|r| s.factory(split_route(r).0).is_some()).collect()
}
fn push_history(cx: &mut Cx, id: ControlId, kind: HistoryKind) {
let Some(s) = shell(cx.tree, id).filter(|s| s.history_on) else { return };
s.history.push(kind);
let depth = s.history.len() as u32;
let hash = (kind == HistoryKind::Page).then(|| hash_for(s.navigation_stack()));
cx.history(HistoryOp::Push { depth, hash });
}
fn back_through_history(cx: &mut Cx, id: ControlId, kind: HistoryKind) -> bool {
if !shell(cx.tree, id).is_some_and(|s| s.history_on && s.history.last() == Some(&kind)) {
return false;
}
cx.history(HistoryOp::Back);
true
}
fn history_moved(cx: &mut Cx, id: ControlId, state: &mut dyn Any, depth: u32, hash: &str) {
let Some(s) = shell(cx.tree, id) else { return };
let count = s.history.len() as u32;
if depth < count {
while let Some(kind) = shell(cx.tree, id).filter(|s| s.history.len() as u32 > depth).and_then(|s| s.history.pop()) {
let done = match kind {
HistoryKind::Popup => match shell(cx.tree, id).and_then(|s| s.popups.last_mut()) {
Some(top) => {
top.history = false;
let wrapper = top.wrapper;
let done = close_popup_now(cx, id, state, wrapper, true);
set_overlay_history(cx.tree, id, wrapper, !done);
done
}
None => true,
},
HistoryKind::Modal => match shell(cx.tree, id).and_then(|s| s.modals.last_mut()) {
Some(top) => {
top.history = false;
let wrapper = top.wrapper;
let done = pop_modal_now(cx, id, state, true);
set_overlay_history(cx.tree, id, wrapper, !done);
done
}
None => true,
},
HistoryKind::Page => pop_page(cx, id, state, true),
};
if !done {
if let Some(s) = shell(cx.tree, id) {
s.history.push(kind);
let depth = s.history.len() as u32;
let hash = (kind == HistoryKind::Page).then(|| hash_for(s.navigation_stack()));
cx.history(HistoryOp::Push { depth, hash });
}
break;
}
}
return;
}
let target = parse_hash(s, hash);
if depth == count && target.iter().map(String::as_str).eq(s.navigation_stack()) {
return;
}
s.history = vec![HistoryKind::Page; target.len()];
cx.history(HistoryOp::Replace { depth: target.len() as u32, hash: Some(hash_for(target.iter().map(String::as_str))) });
rebuild(cx, id, state, &target);
}
fn set_overlay_history(tree: &mut Tree, id: ControlId, wrapper: ControlId, history: bool) {
let Some(s) = shell(tree, id) else { return };
if let Some(o) = s.popups.iter_mut().chain(s.modals.iter_mut()).find(|o| o.wrapper == wrapper) {
o.history = history;
}
}
fn select_tab(cx: &mut Cx, id: ControlId, state: &mut dyn Any, index: usize) {
let Some(s) = shell(cx.tree, id) else { return };
let (count, from) = (s.tabs.len(), s.selected);
if count == 0 {
return;
}
if let Some((animation, left)) = s.tab_switch.take() {
cx.stop_animation(animation);
end_tab_switch(cx.tree, id, left);
}
let to = index.min(count - 1);
if to == from {
return;
}
let Some(s) = shell(cx.tree, id) else { return };
let Some(pages) = s.parts.map(|p| p.pages) else { return };
if s.stacks[to].is_none() {
let insets = s.applied_insets.unwrap_or(Thickness::ZERO);
let look = s.tab_look(insets);
let stack = mount_stack(cx, pages, Vec::new(), false, look, insets);
let root = stack.root;
let Some(s) = shell(cx.tree, id) else { return };
s.stacks[to] = Some(stack);
if let Some(factory) = s.factory(&s.tabs[to].0.clone()) {
let content = factory(state, &Vec::new());
cx.add_child(root, content);
}
}
let Some(s) = shell(cx.tree, id) else { return };
s.selected = to;
let (animate, ms) = (s.p.animate_tabs, s.p.tabs_animation_speed);
let (Some(old), Some(new)) = (s.stacks[from].as_ref().map(|st| st.layer), s.stacks[to].as_ref().map(|st| st.layer)) else {
return;
};
paint_tab_bar(cx.tree, id);
set_visible(cx.tree, new, true);
let distance = width(cx.tree, id);
if !animate || distance <= 0.0 {
set_visible(cx.tree, old, false);
} else {
bring_to_front(cx.tree, pages, new);
let dir = if to > from { 1.0 } else { -1.0 };
let (enter, leave) = (dir * 0.75 * distance, -dir * distance);
if let Some(mut c) = cx.any_mut(new) {
c.set_translation_x(enter);
c.set_opacity(0.001);
}
let animation = cx.animate(new, ms, easing::linear, move |p, cx| {
let v = if p == 1.0 { 1.0 } else { tabs_easing(p) };
if let Some(mut c) = cx.any_mut(new) {
c.set_translation_x(lerp(enter, 0.0, v));
c.set_opacity(lerp(0.001, 1.0, p));
}
if let Some(mut c) = cx.any_mut(old) {
c.set_translation_x(lerp(0.0, leave, v));
}
if p == 1.0 {
post(cx, id, Done::TabShown(from));
}
});
if let Some(s) = shell(cx.tree, id) {
s.tab_switch = Some((animation, from));
}
}
route_changed(cx, id, state);
}
fn bring_to_front(tree: &mut Tree, parent: ControlId, child: ControlId) {
let Some(node) = tree.node_mut(parent) else { return };
if let Some(i) = node.children.iter().position(|c| *c == child) {
let id = node.children.remove(i);
node.children.push(id);
tree.invalidate(parent, crate::types::Dirty::DRAW);
}
}
fn end_tab_switch(tree: &mut Tree, id: ControlId, left: usize) {
let Some(s) = shell(tree, id) else { return };
if left == s.selected {
return;
}
let Some(layer) = s.stacks.get(left).and_then(|st| st.as_ref()).map(|st| st.layer) else { return };
if let Some(mut c) = tree.any_mut(layer) {
c.set_is_visible(false);
c.set_translation_x(0.0);
c.set_opacity(1.0);
}
}
fn paint_tab_bar(tree: &mut Tree, id: ControlId) {
let Some(s) = shell(tree, id) else { return };
let (selected, count) = (s.selected, s.tab_bar.len());
for i in 0..count {
let Some((button, indicator)) = shell(tree, id).map(|s| s.tab_bar[i]) else { return };
let on = i == selected;
if let Some(mut b) = tree.get_mut(button) {
b.set_font_family(if on { "FontTextBold" } else { "" });
b.set_text_color(if on { SkiaShell::TAB_SELECTED_COLOR } else { SkiaShell::TAB_COLOR });
}
set_visible(tree, indicator, on);
}
}
fn open_popup(cx: &mut Cx, id: ControlId, state: &mut dyn Any, content: Detached, o: PopupOptions) {
let route = current_route(cx.tree, id);
if !navigating(cx, id, state, &route, NavigationSource::Push, None, None) {
return;
}
let Some(layer) = shell(cx.tree, id).and_then(|s| s.parts).map(|p| p.popups) else { return };
let inner = SkiaLayer::new().center().scale(if o.animated { 0.5 } else { 1.0 }).children(content);
let content = inner.id();
let wrapper = PopupWrapper { layout: SkiaLayout::default(), content, outside: false };
let mut wrapper = Build::new(wrapper).fill().block_gestures_below(true).opacity(if o.animated { 0.1 } else { 1.0 });
if o.show_overlay {
let dim = o.background_color.unwrap_or(SkiaShell::POPUP_BACKGROUND_COLOR);
wrapper.push_child(backdrop(dim));
}
wrapper.push_child(inner);
let close = o.close_when_background_tapped;
wrapper.handlers.tapped = Some(Box::new(move |me, _, cx| {
let me = me.typed::<PopupWrapper>();
if close && me.outside {
let wrapper = me.id();
request(cx, id, Request::ClosePopup { wrapper: Some(wrapper), animated: o.animated });
}
}));
let wrapper = cx.add_child(layer, wrapper);
let animation = o.animated.then(|| {
cx.animate(wrapper, SkiaShell::POPUPS_ANIMATION_SPEED, easing::linear, move |v, cx| {
if let Some(mut c) = cx.any_mut(wrapper) {
c.set_opacity(lerp(0.1, 1.0, v));
}
if let Some(mut c) = cx.any_mut(content) {
c.set_scale_x(lerp(0.5, 1.0, v));
c.set_scale_y(lerp(0.5, 1.0, v));
}
if v == 1.0 {
post(cx, id, Done::PopupOpened(wrapper));
}
})
});
if let Some(s) = shell(cx.tree, id) {
let history = s.history_on;
s.popups.push(Overlay { wrapper, content, animation, closing: false, animated: o.animated, history });
}
push_history(cx, id, HistoryKind::Popup);
if animation.is_none() {
navigated(cx, id, state, &route, NavigationSource::Push, Some(content));
}
}
fn close_popup(cx: &mut Cx, id: ControlId, state: &mut dyn Any, wrapper: ControlId, animated: bool) {
let Some(p) = shell(cx.tree, id).and_then(|s| s.popups.iter().find(|p| p.wrapper == wrapper)) else { return };
if p.closing {
return;
}
if p.history && back_through_history(cx, id, HistoryKind::Popup) {
set_overlay_history(cx.tree, id, wrapper, false);
return;
}
close_popup_now(cx, id, state, wrapper, animated);
}
fn close_popup_now(cx: &mut Cx, id: ControlId, state: &mut dyn Any, wrapper: ControlId, animated: bool) -> bool {
let Some(p) = shell(cx.tree, id).and_then(|s| s.popups.iter().find(|p| p.wrapper == wrapper)) else { return true };
if p.closing {
return true;
}
let (content, own) = (p.content, p.animated);
let route = current_route(cx.tree, id);
if !navigating(cx, id, state, &route, NavigationSource::Pop, Some(content), Some(content)) {
return false;
}
let Some(p) = shell(cx.tree, id).and_then(|s| s.popups.iter_mut().find(|p| p.wrapper == wrapper)) else { return true };
p.closing = true;
let opening = p.animation.take();
if let Some(opening) = opening {
cx.stop_animation(opening);
}
if animated && own {
let opacity = cx.base(wrapper).map_or(1.0, |b| b.p.opacity);
let scale = cx.base(content).map_or(1.0, |b| b.p.scale_x);
let animation = cx.animate(wrapper, SkiaShell::POPUPS_ANIMATION_SPEED, easing::linear, move |v, cx| {
if let Some(mut c) = cx.any_mut(wrapper) {
c.set_opacity(lerp(opacity, 0.0, v));
}
if let Some(mut c) = cx.any_mut(content) {
c.set_scale_x(lerp(scale, 0.0, v));
c.set_scale_y(lerp(scale, 0.0, v));
}
if v == 1.0 {
post(cx, id, Done::PopupClosed(wrapper));
}
});
if let Some(p) = shell(cx.tree, id).and_then(|s| s.popups.iter_mut().find(|p| p.wrapper == wrapper)) {
p.animation = Some(animation);
}
return true;
}
remove_overlay(cx, id, wrapper);
navigated(cx, id, state, &route, NavigationSource::Pop, None);
true
}
fn backdrop(color: Color) -> Build<SkiaBackdrop> {
SkiaBackdrop::new().blur(SkiaShell::POPUPS_BACKGROUND_BLUR).background_color(color).input_transparent(true)
}
fn remove_overlay(cx: &mut Cx, id: ControlId, wrapper: ControlId) {
if let Some(s) = shell(cx.tree, id) {
for list in [&mut s.popups, &mut s.modals, &mut s.toasts] {
list.retain(|o| o.wrapper != wrapper);
}
}
cx.remove(wrapper);
}
fn push_modal(cx: &mut Cx, id: ControlId, state: &mut dyn Any, content: Detached, o: ModalOptions) {
let route = current_route(cx.tree, id);
if !navigating(cx, id, state, &route, NavigationSource::Push, None, None) {
return;
}
let Some(layer) = shell(cx.tree, id).and_then(|s| s.parts).map(|p| p.modals) else { return };
let mut drawer = SkiaDrawer::new()
.direction(DrawerDirection::FromBottom)
.header_size(0)
.fill()
.responds_to_gestures(o.use_gestures)
.animated(o.animated)
.bounces(false)
.block_gestures_below(true)
.children(content);
let dim = o.freeze_background.then(|| backdrop(SkiaShell::POPUP_BACKGROUND_COLOR));
let wrapper = SkiaLayer::new().fill().block_gestures_below(true);
let (wrapper_id, drawer_id) = (wrapper.id(), drawer.id());
let d = drawer.control_mut();
d.on_state_transition_complete = Some(Box::new(move |_, _, cx, open| {
post(cx, id, if open { Done::ModalOpened(wrapper_id) } else { Done::ModalClosed(wrapper_id) })
}));
if !o.animated {
d.on_is_open_changed = Some(Box::new(move |_, _, cx, open| {
if !open {
post(cx, id, Done::ModalClosed(wrapper_id));
}
}));
}
let wrapper = cx.add_child(layer, wrapper.children((dim, drawer)));
let animated = o.animated;
let opener = cx.start_animator(drawer_id, ValueAnimator::new(0.0, 1.0, 0.0, easing::linear), move |_, cx| {
if let Some(mut drawer) = cx.tree.find_mut::<SkiaDrawer>(drawer_id) {
drawer.set_is_open(true);
}
if !animated {
post(cx, id, Done::ModalOpened(wrapper));
}
});
if let Some(s) = shell(cx.tree, id) {
let history = s.history_on;
s.modals.push(Overlay { wrapper, content: drawer_id, animation: Some(opener), closing: false, animated, history });
}
push_history(cx, id, HistoryKind::Modal);
}
fn pop_modal(cx: &mut Cx, id: ControlId, state: &mut dyn Any, animated: bool) {
let Some(top) = shell(cx.tree, id).and_then(|s| s.modals.last()) else { return };
if top.closing {
return;
}
let wrapper = top.wrapper;
if top.history && back_through_history(cx, id, HistoryKind::Modal) {
set_overlay_history(cx.tree, id, wrapper, false);
return;
}
pop_modal_now(cx, id, state, animated);
}
fn pop_modal_now(cx: &mut Cx, id: ControlId, state: &mut dyn Any, animated: bool) -> bool {
let Some(top) = shell(cx.tree, id).and_then(|s| s.modals.last()) else { return true };
if top.closing {
return true;
}
let (wrapper, drawer) = (top.wrapper, top.content);
let route = current_route(cx.tree, id);
if !navigating(cx, id, state, &route, NavigationSource::Pop, Some(drawer), Some(drawer)) {
return false;
}
let Some(m) = shell(cx.tree, id).and_then(|s| s.modals.iter_mut().find(|m| m.wrapper == wrapper)) else { return true };
m.closing = true;
let opener = m.animation.take();
if let Some(opener) = opener {
cx.stop_animation(opener);
}
if let Some(mut d) = cx.tree.find_mut::<SkiaDrawer>(drawer)
&& d.is_open()
&& animated
{
d.set_animated(true);
d.set_is_open(false);
return true;
}
remove_overlay(cx, id, wrapper);
navigated(cx, id, state, &route, NavigationSource::Pop, None);
true
}
fn modal_closed(cx: &mut Cx, id: ControlId, state: &mut dyn Any, wrapper: ControlId) {
let Some(m) = shell(cx.tree, id).and_then(|s| s.modals.iter_mut().find(|m| m.wrapper == wrapper)) else { return };
if m.closing {
remove_overlay(cx, id, wrapper);
let route = current_route(cx.tree, id);
navigated(cx, id, state, &route, NavigationSource::Pop, None);
return;
}
if m.history && back_through_history(cx, id, HistoryKind::Modal) {
set_overlay_history(cx.tree, id, wrapper, false);
return;
}
remove_overlay(cx, id, wrapper);
}
fn show_toast(cx: &mut Cx, id: ControlId, content: Detached, ms: f32) {
close_all_toasts(cx, id);
let Some(layer) = shell(cx.tree, id).and_then(|s| s.parts).map(|p| p.toasts) else { return };
let toast = SkiaLayer::new()
.vertical_options(LayoutOptions::End)
.background_color(SkiaShell::TOAST_BACKGROUND_COLOR)
.block_gestures_below(true)
.use_cache(CacheType::Operations)
.opacity(0)
.children(SkiaLayer::new().children(content));
let toast = cx.add_child(layer, toast);
let animation = cx.animate(toast, TOAST_IN_MS, easing::linear, move |v, cx| {
let height = cx.base(toast).map_or(0.0, |b| b.rect.height() / b.scale.max(0.01));
if let Some(mut c) = cx.any_mut(toast) {
c.set_translation_y(lerp(height, 0.0, v));
c.set_opacity(v);
}
});
cx.start_animator(toast, ValueAnimator::new(0.0, 1.0, 0.0, easing::linear).delay(ms), move |_, cx| {
post(cx, id, Done::ToastDue(toast))
});
if let Some(s) = shell(cx.tree, id) {
s.toasts.push(Overlay { wrapper: toast, content: toast, animation: Some(animation), closing: false, animated: true, history: false });
}
}
fn close_toast(cx: &mut Cx, id: ControlId, toast: ControlId) {
let Some(t) = shell(cx.tree, id).and_then(|s| s.toasts.iter_mut().find(|t| t.wrapper == toast)) else { return };
if std::mem::replace(&mut t.closing, true) {
return;
}
let showing = t.animation.take();
if let Some(showing) = showing {
cx.stop_animation(showing);
}
let (from_y, from_opacity) = cx.base(toast).map_or((0.0, 1.0), |b| (b.p.translation_y, b.p.opacity));
cx.animate(toast, TOAST_OUT_MS, easing::linear, move |v, cx| {
let height = cx.base(toast).map_or(0.0, |b| b.rect.height() / b.scale.max(0.01));
if let Some(mut c) = cx.any_mut(toast) {
c.set_translation_y(lerp(from_y, height, v));
c.set_opacity(lerp(from_opacity, 0.0, v));
}
if v == 1.0 {
post(cx, id, Done::ToastClosed(toast));
}
});
}
fn close_all_toasts(cx: &mut Cx, id: ControlId) {
let toasts: Vec<ControlId> = shell(cx.tree, id).map(|s| s.toasts.drain(..).map(|t| t.wrapper).collect()).unwrap_or_default();
for toast in toasts {
cx.remove(toast);
}
}
fn finish(cx: &mut Cx, id: ControlId, state: &mut dyn Any, done: Done) {
match done {
Done::Pushed(host) => {
let Some(s) = shell(cx.tree, id) else { return };
let found = s.stacks.iter_mut().enumerate().find_map(|(tab, st)| {
let page = st.as_mut()?.pages.iter_mut().find(|p| p.host == host && !p.popping)?;
page.slide = None;
Some((tab, page.route.clone()))
});
if let Some((tab, route)) = found {
show_pages(cx.tree, id, tab);
navigated(cx, id, state, &route, NavigationSource::Push, Some(host));
}
}
Done::Popped(host) => {
let Some(s) = shell(cx.tree, id) else { return };
let found = s.stacks.iter_mut().enumerate().find_map(|(tab, st)| {
let st = st.as_mut()?;
let i = st.pages.iter().position(|p| p.host == host)?;
let (route, view) = below(st, i);
st.pages.remove(i);
Some((tab, route, view))
});
cx.remove(host);
if let Some((tab, route, view)) = found {
show_pages(cx.tree, id, tab);
route_changed(cx, id, state);
navigated(cx, id, state, &route, NavigationSource::Pop, view);
}
}
Done::TabShown(left) => {
if let Some(s) = shell(cx.tree, id) {
s.tab_switch = None;
}
end_tab_switch(cx.tree, id, left);
}
Done::PopupOpened(wrapper) | Done::ModalOpened(wrapper) => {
let Some(s) = shell(cx.tree, id) else { return };
let Some(o) = s.popups.iter_mut().chain(s.modals.iter_mut()).find(|o| o.wrapper == wrapper) else { return };
if o.animation.take().is_none() {
return;
}
let content = o.content;
let route = current_route(cx.tree, id);
navigated(cx, id, state, &route, NavigationSource::Push, Some(content));
}
Done::PopupClosed(wrapper) => {
remove_overlay(cx, id, wrapper);
let route = current_route(cx.tree, id);
navigated(cx, id, state, &route, NavigationSource::Pop, None);
}
Done::ModalClosed(wrapper) => modal_closed(cx, id, state, wrapper),
Done::ToastDue(toast) => close_toast(cx, id, toast),
Done::ToastClosed(toast) => remove_overlay(cx, id, toast),
}
}