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