perspective_viewer/components/
status_bar.rs1use wasm_bindgen::JsCast;
14use web_sys::*;
15use yew::prelude::*;
16
17use super::status_indicator::StatusIndicator;
18use super::style::LocalStyle;
19use crate::components::containers::select::*;
20use crate::components::status_bar_counter::StatusBarRowsCounter;
21use crate::custom_elements::copy_dropdown::*;
22use crate::custom_elements::export_dropdown::*;
23use crate::presentation::Presentation;
24use crate::renderer::*;
25use crate::session::*;
26#[cfg(test)]
27use crate::utils::WeakScope;
28use crate::utils::*;
29use crate::*;
30
31#[derive(Properties, Clone)]
32pub struct StatusBarProps {
33 pub id: String,
34 pub on_reset: Callback<bool>,
35 pub session: Session,
36 pub renderer: Renderer,
37 pub presentation: Presentation,
38
39 #[cfg(test)]
40 #[prop_or_default]
41 pub weak_link: WeakScope<StatusBar>,
42}
43
44derive_model!(Renderer, Session, Presentation for StatusBarProps);
45
46impl PartialEq for StatusBarProps {
47 fn eq(&self, other: &Self) -> bool {
48 self.id == other.id
49 }
50}
51
52pub enum StatusBarMsg {
53 Reset(bool),
54 Export,
55 Copy,
56 Noop,
57 SetThemeConfig((Vec<String>, Option<usize>)),
58 SetTheme(String),
59 SetTitle(Option<String>),
63}
64
65pub struct StatusBar {
67 is_updating: i32,
68 theme: Option<String>,
69 themes: Vec<String>,
70 export_ref: NodeRef,
71 copy_ref: NodeRef,
72 _sub: [Subscription; 2],
73}
74
75impl Component for StatusBar {
76 type Message = StatusBarMsg;
77 type Properties = StatusBarProps;
78
79 fn create(ctx: &Context<Self>) -> Self {
80 let _sub = [
81 ctx.props()
82 .presentation
83 .theme_config_updated
84 .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
85 ctx.props()
86 .presentation
87 .title_changed
88 .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
89 ];
90
91 let presentation = ctx.props().presentation.clone();
93 let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
94 ApiFuture::spawn(async move {
95 on_theme.emit(presentation.get_selected_theme_config().await?);
96 Ok(())
97 });
98
99 Self {
100 _sub,
101 theme: None,
102 themes: vec![],
103 copy_ref: NodeRef::default(),
104 export_ref: NodeRef::default(),
105 is_updating: 0,
106 }
107 }
108
109 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
110 match msg {
111 StatusBarMsg::Reset(all) => {
112 ctx.props().on_reset.emit(all);
113 false
114 },
115 StatusBarMsg::SetThemeConfig((themes, index)) => {
116 let new_theme = index.and_then(|x| themes.get(x)).cloned();
117 let should_render = new_theme != self.theme || self.themes != themes;
118 self.theme = new_theme;
119 self.themes = themes;
120 should_render
121 },
122 StatusBarMsg::SetTheme(theme_name) => {
123 clone!(
124 ctx.props().renderer,
125 ctx.props().session,
126 ctx.props().presentation
127 );
128 ApiFuture::spawn(async move {
129 presentation.set_theme_name(Some(&theme_name)).await?;
130 let view = session.get_view().into_apierror()?;
131 renderer.restyle_all(&view).await
132 });
133
134 false
135 },
136 StatusBarMsg::Export => {
137 let target = self.export_ref.cast::<HtmlElement>().unwrap();
138 ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
139 false
140 },
141 StatusBarMsg::Copy => {
142 let target = self.copy_ref.cast::<HtmlElement>().unwrap();
143 CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
144 false
145 },
146 StatusBarMsg::Noop => true,
147 StatusBarMsg::SetTitle(title) => {
148 ctx.props().presentation.set_title(title);
149 false
150 },
151 }
152 }
153
154 fn view(&self, ctx: &Context<Self>) -> Html {
155 let mut is_updating_class_name = classes!();
156 if self.is_updating > 0 {
157 is_updating_class_name.push("updating")
158 };
159
160 if ctx.props().presentation.get_title().is_some() {
161 is_updating_class_name.push("titled")
162 };
163
164 let reset = ctx
165 .link()
166 .callback(|event: MouseEvent| StatusBarMsg::Reset(event.shift_key()));
167
168 let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
169 let copy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
170 let theme_button = match &self.theme {
171 None => html! {},
172 Some(selected) => {
173 let ontheme = ctx.link().callback(StatusBarMsg::SetTheme);
174 let values = self
175 .themes
176 .iter()
177 .cloned()
178 .map(SelectItem::Option)
179 .collect::<Vec<_>>();
180
181 html! {
182 if values.len() > 1 {
183 <span class="hover-target">
184 <span id="theme" class="button">
185 <Select<String>
186 id="theme_selector"
187 {values}
188 selected={selected.to_owned()}
189 on_select={ontheme}
190 />
191 </span>
192 </span>
193 }
194 }
195 },
196 };
197
198 let oninput = ctx.link().callback({
199 move |input: InputEvent| {
200 let title = input
201 .target()
202 .unwrap()
203 .unchecked_into::<HtmlInputElement>()
204 .value();
205
206 let title = if title.trim().is_empty() {
207 None
208 } else {
209 Some(title)
210 };
211
212 StatusBarMsg::SetTitle(title)
213 }
214 });
215
216 let is_menu = ctx.props().session.has_table()
217 && (ctx.props().presentation.is_settings_open()
218 || ctx.props().presentation.get_title().is_some());
219
220 if !ctx.props().session.has_table() {
221 is_updating_class_name.push("updating");
222 }
223
224 html! {
225 <>
226 <LocalStyle href={css!("status-bar")} />
227 <div id={ctx.props().id.clone()} class={is_updating_class_name}>
228 <StatusIndicator
229 session={&ctx.props().session}
230 renderer={&ctx.props().renderer}
231 />
232 if is_menu {
233 <label
234 class="input-sizer"
235 data-value={ctx.props().presentation.get_title().unwrap_or_default()}
236 >
237 <input
238 placeholder=" "
239 value={ctx.props().presentation.get_title()}
240 size="10"
241 {oninput}
242 />
243 <span id="status-bar-placeholder" />
244 </label>
245 }
246 <StatusBarRowsCounter session={&ctx.props().session} />
247 <div id="spacer" />
248 if is_menu {
249 <div id="menu-bar" class="section">
250 { theme_button }
251 <div id="plugin-settings"><slot name="plugin-settings" /></div>
252 <span class="hover-target">
253 <span id="reset" class="button" onmousedown={reset}><span /></span>
254 </span>
255 <span class="hover-target" ref={&self.export_ref} onmousedown={export}>
256 <span id="export" class="button"><span /></span>
257 </span>
258 <span class="hover-target" ref={&self.copy_ref} onmousedown={copy}>
259 <span id="copy" class="button"><span /></span>
260 </span>
261 </div>
262 }
263 </div>
264 </>
265 }
266 }
267}