use std::cell::{Cell, RefCell};
use std::rc::Rc;
use day_core::*;
use day_geometry::{Insets, Proposal, Rect};
use day_reactive::{bind_seeded, untrack};
use day_spec::props::{InspectorPaneProps, InspectorPatch, InspectorProps, PaneEdge};
use day_spec::{Cap, Event, Size, Support, kinds};
use crate::*;
pub const INSPECTOR_WIDTH: f64 = 280.0;
pub struct Inspector<V: Binding<bool>> {
visible: V,
width: f64,
scrollable: bool,
edge: PaneEdge,
half_sheet: bool,
sheet_done: TextSource,
content: AnyPiece,
panel: Rc<dyn Fn() -> AnyPiece>,
}
pub fn inspector<V: Binding<bool>, C: Piece, P: Piece>(
visible: V,
content: C,
panel: impl Fn() -> P + 'static,
) -> Inspector<V> {
Inspector {
visible,
width: INSPECTOR_WIDTH,
scrollable: true,
edge: PaneEdge::Trailing,
half_sheet: false,
sheet_done: "✕".into_text(),
content: AnyPiece::new(content),
panel: Rc::new(move || AnyPiece::new(panel())),
}
}
#[day_macros::builder(InspectorBuilder)]
impl<V: Binding<bool>> Inspector<V> {
pub fn width(mut self, width: f64) -> Self {
self.width = width;
self
}
pub fn not_scrollable(mut self) -> Self {
self.scrollable = false;
self
}
pub fn sheet_done<M>(mut self, t: impl IntoText<M>) -> Self {
self.sheet_done = t.into_text();
self
}
pub fn edge(mut self, edge: PaneEdge) -> Self {
self.edge = edge;
self
}
pub fn half_sheet(mut self) -> Self {
self.half_sheet = true;
self
}
}
impl<V: Binding<bool>> Piece for Inspector<V> {
fn build(self, cx: &mut BuildCx) -> RNode {
if day_core::capability(Cap::Inspector) == Support::Native {
build_native(self, cx)
} else {
build_composed(self, cx)
}
}
}
fn compact(window: RNode) -> bool {
day_core::window_size_class(window).is_some_and(|c| c.width == day_spec::WidthClass::Compact)
}
fn panel_content(panel: AnyPiece, scrollable: bool) -> AnyPiece {
if scrollable {
scroll(panel).any()
} else {
panel
}
}
fn build_native<V: Binding<bool>>(inspector: Inspector<V>, cx: &mut BuildCx) -> RNode {
let Inspector {
visible,
width,
scrollable,
edge,
content,
panel,
..
} = inspector;
let initial = visible.peek();
let sizes: Rc<RefCell<std::collections::HashMap<RNode, Size>>> = Rc::default();
let visible_cell = Rc::new(Cell::new(initial));
let node = cx.native(
kinds::INSPECTOR,
&InspectorProps {
visible: initial,
width,
edge,
},
Rc::new(InspectorLayout {
sizes: sizes.clone(),
visible: visible_cell.clone(),
width,
}),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::Yes,
);
let content_pane = pane(node, false, &sizes);
let mut ccx = BuildCx::new(content_pane);
let _ = content.build(&mut ccx);
let panel_pane = pane(node, true, &sizes);
let mut pcx = BuildCx::new(panel_pane);
let _ = panel_content(panel(), scrollable).build(&mut pcx);
{
let v = visible.clone();
let cell = visible_cell;
bind_seeded(
initial,
move || v.read(),
move |show: &bool| {
cell.set(*show);
with_tree(|t| {
t.patch(node, Box::new(InspectorPatch::Visible(*show)), false);
t.mark_needs_measure(node);
t.mark_layout_dirty();
t.layout_if_needed();
});
},
);
}
cx.on(node, move |ev| {
if let Event::InspectorChanged(show) = ev
&& visible.peek() != *show
{
visible.write(*show);
}
});
node
}
fn pane(
host: RNode,
panel: bool,
sizes: &Rc<RefCell<std::collections::HashMap<RNode, Size>>>,
) -> RNode {
let mut cx = BuildCx::new(host);
let pane = cx.native(
kinds::INSPECTOR_PANE,
&InspectorPaneProps { panel },
Rc::new(PassThrough),
Flex::default(),
Boundary::Yes,
);
let sizes = sizes.clone();
cx.on(pane, move |ev| {
if let Event::FrameChanged(sz) = ev {
let changed = sizes.borrow().get(&pane) != Some(sz);
if changed {
sizes.borrow_mut().insert(pane, *sz);
with_tree(|t| {
t.mark_needs_measure(pane);
t.mark_layout_dirty();
t.layout_if_needed();
});
}
}
});
pane
}
struct SheetOpen<V: Binding<bool>> {
visible: V,
window: RNode,
}
impl<V: Binding<bool>> Clone for SheetOpen<V> {
fn clone(&self) -> Self {
SheetOpen {
visible: self.visible.clone(),
window: self.window,
}
}
}
impl<V: Binding<bool>> Binding<Option<String>> for SheetOpen<V> {
fn read(&self) -> Option<String> {
(self.visible.read() && compact(self.window)).then(|| "inspector".to_string())
}
fn peek(&self) -> Option<String> {
let visible = self.visible.peek();
(visible && untrack(|| compact(self.window))).then(|| "inspector".to_string())
}
fn write(&self, v: Option<String>) {
self.visible.write(v.is_some());
}
}
fn pane_width(requested: f64, total: f64) -> f64 {
let w = if total >= 1000.0 {
requested.max(320.0)
} else {
requested
};
w.min(total * 0.4).max(0.0)
}
fn split_row<C: PieceSeq>(
width: f64,
shown: impl Fn() -> bool + 'static,
children: C,
) -> impl Piece {
piece_fn(move |cx| {
let initial = untrack(&shown);
let pane_is_shown = Rc::new(Cell::new(initial));
let node = cx.layout_only(
Rc::new(SplitLayout {
width,
pane_is_shown: pane_is_shown.clone(),
}),
Flex {
grow_w: true,
grow_h: true,
..Default::default()
},
Boundary::No,
);
cx.under(node, |cx| children.build_each(cx));
bind_seeded(initial, shown, move |show: &bool| {
pane_is_shown.set(*show);
with_tree(|tree| {
tree.mark_needs_measure(node);
tree.mark_layout_dirty();
});
});
node
})
}
struct SplitLayout {
width: f64,
pane_is_shown: Rc<Cell<bool>>,
}
impl day_core::Layout for SplitLayout {
fn measure(&self, cx: &mut dyn day_core::LayoutOps, children: &[RNode], p: Proposal) -> Size {
let content = children.first().copied();
let natural = content
.map(|c| cx.measure_child(c, p))
.unwrap_or(Size::ZERO);
Size::new(
p.width.unwrap_or(natural.width),
p.height.unwrap_or(natural.height),
)
}
fn place(&self, cx: &mut dyn day_core::LayoutOps, children: &[RNode], bounds: Rect) {
let (w, h) = (bounds.size.width, bounds.size.height);
let pane = pane_width(self.width, w);
let side_w = if self.pane_is_shown.get() { pane } else { 0.0 };
if let Some(&c) = children.first() {
cx.place_child(c, Rect::new(0.0, 0.0, (w - side_w).max(0.0), h));
}
if let Some(&s) = children.get(1) {
cx.place_child(s, Rect::new(w - side_w, 0.0, side_w, h));
}
for extra in children.iter().skip(2) {
let m = cx.measure_child(*extra, Proposal::new(Some(w), Some(h)));
cx.place_child(*extra, Rect::new(0.0, 0.0, m.width.min(w), m.height.min(h)));
}
}
}
fn build_composed<V: Binding<bool>>(inspector: Inspector<V>, cx: &mut BuildCx) -> RNode {
let Inspector {
visible,
width,
scrollable,
edge: inspector_edge,
half_sheet,
sheet_done,
content,
panel,
} = inspector;
let window = day_core::window_being_built();
if inspector_edge == PaneEdge::Leading {
let lead_visible = visible;
let lead_panel = panel;
return row((
when(
move || lead_visible.read(),
move || {
row((
panel_content(lead_panel(), scrollable).width(width),
divider(),
))
},
),
content.grow(),
))
.build(cx);
}
let pane_binding = visible.clone();
let show_pane = move || pane_binding.read() && !compact(window);
let side_panel = panel.clone();
let sheet_panel = panel;
let sheet_close = visible.clone();
let done = sheet_done.initial();
split_row(
width,
show_pane.clone(),
(
content.grow(),
column((when(show_pane, move || {
row((
divider().width(1.0).grow_h(),
panel_content(side_panel(), scrollable).grow(),
))
.grow()
}),))
.grow(),
{
let sheet = cover(SheetOpen { visible, window }, move |_: &String| {
let close = sheet_close.clone();
column((
row((
spacer(),
button(done.clone())
.action(move || close.write(false))
.id(day_core::id!(day_inspector_done)),
))
.padding(Insets::symmetric(12.0, 8.0)),
panel_content(sheet_panel(), scrollable).grow(),
))
})
.unrouted();
if half_sheet {
sheet.half_sheet()
} else {
sheet
}
},
),
)
.build(cx)
}
#[cfg(feature = "conformance")]
pub(crate) mod conformance {
use std::rc::Rc;
use day_core::conformance::{Case, Drive};
use day_reactive::Signal;
use day_spec::{Cap, Support, kinds};
use crate::*;
#[day_macros::test(day_core)]
fn inspector_shows_panel() -> Case {
Case::new()
.proves(kinds::INSPECTOR)
.proves(kinds::INSPECTOR_PANE)
.page(|| {
let show = Signal::new(false);
inspector(
show,
column((
label("Content").id(day_core::id!(content)),
button("Toggle")
.action(move || show.update(|v| *v = !*v))
.id(day_core::id!(toggle)),
)),
|| label("Properties").id(day_core::id!(panel)),
)
})
.drive(|d: Drive| async move {
d.assert_text("content", "Content").await?;
d.assert_hidden("panel").await?;
d.tap("toggle").await?;
d.assert_text("panel", "Properties").await?;
d.assert_visible("panel").await?;
d.wait_idle().await?;
d.tap("toggle").await?;
d.assert_hidden("panel").await
})
}
thread_local! {
static HALF_HEIGHTS: (Rc<std::cell::Cell<f64>>, Rc<std::cell::Cell<f64>>) =
(Rc::default(), Rc::default());
}
pub(crate) struct RestoreSizeClass;
impl Drop for RestoreSizeClass {
fn drop(&mut self) {
day_core::restore_reported_size_class();
}
}
#[day_macros::test(day_core)]
fn inspector_half_sheet_on_compact() -> Case {
Case::new()
.proves(kinds::COVER)
.requires_support("composed inspector", || {
if day_core::capability(Cap::Inspector) == Support::Native {
Support::Unsupported
} else {
Support::Native
}
})
.page(|| {
let (page_h, panel_h) = HALF_HEIGHTS.with(|h| (h.0.clone(), h.1.clone()));
page_h.set(0.0);
panel_h.set(0.0);
let show = Signal::new(false);
zstack((
crate::nav::conformance::height_probe(page_h),
inspector(
show,
column((
label("Content").id(day_core::id!(content)),
button("Toggle")
.action(move || show.update(|v| *v = !*v))
.id(day_core::id!(toggle)),
)),
move || {
column((
label("Properties").id(day_core::id!(panel)),
crate::nav::conformance::height_probe(panel_h.clone()),
))
},
)
.not_scrollable()
.half_sheet(),
))
})
.drive(|d: Drive| async move {
let _restore = RestoreSizeClass;
day_core::override_size_class(day_spec::SizeClass {
width: day_spec::WidthClass::Compact,
height: day_spec::HeightClass::Medium,
});
d.wait_idle().await?;
d.assert_missing("panel").await?;
d.tap("toggle").await?;
d.assert_text("panel", "Properties").await?;
d.wait_idle().await?;
let heights = HALF_HEIGHTS.with(|h| h.clone());
crate::nav::conformance::check_half(&d, "inspector half sheet panel", &heights)
.await?;
d.assert_text("content", "Content").await?;
d.tap("toggle").await?;
d.assert_missing("panel").await
})
}
}
#[cfg(test)]
mod pane_width_tests {
use super::pane_width;
#[test]
fn the_pane_widens_on_a_wide_window_and_yields_on_a_narrow_one() {
assert_eq!(
pane_width(280.0, 1060.0),
320.0,
"a landscape tablet gets room"
);
assert_eq!(pane_width(360.0, 1280.0), 360.0, "a wider ask is kept");
assert_eq!(
pane_width(280.0, 960.0),
280.0,
"a desktop window keeps its ask"
);
assert_eq!(
pane_width(280.0, 600.0),
240.0,
"never more than two fifths"
);
}
}