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