1use crate::{Button, Icon};
2use gloo::timers::callback::Timeout;
3use implicit_clone::ImplicitClone;
4use std::cell::RefCell;
5use std::fmt;
6use std::rc::Rc;
7use yew::prelude::*;
8
9pub struct PanelBuilder<F: Fn(Option<Html>, I) -> O, I, O> {
10 title: Option<Html>,
11 input: I,
12 finish: F,
13}
14
15impl<F, I, O> PanelBuilder<F, I, O>
16where
17 F: Fn(Option<Html>, I) -> O,
18{
19 fn new(input: I, finish: F) -> PanelBuilder<F, I, O> {
20 Self {
21 title: None,
22 input,
23 finish,
24 }
25 }
26
27 pub fn with_title(self, title: Html) -> PanelBuilder<F, I, O> {
28 Self {
29 title: Some(title),
30 ..self
31 }
32 }
33
34 pub fn finish(self) -> O {
35 (self.finish)(self.title, self.input)
36 }
37}
38
39#[derive(Clone)]
40pub struct PanelStackState {
41 opened_panels: Rc<RefCell<Vec<(Option<Html>, Html)>>>,
42 version: usize,
43 action: Option<StateAction>,
44}
45
46impl ImplicitClone for PanelStackState {}
47
48impl PanelStackState {
49 pub fn new(content: Html) -> PanelBuilder<fn(Option<Html>, Html) -> Self, Html, Self> {
50 PanelBuilder::new(content, |title, content| {
51 let instance = PanelStackState {
52 opened_panels: Default::default(),
53 version: Default::default(),
54 action: Default::default(),
55 };
56
57 instance.opened_panels.borrow_mut().push((title, content));
58
59 instance
60 })
61 }
62
63 pub fn close_panel(&mut self) -> bool {
64 let opened_panels = self.opened_panels.borrow();
65 if opened_panels.len() > 1 {
66 self.version = self.version.wrapping_add(1);
67 self.action.replace(StateAction::Pop);
68 true
69 } else {
70 false
71 }
72 }
73
74 pub fn open_panel(
75 &mut self,
76 content: Html,
77 ) -> PanelBuilder<
78 fn(Option<Html>, (Html, Rc<RefCell<Vec<(Option<Html>, Html)>>>)) -> bool,
79 (Html, Rc<RefCell<Vec<(Option<Html>, Html)>>>),
80 bool,
81 > {
82 let opened_panels = self.opened_panels.clone();
83 self.version = self.version.wrapping_add(1);
84 self.action.replace(StateAction::Push);
85 PanelBuilder::new(
86 (content, opened_panels),
87 |title, (content, opened_panels)| {
88 opened_panels.borrow_mut().push((title, content));
89 true
90 },
91 )
92 }
93}
94
95impl PartialEq for PanelStackState {
96 fn eq(&self, other: &Self) -> bool {
97 Rc::ptr_eq(&self.opened_panels, &other.opened_panels) && self.version == other.version
98 }
99}
100
101impl fmt::Debug for PanelStackState {
102 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103 write!(f, "PanelStackState({})", self.version)
104 }
105}
106
107#[derive(Debug, Copy, Clone, PartialEq)]
108enum StateAction {
109 Push,
110 Pop,
111}
112
113impl From<StateAction> for Classes {
114 fn from(action: StateAction) -> Self {
115 Classes::from(match action {
116 StateAction::Push => "bp3-panel-stack2-push",
117 StateAction::Pop => "bp3-panel-stack2-pop",
118 })
119 }
120}
121
122pub struct PanelStack {
123 timeout_task: Option<Timeout>,
124
125 action_to_perform: Option<StateAction>,
128}
129
130#[derive(Debug, Clone, PartialEq, Properties)]
131pub struct PanelStackProps {
132 pub state: PanelStackState,
133 #[prop_or_default]
134 pub onclose: Option<Callback<()>>,
135 #[prop_or_default]
136 pub class: Classes,
137}
138
139pub enum PanelStackMessage {
140 PopPanel,
141}
142
143impl Component for PanelStack {
144 type Message = PanelStackMessage;
145 type Properties = PanelStackProps;
146
147 fn create(_ctx: &Context<Self>) -> Self {
148 Self {
149 timeout_task: None,
150 action_to_perform: None,
151 }
152 }
153
154 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
155 match msg {
156 PanelStackMessage::PopPanel => {
157 ctx.props().state.opened_panels.borrow_mut().pop();
158 true
159 }
160 }
161 }
162
163 fn view(&self, ctx: &Context<Self>) -> Html {
164 let opened_panels = ctx.props().state.opened_panels.borrow();
165 let last = match self.action_to_perform {
166 Some(StateAction::Pop) => opened_panels.len() - 2,
167 _ => opened_panels.len() - 1,
168 };
169
170 html! {
171 <div
172 class={classes!(
173 "bp3-panel-stack2",
174 self.action_to_perform,
175 ctx.props().class.clone(),
176 )}
177 >
178 {
179 opened_panels
180 .iter()
181 .enumerate()
182 .rev()
183 .map(|(i, (title, content))| html! {
184 <Panel
185 title={title.clone()}
186 animation={
187 match self.action_to_perform {
188 _ if i == last => Animation::EnterStart,
189 Some(StateAction::Push) if i == last - 1 =>
190 Animation::ExitStart,
191 Some(StateAction::Pop) if i == last + 1 =>
192 Animation::ExitStart,
193 _ => Animation::Exited,
194 }
195 }
196 onclose={(i > 0).then(|| ctx.props().onclose.clone()).flatten()}
197 key={i}
198 >
199 {content.clone()}
202 </Panel>
203 })
204 .collect::<Html>()
205 }
206 </div>
207 }
208 }
209
210 fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
211 self.action_to_perform = ctx.props().state.action;
212 true
213 }
214
215 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
216 if self.action_to_perform.take() == Some(StateAction::Pop) {
217 let link = ctx.link().clone();
218 self.timeout_task.replace(Timeout::new(400, move || {
219 link.send_message(PanelStackMessage::PopPanel)
220 }));
221 }
222 }
223}
224
225struct Panel {
226 animation: Animation,
227 timeout_task: Option<Timeout>,
228}
229
230#[derive(Debug, Clone, PartialEq, Properties)]
231struct PanelProps {
232 title: Option<Html>,
233 animation: Animation,
234 onclose: Option<Callback<()>>,
235 children: Children,
236}
237
238#[derive(Debug, Copy, Clone, PartialEq)]
239enum PanelMessage {
240 UpdateAnimation(Animation),
241}
242
243impl Component for Panel {
244 type Message = PanelMessage;
245 type Properties = PanelProps;
246
247 fn create(ctx: &Context<Self>) -> Self {
248 Self {
249 animation: ctx.props().animation,
250 timeout_task: None,
251 }
252 }
253
254 fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
255 match msg {
256 PanelMessage::UpdateAnimation(animation) => {
257 self.animation = animation;
258 true
259 }
260 }
261 }
262
263 fn view(&self, ctx: &Context<Self>) -> Html {
264 let style = if self.animation == Animation::Exited {
265 "display:none"
266 } else {
267 "display:flex"
268 };
269 let classes = classes!(
270 "bp3-panel-stack-view",
271 match self.animation {
272 Animation::EnterStart => Some("bp3-panel-stack2-enter"),
273 Animation::Entering => Some("bp3-panel-stack2-enter bp3-panel-stack2-enter-active"),
274 Animation::Entered => None,
275 Animation::ExitStart => Some("bp3-panel-stack2-exit"),
276 Animation::Exiting => Some("bp3-panel-stack2-exit bp3-panel-stack2-exit-active"),
277 Animation::Exited => None,
278 }
279 );
280 let back_button = ctx.props().onclose.clone().map(|onclose| {
281 html! {
282 <Button
283 class={classes!("bp3-panel-stack-header-back")}
284 style={"padding-right:0"}
285 icon={Icon::ChevronLeft}
286 minimal={true}
287 small={true}
288 onclick={onclose.reform(|_| ())}
289 >
290 </Button>
293 }
294 });
295
296 html! {
297 <div class={classes} style={style}>
298 <div class="bp3-panel-stack-header">
299 <span>{back_button}</span>
300 {ctx.props().title.clone()}
301 <span/>
302 </div>
303 {for ctx.props().children.iter()}
304 </div>
305 }
306 }
307
308 fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
309 self.animation = ctx.props().animation;
310 true
311 }
312
313 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
314 match self.animation {
315 Animation::EnterStart => {
316 let link = ctx.link().clone();
317 self.timeout_task.replace(Timeout::new(0, move || {
318 link.send_message(PanelMessage::UpdateAnimation(Animation::Entering));
319 }));
320 }
321 Animation::Entering => {
322 let link = ctx.link().clone();
323 self.timeout_task.replace(Timeout::new(400, move || {
324 link.send_message(PanelMessage::UpdateAnimation(Animation::Entered));
325 }));
326 }
327 Animation::Entered => {}
328 Animation::ExitStart => {
329 let link = ctx.link().clone();
330 self.timeout_task.replace(Timeout::new(0, move || {
331 link.send_message(PanelMessage::UpdateAnimation(Animation::Exiting));
332 }));
333 }
334 Animation::Exiting => {
335 let link = ctx.link().clone();
336 self.timeout_task.replace(Timeout::new(400, move || {
337 link.send_message(PanelMessage::UpdateAnimation(Animation::Exited));
338 }));
339 }
340 Animation::Exited => {}
341 }
342 }
343}
344
345#[derive(Debug, Copy, Clone, PartialEq)]
346enum Animation {
347 EnterStart,
348 Entering,
349 Entered,
350 ExitStart,
351 Exiting,
352 Exited,
353}