use crate::core::{Color, HorizontalAlignment, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::cupertino::CupertinoNavigationBar;
use crate::widget::metrics::role_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct CupertinoNavigationStack {
base: BaseWidget,
titles: Vec<String>,
animated: bool,
pub popped: Signal1<String>,
pub stack_changed: Signal1<usize>,
}
impl CupertinoNavigationStack {
pub fn new(geometry: Rect, root: impl Into<String>) -> Self {
Self {
base: BaseWidget::new(
WidgetKind::NavigationStack,
geometry,
"CupertinoNavigationStack",
),
titles: vec![root.into()],
animated: true,
popped: Signal1::new(),
stack_changed: Signal1::new(),
}
}
pub fn depth(&self) -> usize {
self.titles.len()
}
pub fn current_title(&self) -> &str {
self.titles.last().map(String::as_str).unwrap_or("")
}
pub fn animated(&self) -> bool {
self.animated
}
pub fn set_animated(&mut self, animated: bool) {
self.animated = animated;
}
pub fn titles(&self) -> &[String] {
&self.titles
}
pub fn push(&mut self, title: &str) {
self.titles.push(title.to_string());
self.base.request_redraw();
self.stack_changed.emit(self.titles.len());
}
pub fn pop(&mut self) -> bool {
if self.titles.len() <= 1 {
return false;
}
let Some(title) = self.titles.pop() else {
return false;
};
self.base.request_redraw();
self.popped.emit(title);
self.stack_changed.emit(self.titles.len());
true
}
pub fn pop_to_root(&mut self) {
if self.titles.len() <= 1 {
return;
}
let top = self.titles.last().cloned().unwrap_or_default();
self.titles.truncate(1);
self.base.request_redraw();
self.popped.emit(top);
self.stack_changed.emit(self.titles.len());
}
pub fn set_root_title(&mut self, title: &str) {
if let Some(root) = self.titles.first_mut() {
*root = title.to_string();
self.base.request_redraw();
}
}
pub fn handle_back(&mut self) -> bool {
self.pop()
}
pub fn bind_navigation_bar(&mut self, bar: &mut CupertinoNavigationBar) {
bar.set_title(self.current_title());
bar.show_back_button(self.depth() > 1);
if let Some(parent) = self.titles.iter().rev().nth(1) {
bar.set_back_button_text(parent);
}
self.base.request_redraw();
}
}
impl Widget for CupertinoNavigationStack {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> Size {
Size::new(160, 24)
}
impl_draw_bridge!();
}
impl EventHandler for CupertinoNavigationStack {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if let Event::MousePress { pos, button: 1, .. } = event {
if self.geometry().contains_point(*pos) {
self.handle_back();
}
}
}
}
impl Draw for CupertinoNavigationStack {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let style = self.base.style();
let ink = style.text_color.unwrap_or(Color::rgba(30, 30, 30, 255));
let surface = style.background_color.unwrap_or(Color::rgba(245, 245, 245, 255));
let font = &role_font(style, 13.0);
context.fill_rect(rect, surface);
let dot = 5i32;
let mut x = rect.x + 4;
for index in 0..self.titles.len() {
let active = index + 1 == self.titles.len();
context.fill_circle(
crate::core::Point::new(x, rect.y + rect.height as i32 / 2),
dot as u32,
if active { ink } else { ink.with_alpha(90) },
);
x += dot * 3;
}
context.draw_text_fitted_cow(
Rect::new(
x + 4,
rect.y + 2,
rect.right().saturating_sub(x + 4) as u32,
rect.height.saturating_sub(4),
),
self.current_title(),
font,
ink,
HorizontalAlignment::Left,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_pop_and_root_guard() {
let mut stack = CupertinoNavigationStack::new(Rect::new(0, 0, 160, 24), "Home");
assert_eq!(stack.depth(), 1);
assert!(!stack.pop(), "the root cannot be popped");
stack.push("Detail");
assert_eq!(stack.current_title(), "Detail");
let popped = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let sink = popped.clone();
stack.popped.connect(move |title: std::sync::Arc<String>| {
*sink.lock().unwrap() = (*title).clone();
});
assert!(stack.pop());
assert_eq!(*popped.lock().unwrap(), "Detail");
assert_eq!(stack.current_title(), "Home");
}
}