Skip to main content

perspective_viewer/components/containers/
sidebar.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use perspective_client::clone;
14use wasm_bindgen::JsCast;
15use wasm_bindgen::prelude::Closure;
16use web_sys::HtmlElement;
17use yew::{
18    Callback, Children, Html, Properties, function_component, html, use_effect_with, use_mut_ref,
19    use_node_ref,
20};
21
22use crate::components::containers::sidebar_close_button::SidebarCloseButton;
23use crate::components::editable_header::{EditableHeader, EditableHeaderProps};
24use crate::js::{ResizeObserver, ResizeObserverEntry};
25
26#[derive(PartialEq, Clone, Properties)]
27pub struct SidebarProps {
28    /// The component's children.
29    pub children: Children,
30
31    /// When this callback is called, the sidebar will close
32    pub on_close: Callback<()>,
33    pub id_prefix: String,
34    pub width_override: Option<i32>,
35    pub selected_tab: Option<usize>,
36    pub header_props: EditableHeaderProps,
37
38    /// Trap-door width shared across this sidebar's tabs: the lifted
39    /// running max of the widths this component reports through
40    /// `on_auto_width`. Held by the parent (ultimately
41    /// `PerspectiveViewer`'s geometry state, like the settings panel's
42    /// Query/Plugin/Debug trap-door) so it survives tab switches AND
43    /// sidebar re-mounts, and clears on divider reset.
44    #[prop_or_default]
45    pub auto_width: f64,
46
47    /// Fires with the sidebar's rendered width after each render; the
48    /// owner keeps the running max threaded back as `auto_width`.
49    #[prop_or_default]
50    pub on_auto_width: Callback<f64>,
51
52    /// Pinned state for the header's pin toggle; the button renders only
53    /// when `on_toggle_pin` is provided.
54    #[prop_or_default]
55    pub is_pinned: bool,
56
57    #[prop_or_default]
58    pub on_toggle_pin: Option<Callback<()>>,
59}
60
61/// Sidebars are designed to live in a
62/// [`super::split_panel::SplitPanel`]
63#[function_component]
64pub fn Sidebar(p: &SidebarProps) -> Html {
65    let id = &p.id_prefix;
66    let noderef = use_node_ref();
67
68    // The trap-door reports the sidebar's rendered width to its owner via
69    // a `ResizeObserver` on the sidebar element, NOT a render effect:
70    // width changes are driven by DOM mutations anywhere in the tab
71    // subtree (e.g. the window editor staging a long column name into a
72    // slot), which need not re-render this component at all. The observer
73    // sees every one. `contentRect` (not the border box) is load-bearing:
74    // the sizer below is a content child, so ratcheting the border box
75    // would feed any sidebar padding back into unbounded growth. A manual
76    // divider drag (`width_override`) disables the ratchet until the
77    // divider resets.
78    let live_props = use_mut_ref(|| (Callback::<f64>::default(), None::<i32>));
79    *live_props.borrow_mut() = (p.on_auto_width.clone(), p.width_override);
80    use_effect_with((), {
81        clone!(noderef, live_props);
82        move |_| {
83            let closure =
84                Closure::<dyn FnMut(js_sys::Array)>::new(move |entries: js_sys::Array| {
85                    let (on_auto_width, width_override) = live_props.borrow().clone();
86                    if width_override.is_none() {
87                        for entry in entries.iter() {
88                            let entry: ResizeObserverEntry = entry.unchecked_into();
89                            on_auto_width.emit(entry.content_rect().width());
90                        }
91                    }
92                });
93
94            let observer = ResizeObserver::new(closure.as_ref().unchecked_ref());
95            let elem = noderef.cast::<HtmlElement>();
96            if let Some(elem) = &elem {
97                observer.observe(elem);
98            }
99
100            move || {
101                if let Some(elem) = &elem {
102                    observer.unobserve(elem);
103                }
104
105                drop(closure);
106            }
107        }
108    });
109
110    let auto_width = if p.width_override.is_none() {
111        p.auto_width
112    } else {
113        0.0
114    };
115
116    let width_style = format!("min-width: 200px; width: {}px", auto_width);
117    let pin_button = p.on_toggle_pin.as_ref().map(|cb| {
118        let onclick = {
119            let cb = cb.clone();
120            Callback::from(move |_: web_sys::MouseEvent| cb.emit(()))
121        };
122
123        let mut class = yew::classes!("sidebar_pin_button");
124        if p.is_pinned {
125            class.push("is-pinned");
126        }
127
128        html! {
129            <span
130                id={format!("{id}_pin_button")}
131                {class}
132                title={if p.is_pinned { "Unpin" } else { "Pin" }}
133                {onclick}
134            />
135        }
136    });
137
138    html! {
139        <>
140            <SidebarCloseButton id={format!("{id}_close_button")} on_close_sidebar={&p.on_close} />
141            <div class="sidebar_column" id={format!("{id}_sidebar")} ref={noderef}>
142                <div class="sidebar_header">
143                    <EditableHeader ..p.header_props.clone() />
144                    { pin_button }
145                </div>
146                <div class="sidebar_border" id={format!("{id}_border")} />
147                { p.children.iter().collect::<Html>() }
148                <div class="sidebar-auto-width" style={width_style} />
149            </div>
150        </>
151    }
152}