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