use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{Scope, Signal};
use crate::theme::TokenSet;
use crate::ui::{
dyn_view, dyn_view_scoped, Element, EventCtx, Key, KeyChord, Mods, MouseButton, MouseKind,
Phase, UiEvent, View, ViewId,
};
#[path = "page_host_bar.rs"]
mod bar;
type PageBuilder = Box<dyn FnMut(Scope) -> View>;
type BadgeFn = Box<dyn Fn() -> Option<String>>;
type ChangeBox = Box<dyn FnMut(&str)>;
type ChangeFn = Rc<RefCell<Option<ChangeBox>>>;
struct PageDef {
id: String,
title: String,
badge: Option<BadgeFn>,
build: PageBuilder,
}
pub struct PageHost {
pages: Vec<PageDef>,
active: Option<Signal<String>>,
initial: Option<String>,
on_change: Option<ChangeBox>,
prev_chords: Vec<KeyChord>,
next_chords: Vec<KeyChord>,
number_jump: bool,
layout: Option<LayoutStyle>,
}
fn idx_of(ids: &[String], id: &str) -> usize {
ids.iter().position(|p| p == id).unwrap_or(0)
}
impl PageHost {
pub fn new() -> PageHost {
PageHost {
pages: Vec::new(),
active: None,
initial: None,
on_change: None,
prev_chords: vec![KeyChord::new(Mods::CTRL, Key::PageUp)],
next_chords: vec![KeyChord::new(Mods::CTRL, Key::PageDown)],
number_jump: false,
layout: None,
}
}
pub fn page(
mut self,
id: impl Into<String>,
title: impl Into<String>,
build: impl FnMut(Scope) -> View + 'static,
) -> PageHost {
let id = id.into();
debug_assert!(
!self.pages.iter().any(|p| p.id == id),
"PageHost: duplicate page id {id:?}"
);
self.pages.push(PageDef {
id,
title: title.into(),
badge: None,
build: Box::new(build),
});
self
}
pub fn badge(mut self, id: &str, badge: impl Fn() -> Option<String> + 'static) -> PageHost {
match self.pages.iter_mut().find(|p| p.id == id) {
Some(p) => p.badge = Some(Box::new(badge)),
None => debug_assert!(false, "PageHost::badge: unknown page id {id:?}"),
}
self
}
pub fn active(mut self, active: Signal<String>) -> PageHost {
self.active = Some(active);
self
}
pub fn initial(mut self, id: impl Into<String>) -> PageHost {
self.initial = Some(id.into());
self
}
pub fn on_change(mut self, f: impl FnMut(&str) + 'static) -> PageHost {
self.on_change = Some(Box::new(f));
self
}
pub fn chords(mut self, prev: &[KeyChord], next: &[KeyChord]) -> PageHost {
self.prev_chords = prev.to_vec();
self.next_chords = next.to_vec();
self
}
pub fn number_jump(mut self, on: bool) -> PageHost {
self.number_jump = on;
self
}
pub fn layout(mut self, layout: LayoutStyle) -> PageHost {
self.layout = Some(layout);
self
}
pub fn view(self, cx: Scope) -> View {
self.assemble(cx, None).build()
}
pub fn element(self, cx: Scope, t: &TokenSet) -> Element {
self.assemble(cx, Some(*t))
}
fn assemble(self, cx: Scope, fixed: Option<TokenSet>) -> Element {
let n = self.pages.len();
let mut ids = Vec::with_capacity(n);
let mut titles = Vec::with_capacity(n);
let mut badges = Vec::with_capacity(n);
let mut builders = Vec::with_capacity(n);
for p in self.pages {
ids.push(p.id);
titles.push(p.title);
badges.push(p.badge);
builders.push(p.build);
}
let ids: Rc<Vec<String>> = Rc::new(ids);
let titles: Rc<Vec<String>> = Rc::new(titles);
let badges: Rc<Vec<Option<BadgeFn>>> = Rc::new(badges);
let builders = Rc::new(RefCell::new(builders));
if let (Some(want), None) = (&self.initial, &self.active) {
debug_assert!(
ids.contains(want),
"PageHost::initial: unknown page id {want:?}"
);
}
let start = self
.initial
.filter(|want| ids.contains(want))
.or_else(|| ids.first().cloned())
.unwrap_or_default();
let active = self.active.unwrap_or_else(|| cx.signal(start));
let on_change: ChangeFn = Rc::new(RefCell::new(self.on_change));
let switch: Rc<dyn Fn(usize)> = {
let ids = ids.clone();
let on_change = on_change.clone();
Rc::new(move |target: usize| {
if ids.is_empty() {
return;
}
let target = target.min(ids.len() - 1);
let cur = active.with_untracked(|id| idx_of(&ids, id));
if cur == target {
return;
}
active.set(ids[target].clone());
if let Some(f) = on_change.borrow_mut().as_mut() {
f(ids[target].as_str());
}
})
};
let switch_rel: Rc<dyn Fn(i32)> = {
let ids = ids.clone();
let switch = switch.clone();
Rc::new(move |dir: i32| {
let n = ids.len();
if n == 0 {
return;
}
let cur = active.with_untracked(|id| idx_of(&ids, id)) as i32;
switch((cur + dir).rem_euclid(n as i32) as usize);
})
};
let bar_state = Rc::new(RefCell::new(bar::BarModel {
items: Vec::new(),
active: 0,
}));
let window_first = Rc::new(Cell::new(0usize));
let drawn_plan: Rc<RefCell<Option<(bar::BarPlan, i32)>>> = Rc::new(RefCell::new(None));
let bar_handler = {
let state = bar_state.clone();
let first = window_first.clone();
let drawn = drawn_plan.clone();
let switch = switch.clone();
let switch_rel = switch_rel.clone();
move |ctx: &mut EventCtx, ev: &UiEvent| match ev {
UiEvent::Key(k) => {
if k.mods != Mods::NONE {
return;
}
match k.key {
Key::Left => switch_rel(-1),
Key::Right => switch_rel(1),
_ => return,
}
ctx.stop_propagation();
}
UiEvent::Mouse(m) => {
if let MouseKind::Down(MouseButton::Left) = m.kind {
let rect = ctx.current_rect();
let hit = {
let stashed = drawn.borrow().clone();
let (plan, avail) = stashed.unwrap_or_else(|| {
let model = state.borrow();
(bar::plan_bar(&model, first.get(), rect.w), rect.w)
});
bar::hit_bar(&plan, avail, m.pos.x - rect.x)
};
match hit {
bar::BarHit::Prev => switch_rel(-1),
bar::BarHit::Next => switch_rel(1),
bar::BarHit::Tab(i) => switch(i),
bar::BarHit::Miss => return,
}
ctx.stop_propagation();
}
}
_ => {}
}
};
let access_value = {
let ids = ids.clone();
let titles = titles.clone();
let badges = badges.clone();
move || {
if titles.is_empty() {
return String::new();
}
let idx = active.with_untracked(|id| idx_of(&ids, id));
let mut s = format!("{} ({}/{})", titles[idx], idx + 1, titles.len());
if let Some(getter) = badges.get(idx).and_then(|g| g.as_ref()) {
if let Some(b) = crate::reactive::untrack(getter) {
s.push_str(" [");
s.push_str(&b);
s.push(']');
}
}
s
}
};
let bar_dyn = {
let ids = ids.clone();
let titles = titles.clone();
let badges = badges.clone();
let state = bar_state.clone();
let first = window_first.clone();
let drawn = drawn_plan.clone();
dyn_view(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(2)),
move || {
let t = fixed.unwrap_or_else(|| crate::widgets::theme_tokens(cx));
let active_idx = idx_of(&ids, &active.get());
let items: Vec<bar::BarItem> = titles
.iter()
.enumerate()
.map(|(i, title)| bar::BarItem {
title: title.clone(),
badge: badges[i].as_ref().and_then(|f| f()),
})
.collect();
*state.borrow_mut() = bar::BarModel {
items,
active: active_idx,
};
let ink = bar::ink_from(&t);
let state = state.clone();
let first = first.clone();
let drawn = drawn.clone();
Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Cells(2)),
)
.draw(move |canvas, rect| {
let m = state.borrow();
let plan = bar::plan_bar(&m, first.get(), rect.w);
first.set(plan.first);
*drawn.borrow_mut() = Some((plan.clone(), rect.w));
bar::draw_bar(canvas, rect, &m, &plan, &ink);
})
.build()
},
)
};
let bar_el = Element::new()
.style(
LayoutStyle::default()
.height(Dimension::Cells(2))
.shrink(0.0),
)
.role(crate::ui::Role::Tabs)
.access_value(access_value)
.focusable()
.on(Phase::Bubble, bar_handler)
.child(bar_dyn);
let page_dyn = {
let ids = ids.clone();
dyn_view_scoped(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.grow(1.0),
move |gen_cx| {
let idx = idx_of(&ids, &active.get());
match builders.borrow_mut().get_mut(idx) {
Some(build) => build(gen_cx),
None => crate::ui::text(""),
}
},
)
};
let anchor: Rc<Cell<Option<ViewId>>> = Rc::new(Cell::new(None));
let mut root = Element::new()
.style(self.layout.unwrap_or_else(LayoutStyle::column))
.on(Phase::Capture, {
let anchor = anchor.clone();
move |ctx: &mut EventCtx, ev: &UiEvent| {
if matches!(ev, UiEvent::Key(_)) {
anchor.set(ctx.current());
}
}
})
.on(Phase::Capture, {
let prev = self.prev_chords.clone();
let next = self.next_chords.clone();
let switch_rel = switch_rel.clone();
let anchor = anchor.clone();
move |ctx: &mut EventCtx, ev: &UiEvent| {
let UiEvent::Key(k) = ev else { return };
let chord = k.chord().normalized();
let dir = if next.iter().any(|c| c.normalized() == chord) {
1
} else if prev.iter().any(|c| c.normalized() == chord) {
-1
} else {
return;
};
switch_rel(dir);
if let Some(id) = anchor.get() {
ctx.request_focus(id);
}
ctx.stop_propagation();
}
});
for c in &self.next_chords {
let switch_rel = switch_rel.clone();
let anchor = anchor.clone();
root = root.shortcut_labeled(*c, "next page", move |ctx| {
switch_rel(1);
if let Some(id) = anchor.get() {
ctx.request_focus(id);
}
});
}
for c in &self.prev_chords {
let switch_rel = switch_rel.clone();
let anchor = anchor.clone();
root = root.shortcut_labeled(*c, "previous page", move |ctx| {
switch_rel(-1);
if let Some(id) = anchor.get() {
ctx.request_focus(id);
}
});
}
if self.number_jump {
for i in 0..n.min(9) {
let digit = char::from_digit(i as u32 + 1, 10).expect("digits 1-9");
let switch = switch.clone();
let anchor = anchor.clone();
root = root.shortcut_labeled(
KeyChord::plain(Key::Char(digit)),
format!("page {}: {}", i + 1, titles[i]),
move |ctx| {
switch(i);
if let Some(id) = anchor.get() {
ctx.request_focus(id);
}
},
);
}
}
root.child(bar_el.build()).child(page_dyn)
}
}
impl Default for PageHost {
fn default() -> Self {
PageHost::new()
}
}
#[cfg(test)]
#[path = "page_host_tests.rs"]
mod tests;