perspective_viewer/components/
datetime_column_style.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
13mod custom;
14mod simple;
15
16use std::sync::{Arc, LazyLock};
17
18use derivative::Derivative;
19use perspective_js::json;
20use perspective_js::utils::global::navigator;
21use wasm_bindgen::prelude::*;
22use yew::prelude::*;
23use yew::*;
24
25use super::form::color_selector::*;
26use super::modal::{ModalLink, SetModalLink};
27use super::style::LocalStyle;
28use crate::components::datetime_column_style::custom::DatetimeStyleCustom;
29use crate::components::datetime_column_style::simple::DatetimeStyleSimple;
30use crate::components::form::select_field::{SelectEnumField, SelectValueField};
31use crate::config::*;
32use crate::utils::WeakScope;
33use crate::*;
34
35#[wasm_bindgen]
36extern "C" {
37    #[wasm_bindgen(js_name = supportedValuesOf, js_namespace = Intl)]
38    pub fn supported_values_of(s: &JsValue) -> js_sys::Array;
39}
40
41static ALL_TIMEZONES: LazyLock<Arc<Vec<String>>> = LazyLock::new(|| {
42    Arc::new(
43        supported_values_of(&JsValue::from("timeZone"))
44            .iter()
45            .map(|x| x.as_string().unwrap())
46            .collect(),
47    )
48});
49
50static USER_TIMEZONE: LazyLock<String> = LazyLock::new(|| {
51    js_sys::Reflect::get(
52        &js_sys::Intl::DateTimeFormat::new(&navigator().languages(), &json!({})).resolved_options(),
53        &JsValue::from("timeZone"),
54    )
55    .unwrap()
56    .as_string()
57    .unwrap()
58});
59
60pub enum DatetimeColumnStyleMsg {
61    SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig),
62    CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig),
63    TimezoneChanged(Option<String>),
64    ColorModeChanged(Option<DatetimeColorMode>),
65    ColorChanged(String),
66}
67
68#[derive(Properties, Derivative)]
69#[derivative(Debug)]
70pub struct DatetimeColumnStyleProps {
71    pub enable_time_config: bool,
72
73    pub config: Option<DatetimeColumnStyleConfig>,
74
75    pub default_config: DatetimeColumnStyleDefaultConfig,
76
77    #[prop_or_default]
78    pub on_change: Callback<ColumnConfigValueUpdate>,
79
80    #[prop_or_default]
81    #[derivative(Debug = "ignore")]
82    weak_link: WeakScope<DatetimeColumnStyle>,
83}
84
85impl ModalLink<DatetimeColumnStyle> for DatetimeColumnStyleProps {
86    fn weak_link(&self) -> &'_ WeakScope<DatetimeColumnStyle> {
87        &self.weak_link
88    }
89}
90
91impl PartialEq for DatetimeColumnStyleProps {
92    fn eq(&self, _other: &Self) -> bool {
93        false
94    }
95}
96
97/// Column style controls for the `datetime` type.
98#[derive(Debug)]
99pub struct DatetimeColumnStyle {
100    config: DatetimeColumnStyleConfig,
101    default_config: DatetimeColumnStyleDefaultConfig,
102}
103
104impl DatetimeColumnStyle {
105    /// When this config has changed, we must signal the wrapper element.
106    fn dispatch_config(&self, ctx: &Context<Self>) {
107        let update =
108            Some(self.config.clone()).filter(|x| x != &DatetimeColumnStyleConfig::default());
109        ctx.props()
110            .on_change
111            .emit(ColumnConfigValueUpdate::DatagridDatetimeStyle(update));
112    }
113
114    /// Generate a color selector component for a specific `StringColorMode`
115    /// variant.
116    fn color_select_row(&self, ctx: &Context<Self>, mode: &DatetimeColorMode, title: &str) -> Html {
117        let on_color = ctx.link().callback(DatetimeColumnStyleMsg::ColorChanged);
118        let color = self
119            .config
120            .color
121            .clone()
122            .unwrap_or_else(|| self.default_config.color.to_owned());
123
124        let color_props = props!(ColorProps {
125            title: title.to_owned(),
126            color,
127            on_color
128        });
129
130        if &self.config.datetime_color_mode == mode {
131            html! { <div class="row"><ColorSelector ..color_props /></div> }
132        } else {
133            html! {}
134        }
135    }
136}
137
138impl Component for DatetimeColumnStyle {
139    type Message = DatetimeColumnStyleMsg;
140    type Properties = DatetimeColumnStyleProps;
141
142    fn create(ctx: &Context<Self>) -> Self {
143        ctx.set_modal_link();
144        Self {
145            config: ctx.props().config.clone().unwrap_or_default(),
146            default_config: ctx.props().default_config.clone(),
147        }
148    }
149
150    // Always re-render when config changes.
151    fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
152        let mut rerender = false;
153        let mut new_config = ctx.props().config.clone().unwrap_or_default();
154        if self.config != new_config {
155            std::mem::swap(&mut self.config, &mut new_config);
156            rerender = true;
157        }
158        if old.enable_time_config != ctx.props().enable_time_config {
159            rerender = true;
160        }
161        rerender
162    }
163
164    // TODO could be more conservative here with re-rendering
165    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
166        match msg {
167            DatetimeColumnStyleMsg::TimezoneChanged(val) => {
168                if Some(&*USER_TIMEZONE) != val.as_ref() {
169                    *self.config.date_format.time_zone_mut() = val;
170                } else {
171                    *self.config.date_format.time_zone_mut() = None;
172                }
173
174                self.dispatch_config(ctx);
175                true
176            },
177            DatetimeColumnStyleMsg::ColorModeChanged(mode) => {
178                self.config.datetime_color_mode = mode.unwrap_or_default();
179                self.dispatch_config(ctx);
180                true
181            },
182            DatetimeColumnStyleMsg::ColorChanged(color) => {
183                self.config.color = Some(color);
184                self.dispatch_config(ctx);
185                true
186            },
187
188            DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(simple) => {
189                self.config.date_format = DatetimeFormatType::Simple(simple);
190                self.dispatch_config(ctx);
191                true
192            },
193            DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(custom) => {
194                self.config.date_format = DatetimeFormatType::Custom(custom);
195                self.dispatch_config(ctx);
196                true
197            },
198        }
199    }
200
201    fn view(&self, ctx: &Context<Self>) -> Html {
202        let selected_color_mode = self.config.datetime_color_mode;
203        let color_mode_changed = ctx
204            .link()
205            .callback(DatetimeColumnStyleMsg::ColorModeChanged);
206
207        let color_controls = match selected_color_mode {
208            DatetimeColorMode::None => html! {},
209            DatetimeColorMode::Foreground => {
210                self.color_select_row(ctx, &DatetimeColorMode::Foreground, "foreground-label")
211            },
212            DatetimeColorMode::Background => {
213                self.color_select_row(ctx, &DatetimeColorMode::Background, "background-label")
214            },
215        };
216
217        html! {
218            <>
219                <LocalStyle href={css!("column-style")} />
220                <div id="column-style-container" class="datetime-column-style-container">
221                    <SelectEnumField<DatetimeColorMode>
222                        label="color"
223                        on_change={color_mode_changed}
224                        current_value={selected_color_mode}
225                    />
226                    { color_controls }
227                    if ctx.props().enable_time_config {
228                        <SelectValueField<String>
229                            label="timezone"
230                            values={ALL_TIMEZONES.clone()}
231                            default_value={(*USER_TIMEZONE).clone()}
232                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged)}
233                            current_value={self.config.date_format.time_zone().as_ref().unwrap_or(&*USER_TIMEZONE).clone()}
234                        />
235                    }
236                    if let DatetimeFormatType::Simple(config) = &self.config.date_format {
237                        if ctx.props().enable_time_config {
238                            <div class="row">
239                                <button
240                                    id="datetime_format"
241                                    data-title="Simple"
242                                    data-title-hover="Switch to Custom"
243                                    onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig::default()))}
244                                />
245                            </div>
246                        }
247                        <DatetimeStyleSimple
248                            enable_time_config={ctx.props().enable_time_config}
249                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged)}
250                            config={config.clone()}
251                        />
252                    } else if let DatetimeFormatType::Custom(config) = &self.config.date_format {
253                        if ctx.props().enable_time_config {
254                            <div class="row">
255                                <button
256                                    id="datetime_format"
257                                    data-title="Custom"
258                                    data-title-hover="Switch to Simple"
259                                    onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig::default()))}
260                                />
261                            </div>
262                        }
263                        <DatetimeStyleCustom
264                            enable_time_config={ctx.props().enable_time_config}
265                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged)}
266                            config={config.clone()}
267                        />
268                    }
269                </div>
270            </>
271        }
272    }
273}