Skip to main content

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