diff --git a/.github/actions/install-deps/action.yaml b/.github/actions/install-deps/action.yaml index 42e8fc05c9..e3ebe0c328 100644 --- a/.github/actions/install-deps/action.yaml +++ b/.github/actions/install-deps/action.yaml @@ -164,6 +164,16 @@ runs: targets: aarch64-unknown-linux-gnu components: rust-src + # Did you see a CI error of the form: + # + # error: failed to install component: 'clippy-preview-aarch64-unknown-linux-gnu', + # detected conflict: 'bin/cargo-clippy' + # + # See https://github.com/rust-lang/rustup/issues/988#issuecomment-1820438467 + - name: Stupid cargo hack + shell: bash + run: cargo version + # # TODO doesn't work. # - name: Install LLVM 17 # if: ${{ inputs.cpp == 'true' }} diff --git a/rust/perspective-client/src/rust/config/expressions.rs b/rust/perspective-client/src/rust/config/expressions.rs index c851c33362..e1756ad64a 100644 --- a/rust/perspective-client/src/rust/config/expressions.rs +++ b/rust/perspective-client/src/rust/config/expressions.rs @@ -251,7 +251,7 @@ impl Expressions { } #[doc(hidden)] -#[derive(Serialize, Clone, Copy)] +#[derive(Serialize, Clone, Copy, PartialEq)] pub struct CompletionItemSuggestion { pub label: &'static str, pub insert_text: &'static str, diff --git a/rust/perspective-viewer/src/less/column-selector.less b/rust/perspective-viewer/src/less/column-selector.less index ac1de261ef..537f1e716a 100644 --- a/rust/perspective-viewer/src/less/column-selector.less +++ b/rust/perspective-viewer/src/less/column-selector.less @@ -210,6 +210,11 @@ } } + span.expression-edit-button.disabled { + opacity: 0; + pointer-events: none; + } + span.expression-delete-button { padding-left: 5px; margin-right: 8px; diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 4d0ae1bcf4..8c17946326 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -314,7 +314,9 @@ &:before { display: inline-block; height: 20px; + min-height: 20px; width: 20px; + min-width: 20px; content: ""; mask-size: cover; -webkit-mask-size: cover; @@ -351,7 +353,9 @@ &:before { display: inline-block; height: 20px; + min-height: 20px; width: 20px; + min-width: 20px; content: ""; mask-size: cover; -webkit-mask-size: cover; diff --git a/rust/perspective-viewer/src/rust/components/column_dropdown.rs b/rust/perspective-viewer/src/rust/components/column_dropdown.rs index 99a4bacd9e..9d38b0de94 100644 --- a/rust/perspective-viewer/src/rust/components/column_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/column_dropdown.rs @@ -10,153 +10,263 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::cell::RefCell; +use std::collections::HashSet; +use std::rc::Rc; + +use perspective_client::clone; +use perspective_client::config::Expression; use web_sys::*; +use yew::html::ImplicitClone; use yew::prelude::*; use super::column_selector::InPlaceColumn; -use super::modal::*; -use crate::utils::WeakScope; +use super::portal::PortalModal; +use crate::session::Session; +use crate::utils::*; +use crate::*; static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/column-dropdown.css")); -#[derive(Properties, PartialEq)] -pub struct ColumnDropDownProps { - #[prop_or_default] - pub weak_link: WeakScope, -} - -impl ModalLink for ColumnDropDownProps { - fn weak_link(&self) -> &'_ WeakScope { - &self.weak_link - } +/// Shared state for the column dropdown, updated imperatively. +#[derive(Default)] +pub struct ColumnDropDownState { + pub values: Vec, + pub selected: usize, + pub width: f64, + pub on_select: Option>, + pub target: Option, + pub no_results: bool, } -pub enum ColumnDropDownMsg { - SetValues(Vec, f64), - SetCallback(Callback), - ItemDown, - ItemUp, - ItemSelect, +/// A clonable handle for the column dropdown shared state. +#[derive(Clone)] +pub struct ColumnDropDownElement { + state: Rc>, + session: Session, + notify: Rc>, } -pub struct ColumnDropDown { - values: Option>, - selected: usize, - width: f64, - on_select: Option>, +impl PartialEq for ColumnDropDownElement { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } } -impl Component for ColumnDropDown { - type Message = ColumnDropDownMsg; - type Properties = ColumnDropDownProps; +impl ImplicitClone for ColumnDropDownElement {} - fn create(ctx: &Context) -> Self { - ctx.set_modal_link(); +impl ColumnDropDownElement { + pub fn new(session: Session) -> Self { Self { - values: Some(vec![]), - selected: 0, - width: 0.0, - on_select: None, + state: Default::default(), + session, + notify: Rc::default(), } } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - ColumnDropDownMsg::SetCallback(callback) => { - self.on_select = Some(callback); - false - }, - ColumnDropDownMsg::SetValues(values, width) => { - self.values = Some(values); - self.selected = 0; - self.width = width; - true - }, - ColumnDropDownMsg::ItemSelect => { - if let Some(ref values) = self.values { - match values.get(self.selected) { - None => { - console::error_1(&"Selected out-of-bounds".into()); - false - }, - Some(x) => { - self.on_select.as_ref().unwrap().emit(x.clone()); - false - }, - } - } else { - console::error_1(&"No Values".into()); - false - } - }, - ColumnDropDownMsg::ItemDown => { - self.selected += 1; - if let Some(ref values) = self.values - && self.selected >= values.len() - { - self.selected = 0; - } + pub fn autocomplete( + &self, + target: HtmlInputElement, + exclude: HashSet, + callback: Callback, + ) -> Option<()> { + let input = target.value(); + let metadata = self.session.metadata(); + let mut values: Vec = vec![]; + let small_input = input.to_lowercase(); + for col in metadata.get_table_columns()? { + if !exclude.contains(col) && col.to_lowercase().contains(&small_input) { + values.push(InPlaceColumn::Column(col.to_owned())); + } + } + + for col in self.session.metadata().get_expression_columns() { + if !exclude.contains(col) && col.to_lowercase().contains(&small_input) { + values.push(InPlaceColumn::Column(col.to_owned())); + } + } - true - }, - ColumnDropDownMsg::ItemUp => { - if let Some(ref values) = self.values - && self.selected < 1 - { - self.selected = values.len(); + clone!(self.state, self.session, self.notify); + let target_elem: HtmlElement = target.clone().into(); + let width = target.get_bounding_client_rect().width(); + ApiFuture::spawn(async move { + if !exclude.contains(&input) { + let is_expr = session.validate_expr(&input).await?.is_none(); + if is_expr { + values.push(InPlaceColumn::Expression(Expression::new( + None, + input.into(), + ))); } + } + + let no_results = values.is_empty(); + { + let mut s = state.borrow_mut(); + s.values = values; + s.selected = 0; + s.width = width; + s.on_select = Some(callback); + s.target = Some(target_elem); + s.no_results = no_results; + } + notify.emit(()); + Ok(()) + }); + + Some(()) + } - self.selected -= 1; - true - }, + pub fn item_select(&self) { + let state = self.state.borrow(); + if let Some(value) = state.values.get(state.selected) + && let Some(ref cb) = state.on_select + { + cb.emit(value.clone()); } } - fn changed(&mut self, _ctx: &Context, _old: &Self::Properties) -> bool { - false + pub fn item_down(&self) { + let mut state = self.state.borrow_mut(); + state.selected += 1; + if state.selected >= state.values.len() { + state.selected = 0; + } + + drop(state); + self.notify.emit(()); } - fn view(&self, _ctx: &Context) -> Html { - let body = html! { - if let Some(ref values) = self.values { - if !values.is_empty() { - { for values - .iter() - .enumerate() - .map(|(idx, value)| { - let click = self.on_select.as_ref().unwrap().reform({ - let value = value.clone(); - move |_: MouseEvent| value.clone() - }); - - let row = match value { - InPlaceColumn::Column(col) => html! { - { col } - }, - InPlaceColumn::Expression(col) => html! { - { col.name.clone() } - }, - }; - - html! { - if idx == self.selected { - { row } - } else { - { row } - } - } - }) } - } else { - - } - } + pub fn item_up(&self) { + let mut state = self.state.borrow_mut(); + if state.selected < 1 { + state.selected = state.values.len(); + } + + state.selected -= 1; + drop(state); + self.notify.emit(()); + } + + pub fn hide(&self) -> ApiResult<()> { + self.state.borrow_mut().target = None; + self.notify.emit(()); + Ok(()) + } +} + +/// A portal component that renders the column dropdown. Should be placed in +/// the view of the component that creates the `ColumnDropDownElement`. +#[derive(Properties, PartialEq)] +pub struct ColumnDropDownPortalProps { + pub element: ColumnDropDownElement, + pub theme: String, +} + +pub struct ColumnDropDownPortal { + _sub: Subscription, +} + +impl Component for ColumnDropDownPortal { + type Message = (); + type Properties = ColumnDropDownPortalProps; + + fn create(ctx: &Context) -> Self { + let link = ctx.link().clone(); + let sub = ctx + .props() + .element + .notify + .add_listener(move |()| link.send_message(())); + Self { _sub: sub } + } + + fn update(&mut self, _ctx: &Context, _msg: ()) -> bool { + true + } + + fn view(&self, ctx: &Context) -> Html { + let state = ctx.props().element.state.borrow(); + let target = state.target.clone(); + let on_close = { + let element = ctx.props().element.clone(); + Callback::from(move |()| { + let _ = element.hide(); + }) }; - let position = format!( - ":host{{min-width:{}px;max-width:{}px}}", - self.width, self.width - ); + if target.is_some() { + let values = state.values.clone(); + let selected = state.selected; + let width = state.width; + let on_select = state.on_select.clone(); + drop(state); - html! { <>{ body } } + html! { + + + + } + } else { + html! {} + } } } + +/// Pure view component for the column dropdown content. +#[derive(Properties, PartialEq)] +struct ColumnDropDownViewProps { + values: Vec, + selected: usize, + width: f64, + on_select: Option>, +} + +#[function_component] +fn ColumnDropDownView(props: &ColumnDropDownViewProps) -> Html { + let body = html! { + if !props.values.is_empty() { + { for props.values + .iter() + .enumerate() + .map(|(idx, value)| { + let click = props.on_select.as_ref().unwrap().reform({ + let value = value.clone(); + move |_: MouseEvent| value.clone() + }); + + let row = match value { + InPlaceColumn::Column(col) => html! { + { col } + }, + InPlaceColumn::Expression(col) => html! { + { col.name.clone() } + }, + }; + + html! { + if idx == props.selected { + { row } + } else { + { row } + } + } + }) } + } else { + + } + }; + + let position = format!( + ":host{{min-width:{}px;max-width:{}px}}", + props.width, props.width + ); + + html! { <>{ body } } +} diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index 508dab014b..a37fa82f72 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -27,6 +27,7 @@ use std::rc::Rc; pub use empty_column::*; pub use invalid_column::*; +use perspective_client::config::ViewConfig; use perspective_js::utils::ApiFuture; pub use pivot_column::*; use web_sys::*; @@ -39,9 +40,9 @@ use self::inactive_column::*; use super::containers::scroll_panel::*; use super::containers::split_panel::{Orientation, SplitPanel}; use super::style::LocalStyle; +use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal}; use crate::components::containers::scroll_panel_item::ScrollPanelItem; use crate::css; -use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; use crate::presentation::ColumnLocator; use crate::renderer::*; @@ -60,23 +61,27 @@ pub struct ColumnSelectorProps { /// This is passed to the add_expression_button for styling. pub selected_column: Option, - /// Fires when this component is resized via the UI. - #[prop_or_default] - pub on_resize: Option>>, - /// Value props threaded from root's `SessionProps` / `RendererProps`. pub has_table: bool, pub named_column_count: usize, - pub view_config: Rc, + pub view_config: PtrEqRc, pub drag_column: Option, + /// Cloned session metadata snapshot — threaded from `SessionProps` /// so that metadata changes trigger re-renders via prop diffing. - pub metadata: Rc, + pub metadata: SessionMetadataRc, + + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, // State pub session: Session, pub renderer: Renderer, pub dragdrop: DragDrop, + + /// Fires when this component is resized via the UI. + #[prop_or_default] + pub on_resize: Option>>, } impl PartialEq for ColumnSelectorProps { @@ -87,6 +92,7 @@ impl PartialEq for ColumnSelectorProps { && self.view_config == rhs.view_config && self.drag_column == rhs.drag_column && self.metadata == rhs.metadata + && self.selected_theme == rhs.selected_theme } } @@ -163,12 +169,14 @@ impl Component for ColumnSelector { .columns .iter() .position(|x| x.as_ref() == Some(&column)); + let min_cols = ctx.props().renderer.metadata().min; let is_to_empty = !config .columns .get(index) .map(|x| x.is_some()) .unwrap_or_default(); + min_cols .and_then(|x| from_index.map(|fi| fi < x)) .unwrap_or_default() @@ -180,6 +188,7 @@ impl Component for ColumnSelector { .metadata .get_column_table_type(column.as_str()) .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, col_type, @@ -242,22 +251,25 @@ impl Component for ColumnSelector { .. } = ctx.props(); let metadata = &ctx.props().metadata; + // When `config.columns` is empty but the table has columns (transient // state during `load()` after `reset()` clears the config), fill in // all table columns as active — matching `validate_view_config()`. let prop_config = &ctx.props().view_config; - let config: Rc = if prop_config.columns.is_empty() { + let config = if prop_config.columns.is_empty() { if let Some(table_cols) = metadata.get_table_columns() { - Rc::new(perspective_client::config::ViewConfig { + ViewConfig { columns: table_cols.iter().map(|c| Some(c.clone())).collect(), ..(**prop_config).clone() - }) + } + .into() } else { prop_config.clone() } } else { prop_config.clone() }; + let is_aggregated = config.is_aggregated(); let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, dragdrop); let onselect = ctx.link().callback(|()| Redraw); @@ -320,6 +332,7 @@ impl Component for ColumnSelector { view_config={ctx.props().view_config.clone()} drag_column={ctx.props().drag_column.clone()} metadata={metadata.clone()} + selected_theme={ctx.props().selected_theme.clone()} {dragdrop} {renderer} {session} @@ -496,6 +509,10 @@ impl Component for ColumnSelector { > { for selected_columns } + } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs index 24b6377ad6..81488696b7 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs @@ -11,7 +11,6 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use std::collections::HashSet; -use std::rc::Rc; use perspective_client::config::*; use perspective_js::utils::ApiFuture; @@ -21,9 +20,9 @@ use yew::prelude::*; use super::InPlaceColumn; use super::aggregate_selector::*; use super::expr_edit_button::*; +use crate::components::column_dropdown::ColumnDropDownElement; use crate::components::column_selector::{EmptyColumn, InvalidColumn}; use crate::components::type_icon::TypeIcon; -use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; use crate::js::plugin::*; use crate::presentation::ColumnLocator; @@ -79,10 +78,10 @@ pub struct ActiveColumnProps { pub col_type: Option, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// View config snapshot — threaded from parent as a value prop. - pub view_config: Rc, + pub view_config: PtrEqRc, /// State pub session: Session, @@ -395,14 +394,13 @@ impl Component for ActiveColumn { if !ctx.props().is_aggregated { } - if show_edit_btn { - - } + diff --git a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs index 2aceef4f18..931d3695d4 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs @@ -22,6 +22,7 @@ use crate::components::style::LocalStyle; use crate::css; use crate::renderer::*; use crate::session::*; +use crate::utils::PtrEqRc; #[derive(Properties)] pub struct AggregateSelectorProps { @@ -32,10 +33,10 @@ pub struct AggregateSelectorProps { pub aggregate: Option, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// View config snapshot — threaded from parent as a value prop. - pub view_config: Rc, + pub view_config: PtrEqRc, // State pub renderer: Renderer, diff --git a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs index 8c291a0a5d..d9bf1633c6 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs @@ -21,11 +21,12 @@ use super::InPlaceColumn; use super::filter_column::*; use super::pivot_column::*; use super::sort_column::*; +use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal}; use crate::components::containers::dragdrop_list::*; use crate::components::containers::select::{Select, SelectItem}; +use crate::components::filter_dropdown::{FilterDropDownElement, FilterDropDownPortal}; use crate::components::style::LocalStyle; use crate::css; -use crate::custom_elements::{ColumnDropDownElement, FilterDropDownElement}; use crate::dragdrop::*; use crate::renderer::*; use crate::session::drag_drop_update::*; @@ -42,12 +43,15 @@ pub struct ConfigSelectorProps { /// Current view config threaded as a value prop so that config changes /// (group_by, sort, filter, etc.) trigger re-renders via normal prop /// diffing rather than a PubSub `view_created` subscription. - pub view_config: Rc, + pub view_config: PtrEqRc, /// Column currently being dragged — threaded to show `dragdrop-highlight` /// without subscribing to `dragstart_received`/`dragend_received`. pub drag_column: Option, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, + + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, // State pub session: Session, @@ -60,6 +64,7 @@ impl PartialEq for ConfigSelectorProps { self.view_config == other.view_config && self.drag_column == other.drag_column && self.metadata == other.metadata + && self.selected_theme == other.selected_theme } } @@ -602,43 +607,44 @@ impl Component for ConfigSelector { let group_rollups = requirements.get_group_rollups(&rollup_features); html! { -
- -
- if group_rollups.len() > 1 { - - id="group_rollup_mode_selector" - wrapper_class="group_rollup_wrapper" - values={Rc::new( + <> +
+ +
+ if group_rollups.len() > 1 { + + id="group_rollup_mode_selector" + wrapper_class="group_rollup_wrapper" + values={Rc::new( group_rollups .iter() .map(|x| SelectItem::Option(*x)) .collect(), )} - selected={config.group_rollup_mode} - on_select={on_group_rollup_mode} - /> - } - if !config.group_by.is_empty() && config.split_by.is_empty() { - - } -
- if features.group_by { - >()} - is_dragover={ctx.props().dragdrop.is_dragover(DragTarget::GroupBy)} - {dragdrop} - > - { for config.group_by.iter().map(|group_by| { + selected={config.group_rollup_mode} + on_select={on_group_rollup_mode} + /> + } + if !config.group_by.is_empty() && config.split_by.is_empty() { + + } +
+ if features.group_by { + >()} + is_dragover={ctx.props().dragdrop.is_dragover(DragTarget::GroupBy)} + {dragdrop} + > + { for config.group_by.iter().map(|group_by| { html_nested! { } }) } - - } - if features.split_by { - if !config.split_by.is_empty() { -
- -
+ } - >()} - is_dragover={dragdrop.is_dragover(DragTarget::SplitBy)} - {dragdrop} - > - { for config.split_by.iter().map(|split_by| { + if features.split_by { + if !config.split_by.is_empty() { +
+ +
+ } + >()} + is_dragover={dragdrop.is_dragover(DragTarget::SplitBy)} + {dragdrop} + > + { for config.split_by.iter().map(|split_by| { html_nested! { } }) } - - } - if features.sort { - >()} - is_dragover={dragdrop.is_dragover(DragTarget::Sort).map(|(index, name)| { +
+ } + if features.sort { + >()} + is_dragover={dragdrop.is_dragover(DragTarget::Sort).map(|(index, name)| { (index, Sort(name, SortDir::Asc)) })} - {dragdrop} - > - { for config.sort.iter().enumerate().map(|(idx, sort)| { + {dragdrop} + > + { for config.sort.iter().enumerate().map(|(idx, sort)| { html_nested! { } }) } - - } - if !features.filter_ops.is_empty() { - >()} - is_dragover={dragdrop.is_dragover(DragTarget::Filter).map(|(index, name)| { + + } + if !features.filter_ops.is_empty() { + >()} + is_dragover={dragdrop.is_dragover(DragTarget::Filter).map(|(index, name)| { (index, Filter::new(&name, "", FilterTerm::Scalar(Scalar::Null))) })} - {dragdrop} - > - { for config.filter.iter().enumerate().map(|(idx, filter)| { + {dragdrop} + > + { for config.filter.iter().enumerate().map(|(idx, filter)| { let filter_keydown = ctx.link() .callback(move |txt| ConfigSelectorMsg::SetFilterValue(idx, txt)); @@ -741,9 +747,18 @@ impl Component for ConfigSelector { } }) } - - } -
+ + } +
+ + + } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/empty_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/empty_column.rs index 70a1732144..2a66a3d62f 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/empty_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/empty_column.rs @@ -16,9 +16,9 @@ use perspective_client::config::Expression; use web_sys::*; use yew::prelude::*; +use crate::components::column_dropdown::ColumnDropDownElement; use crate::components::style::LocalStyle; use crate::css; -use crate::custom_elements::ColumnDropDownElement; #[derive(Properties)] pub struct EmptyColumnProps { @@ -38,7 +38,7 @@ impl PartialEq for EmptyColumnProps { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum InPlaceColumn { Column(String), Expression(Expression<'static>), diff --git a/rust/perspective-viewer/src/rust/components/column_selector/expr_edit_button.rs b/rust/perspective-viewer/src/rust/components/column_selector/expr_edit_button.rs index 281da28669..09c5ddcad8 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/expr_edit_button.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/expr_edit_button.rs @@ -27,6 +27,10 @@ pub struct ExprEditButtonProps { /// Is the expression/config panel open? pub is_editing: bool, + + /// Is the expression/config panel enabled? If not, show an invisible + /// square in the same dimensions (so the layout does not jump around). + pub is_disabled: bool, } /// A button that goes into a column-list for a custom expression @@ -42,7 +46,9 @@ pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html { p.on_open_expr_panel.emit(name) }); - let class = if p.is_editing { + let class = if p.is_disabled { + "expression-edit-button disabled" + } else if p.is_editing { "expression-edit-button is-editing" } else { "expression-edit-button" diff --git a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs index 70c9cc2ae6..2ef12de155 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs @@ -22,9 +22,9 @@ use yew::prelude::*; use crate::components::containers::dragdrop_list::*; use crate::components::containers::select::*; +use crate::components::filter_dropdown::FilterDropDownElement; use crate::components::style::LocalStyle; use crate::components::type_icon::TypeIcon; -use crate::custom_elements::*; use crate::dragdrop::*; use crate::renderer::*; use crate::session::*; @@ -39,9 +39,9 @@ pub struct FilterColumnProps { pub on_keydown: Callback, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// Current view config threaded as a value prop. - pub view_config: Rc, + pub view_config: PtrEqRc, // State pub session: Session, diff --git a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs index 38b2d689a2..d4d6a77a10 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use itertools::Itertools; use perspective_client::config::*; use perspective_js::utils::ApiFuture; @@ -47,11 +45,11 @@ pub struct InactiveColumnProps { pub is_expression: bool, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// View config snapshot — threaded from parent so we avoid /// `session.get_view_config()` calls. - pub view_config: Rc, + pub view_config: PtrEqRc, /// `dragend` event`. pub ondragend: Callback<()>, @@ -176,14 +174,13 @@ impl Component for InactiveColumn { { ctx.props().name.clone() } - if is_expression { - - } + diff --git a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs index 27ccc9bda9..4b439dc574 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use perspective_client::config::ColumnType; use web_sys::*; use yew::prelude::*; @@ -35,7 +33,7 @@ pub struct PivotColumnProps { /// Session metadata snapshot — threaded from `SessionProps`. #[prop_or_default] - pub metadata: Option>, + pub metadata: Option, // State #[prop_or_default] diff --git a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs index 40ba61d4eb..5a579f093b 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use perspective_client::config::*; use perspective_js::utils::ApiFuture; use web_sys::*; @@ -30,10 +28,10 @@ pub struct SortColumnProps { pub idx: usize, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// Current view config — threaded as a value prop. - pub view_config: Rc, + pub view_config: PtrEqRc, // State pub session: Session, diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs index 3c7b57a6a9..605cffb4d2 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs @@ -36,11 +36,12 @@ use crate::components::type_icon::TypeIconType; use crate::custom_events::CustomEvents; use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation}; use crate::renderer::Renderer; -use crate::session::{Session, SessionMetadata}; +use crate::session::{Session, SessionMetadataRc}; use crate::tasks::{ EditExpression, HasCustomEvents, HasPresentation, HasRenderer, HasSession, can_render_column_styles, locator_name_or_default, locator_view_type, }; +use crate::utils::PtrEqRc; use crate::*; #[derive(Clone, Derivative, Properties)] @@ -58,10 +59,13 @@ pub struct ColumnSettingsPanelProps { pub plugin_name: Option, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// View config snapshot — threaded from `SessionProps`. - pub view_config: Rc, + pub view_config: PtrEqRc, + + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, // State #[derivative(Debug = "ignore")] @@ -84,6 +88,7 @@ impl PartialEq for ColumnSettingsPanelProps { && self.plugin_name == other.plugin_name && self.metadata == other.metadata && self.view_config == other.view_config + && self.selected_theme == other.selected_theme } } @@ -294,6 +299,7 @@ impl Component for ColumnSettingsPanel { disabled: !ctx.props().selected_column.is_expr(), reset_count: self.reset_count, metadata: ctx.props().metadata.clone(), + selected_theme: ctx.props().selected_theme.clone(), session: &ctx.props().session }); @@ -341,6 +347,7 @@ impl Component for ColumnSettingsPanel { group_by_depth: ctx.props().view_config.group_by.len() as u32, view_config: ctx.props().view_config.clone(), metadata: ctx.props().metadata.clone(), + selected_theme: ctx.props().selected_theme.clone(), custom_events: ctx.props().custom_events.clone(), presentation: ctx.props().presentation.clone(), renderer: ctx.props().renderer.clone(), diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs index 0322ef40f8..1266776bc5 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs @@ -14,8 +14,6 @@ mod agg_depth_selector; mod stub; mod symbol; -use std::rc::Rc; - use itertools::Itertools; use perspective_client::config::ColumnType; use yew::{Html, Properties, function_component, html}; @@ -35,6 +33,7 @@ use crate::tasks::{ HasCustomEvents, HasPresentation, HasRenderer, HasSession, SendPluginConfig, get_column_style_control_options, }; +use crate::utils::PtrEqRc; #[derive(Clone, PartialEq, Properties)] pub struct StyleTabProps { @@ -43,10 +42,13 @@ pub struct StyleTabProps { pub group_by_depth: u32, /// View config snapshot — threaded from parent. - pub view_config: Rc, + pub view_config: PtrEqRc, /// Session metadata snapshot — threaded from parent. - pub metadata: Rc, + pub metadata: PtrEqRc, + + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, // State pub custom_events: CustomEvents, @@ -162,6 +164,7 @@ pub fn StyleTab(props: &StyleTabProps) -> Html { {restored_config} on_change={on_change.clone()} column_name={props.column_name.clone()} + selected_theme={props.selected_theme.clone()} session={props.session.clone()} /> })) diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs index e7490b4594..c0b9f828df 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs @@ -23,10 +23,10 @@ use itertools::Itertools; use yew::{Callback, Html, Properties, html}; use crate::components::column_settings_sidebar::style_tab::symbol::symbol_pairs::PairsList; +use crate::components::filter_dropdown::{FilterDropDownElement, FilterDropDownPortal}; use crate::components::style::LocalStyle; use crate::config::{ColumnConfigValueUpdate, KeyValueOpts, SymbolKVPair}; use crate::css; -use crate::custom_elements::FilterDropDownElement; use crate::session::Session; #[derive(Properties, PartialEq, Clone)] @@ -36,6 +36,8 @@ pub struct SymbolAttrProps { pub restored_config: Option>, pub on_change: Callback, pub default_config: KeyValueOpts, + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, } impl SymbolAttrProps { pub fn next_default_symbol(&self, pairs_len: usize) -> String { @@ -125,6 +127,10 @@ impl yew::Component for SymbolStyle { values={Rc::new(ctx.props().default_config.values.clone())} {update_pairs} /> + } } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs index 28b4319c56..a00cca1ccf 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs @@ -18,8 +18,8 @@ use perspective_client::clone; use yew::{Html, Properties, function_component, html}; use crate::components::empty_row::EmptyRow; +use crate::components::filter_dropdown::FilterDropDownElement; use crate::config::SymbolKVPair; -use crate::custom_elements::FilterDropDownElement; #[derive(Properties, PartialEq)] pub struct RowSelectorProps { diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs index ed6db352d1..e14b6c3fc9 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs @@ -16,10 +16,11 @@ use itertools::Itertools; use yew::{Callback, Html, Properties, html}; use crate::components::column_settings_sidebar::style_tab::symbol::symbol_pairs_item::PairsListItem; +use crate::components::filter_dropdown::FilterDropDownElement; use crate::components::style::LocalStyle; use crate::config::SymbolKVPair; use crate::css; -use crate::custom_elements::FilterDropDownElement; +use crate::utils::PtrEqRc; #[derive(Properties, PartialEq)] pub struct PairsListProps { @@ -28,7 +29,7 @@ pub struct PairsListProps { pub update_pairs: Callback>, pub id: Option, pub row_dropdown: Rc, - pub values: Rc>, + pub values: PtrEqRc>, pub column_name: String, } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs index 7f4f1974cf..4dbb1e0f67 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs @@ -16,8 +16,9 @@ use yew::{Callback, Html, Properties, html}; use crate::components::column_settings_sidebar::style_tab::symbol::row_selector::RowSelector; use crate::components::column_settings_sidebar::style_tab::symbol::symbol_selector::SymbolSelector; +use crate::components::filter_dropdown::FilterDropDownElement; use crate::config::SymbolKVPair; -use crate::custom_elements::FilterDropDownElement; +use crate::utils::PtrEqRc; #[derive(Properties, PartialEq)] pub struct PairsListItemProps { @@ -26,7 +27,7 @@ pub struct PairsListItemProps { pub pairs: Vec, pub update_pairs: Callback>, pub row_dropdown: Rc, - pub values: Rc>, + pub values: PtrEqRc>, pub focused: bool, pub set_focused_index: Callback>, pub column_name: String, diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs index 455d9089c5..7e906cd286 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs @@ -10,18 +10,17 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use itertools::Itertools; use yew::{Callback, Html, Properties, function_component, html}; use crate::components::containers::select::{Select, SelectItem}; +use crate::utils::PtrEqRc; #[derive(Properties, PartialEq)] pub struct SymbolSelectorProps { pub index: usize, pub selected_value: Option, - pub values: Rc>, + pub values: PtrEqRc>, pub callback: Callback, } diff --git a/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs b/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs index 9168d7fcc8..557a4c9ab8 100644 --- a/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs +++ b/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs @@ -19,9 +19,9 @@ use web_sys::*; use yew::html::Scope; use yew::prelude::*; +use crate::components::column_dropdown::ColumnDropDownElement; use crate::components::column_selector::{EmptyColumn, InPlaceColumn, InvalidColumn}; use crate::components::type_icon::TypeIcon; -use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; use crate::utils::DragTarget; diff --git a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs index f87cbfacbb..77db116142 100644 --- a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs @@ -15,28 +15,15 @@ use std::rc::Rc; use yew::prelude::*; use super::containers::dropdown_menu::*; -use super::modal::*; -use super::style::StyleProvider; use crate::renderer::*; use crate::tasks::*; -use crate::utils::*; type CopyDropDownMenuItem = DropDownMenuItem; #[derive(Properties, PartialEq)] pub struct CopyDropDownMenuProps { pub callback: Callback, - pub root: web_sys::HtmlElement, pub renderer: Renderer, - - #[prop_or_default] - weak_link: WeakScope, -} - -impl ModalLink for CopyDropDownMenuProps { - fn weak_link(&self) -> &'_ WeakScope { - &self.weak_link - } } pub struct CopyDropDownMenu {} @@ -45,8 +32,7 @@ impl Component for CopyDropDownMenu { type Message = (); type Properties = CopyDropDownMenuProps; - fn create(ctx: &Context) -> Self { - ctx.set_modal_link(); + fn create(_ctx: &Context) -> Self { Self {} } @@ -59,12 +45,13 @@ impl Component for CopyDropDownMenu { let is_chart = plugin.name().as_str() != "Datagrid"; let has_selection = ctx.props().renderer.get_selection().is_some(); html! { - + <> +
values={Rc::new(get_menu_items(is_chart, has_selection))} callback={&ctx.props().callback} /> - + } } } diff --git a/rust/perspective-viewer/src/rust/components/editable_header.rs b/rust/perspective-viewer/src/rust/components/editable_header.rs index 7839cc82d9..93049fc6fc 100644 --- a/rust/perspective-viewer/src/rust/components/editable_header.rs +++ b/rust/perspective-viewer/src/rust/components/editable_header.rs @@ -19,7 +19,7 @@ use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, h use super::type_icon::TypeIconType; use crate::components::type_icon::TypeIcon; use crate::maybe; -use crate::session::{Session, SessionMetadata}; +use crate::session::{Session, SessionMetadataRc}; #[derive(Clone, PartialEq, Properties)] pub struct EditableHeaderProps { @@ -33,7 +33,7 @@ pub struct EditableHeaderProps { pub reset_count: u8, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, // State pub session: Session, diff --git a/rust/perspective-viewer/src/rust/components/empty_row.rs b/rust/perspective-viewer/src/rust/components/empty_row.rs index 507c77f021..a26997ea12 100644 --- a/rust/perspective-viewer/src/rust/components/empty_row.rs +++ b/rust/perspective-viewer/src/rust/components/empty_row.rs @@ -19,9 +19,9 @@ use wasm_bindgen::JsCast; use web_sys::*; use yew::prelude::*; +use crate::components::filter_dropdown::FilterDropDownElement; use crate::components::style::LocalStyle; use crate::css; -use crate::custom_elements::FilterDropDownElement; #[derive(Properties, Derivative)] #[derivative(Debug)] diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index 547c953190..d7ee7533b2 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -12,16 +12,12 @@ use std::rc::Rc; -use session::Session; use yew::prelude::*; use super::containers::dropdown_menu::*; -use super::modal::{ModalLink, SetModalLink}; -use super::style::StyleProvider; use crate::renderer::*; +use crate::session::Session; use crate::tasks::*; -use crate::utils::*; -use crate::*; pub type ExportDropDownMenuItem = DropDownMenuItem; @@ -30,16 +26,6 @@ pub struct ExportDropDownMenuProps { pub renderer: Renderer, pub session: Session, pub callback: Callback, - pub root: web_sys::HtmlElement, - - #[prop_or_default] - weak_link: WeakScope, -} - -impl ModalLink for ExportDropDownMenuProps { - fn weak_link(&self) -> &'_ utils::WeakScope { - &self.weak_link - } } pub enum ExportDropDownMenuMsg { @@ -60,11 +46,9 @@ impl Component for ExportDropDownMenu { fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { let callback = ctx.link().callback(|_| ExportDropDownMenuMsg::TitleChange); let plugin = ctx.props().renderer.get_active_plugin().unwrap(); - // let has_render = js_sys::Reflect::has(&plugin, - // js_intern::js_intern!("render")).unwrap(); let is_chart = plugin.name().as_str() != "Datagrid"; html! { - + <> { "Save as" } - + } } @@ -96,7 +80,6 @@ impl Component for ExportDropDownMenu { } fn create(ctx: &Context) -> Self { - ctx.set_modal_link(); Self { title: ctx .props() diff --git a/rust/perspective-viewer/src/rust/components/expression_editor.rs b/rust/perspective-viewer/src/rust/components/expression_editor.rs index aa37a04bbe..296966d084 100644 --- a/rust/perspective-viewer/src/rust/components/expression_editor.rs +++ b/rust/perspective-viewer/src/rust/components/expression_editor.rs @@ -17,7 +17,7 @@ use yew::prelude::*; use super::form::code_editor::*; use super::style::LocalStyle; -use crate::session::{Session, SessionMetadata}; +use crate::session::{Session, SessionMetadata, SessionMetadataRc}; use crate::*; #[derive(Properties, PartialEq, Clone)] @@ -32,7 +32,11 @@ pub struct ExpressionEditorProps { pub reset_count: u8, /// Session metadata snapshot — threaded from `SessionProps`. - pub metadata: Rc, + pub metadata: SessionMetadataRc, + + /// Selected theme name, threaded for PortalModal consumers. + #[prop_or_default] + pub selected_theme: Option, // State pub session: Session, @@ -126,6 +130,7 @@ impl Component for ExpressionEditor { {disabled} oninput={self.oninput.clone()} onsave={ctx.props().on_save.clone()} + theme={ctx.props().selected_theme.clone().unwrap_or_default()} />
diff --git a/rust/perspective-viewer/src/rust/components/filter_dropdown.rs b/rust/perspective-viewer/src/rust/components/filter_dropdown.rs index 2b26beac6a..e1b9ecd7c0 100644 --- a/rust/perspective-viewer/src/rust/components/filter_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/filter_dropdown.rs @@ -10,135 +10,279 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::cell::RefCell; +use std::collections::HashSet; +use std::rc::Rc; + +use perspective_client::clone; use web_sys::*; +use yew::html::ImplicitClone; use yew::prelude::*; -use super::modal::*; -use crate::utils::WeakScope; +use super::portal::PortalModal; +use crate::session::Session; +use crate::utils::*; +use crate::*; static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/filter-dropdown.css")); -#[derive(Properties, PartialEq)] -pub struct FilterDropDownProps { - #[prop_or_default] - pub weak_link: WeakScope, -} - -impl ModalLink for FilterDropDownProps { - fn weak_link(&self) -> &'_ WeakScope { - &self.weak_link - } +#[derive(Default)] +struct FilterDropDownState { + values: Vec, + selected: usize, + on_select: Option>, + target: Option, } -pub enum FilterDropDownMsg { - SetValues(Vec), - SetCallback(Callback), - ItemDown, - ItemUp, - ItemSelect, +#[derive(Clone)] +pub struct FilterDropDownElement { + state: Rc>, + session: Session, + column: Rc>>, + all_values: Rc>>>, + notify: Rc>, } -pub struct FilterDropDown { - values: Option>, - selected: usize, - on_select: Option>, +impl PartialEq for FilterDropDownElement { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } } -impl Component for FilterDropDown { - type Message = FilterDropDownMsg; - type Properties = FilterDropDownProps; +impl ImplicitClone for FilterDropDownElement {} - fn create(ctx: &Context) -> Self { - ctx.set_modal_link(); +impl FilterDropDownElement { + pub fn new(session: Session) -> Self { Self { - values: Some(vec![]), - selected: 0, - on_select: None, + state: Default::default(), + session, + column: Default::default(), + all_values: Default::default(), + notify: Rc::default(), } } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - FilterDropDownMsg::SetCallback(callback) => { - self.on_select = Some(callback); - false - }, - FilterDropDownMsg::SetValues(values) => { - self.values = Some(values); - self.selected = 0; - true - }, - FilterDropDownMsg::ItemSelect => { - if let Some(ref values) = self.values { - match values.get(self.selected) { - None => { - console::error_1(&"Selected out-of-bounds".into()); - false - }, - Some(x) => { - self.on_select.as_ref().unwrap().emit(x.clone()); - false - }, - } + pub fn reautocomplete(&self) { + // Re-open portal with current target + self.notify.emit(()); + } + + pub fn autocomplete( + &self, + column: (usize, String), + input: String, + exclude: HashSet, + target: HtmlElement, + callback: Callback, + ) { + let current_column = self.column.borrow().clone(); + match current_column { + Some(filter_col) if filter_col == column => { + let values = filter_values(&input, &self.all_values, &exclude); + if values.len() == 1 && values[0] == input { + let _ = self.hide(); } else { - console::error_1(&"No Values".into()); - false + let mut s = self.state.borrow_mut(); + s.values = values; + s.selected = 0; + s.on_select = Some(callback); + if s.target.is_none() { + s.target = Some(target); + } + + drop(s); + self.notify.emit(()); } }, - FilterDropDownMsg::ItemDown => { - self.selected += 1; - if let Some(ref values) = self.values - && self.selected >= values.len() - { - self.selected = 0; - }; - - true - }, - FilterDropDownMsg::ItemUp => { - if let Some(ref values) = self.values - && self.selected < 1 - { - self.selected = values.len(); - }; - - self.selected -= 1; - true + _ => { + clone!( + self.state, + self.session, + self.all_values, + self.notify, + old_column = self.column + ); + ApiFuture::spawn(async move { + let fetched = session.get_column_values(column.1.clone()).await?; + *all_values.borrow_mut() = Some(fetched); + let values = filter_values(&input, &all_values, &exclude); + let should_hide = values.len() == 1 && values[0] == input; + + *old_column.borrow_mut() = Some(column); + { + let mut s = state.borrow_mut(); + s.on_select = Some(callback); + if should_hide { + let fv = self::filter_values("", &all_values, &exclude); + s.values = fv; + s.target = Some(target); + } else { + s.values = values; + s.target = Some(target); + } + s.selected = 0; + } + if should_hide { + state.borrow_mut().target = None; + } + + notify.emit(()); + Ok(()) + }); }, } } - fn changed(&mut self, _ctx: &Context, _old: &Self::Properties) -> bool { - false + pub fn item_select(&self) { + let state = self.state.borrow(); + if let Some(value) = state.values.get(state.selected) + && let Some(ref cb) = state.on_select + { + cb.emit(value.clone()); + } } - fn view(&self, _ctx: &Context) -> Html { - let body = html! { - if let Some(ref values) = self.values { - if !values.is_empty() { - { for values - .iter() - .enumerate() - .map(|(idx, value)| { - let click = self.on_select.as_ref().unwrap().reform({ - let value = value.clone(); - move |_: MouseEvent| value.clone() - }); - - html! { - if idx == self.selected { - { value } - } else { - { value } - } - } - }) } - } else { - { "No Completions" } - } - } + pub fn item_down(&self) { + let mut state = self.state.borrow_mut(); + state.selected += 1; + if state.selected >= state.values.len() { + state.selected = 0; + } + + drop(state); + self.notify.emit(()); + } + + pub fn item_up(&self) { + let mut state = self.state.borrow_mut(); + if state.selected < 1 { + state.selected = state.values.len(); + } + + state.selected -= 1; + drop(state); + self.notify.emit(()); + } + + pub fn hide(&self) -> ApiResult<()> { + self.state.borrow_mut().target = None; + self.column.borrow_mut().take(); + self.notify.emit(()); + Ok(()) + } +} + +#[derive(Properties, PartialEq)] +pub struct FilterDropDownPortalProps { + pub element: FilterDropDownElement, + pub theme: String, +} + +pub struct FilterDropDownPortal { + _sub: Subscription, +} + +impl Component for FilterDropDownPortal { + type Message = (); + type Properties = FilterDropDownPortalProps; + + fn create(ctx: &Context) -> Self { + let link = ctx.link().clone(); + let sub = ctx + .props() + .element + .notify + .add_listener(move |()| link.send_message(())); + Self { _sub: sub } + } + + fn update(&mut self, _ctx: &Context, _msg: ()) -> bool { + true + } + + fn view(&self, ctx: &Context) -> Html { + let state = ctx.props().element.state.borrow(); + let target = state.target.clone(); + let on_close = { + let element = ctx.props().element.clone(); + Callback::from(move |()| { + let _ = element.hide(); + }) }; - html! { <>{ body } } + if target.is_some() { + let values = state.values.clone(); + let selected = state.selected; + let on_select = state.on_select.clone(); + drop(state); + + html! { + + + + } + } else { + html! {} + } + } +} + +#[derive(Properties, PartialEq)] +struct FilterDropDownViewProps { + values: Vec, + selected: usize, + on_select: Option>, +} + +#[function_component] +fn FilterDropDownView(props: &FilterDropDownViewProps) -> Html { + let body = html! { + if !props.values.is_empty() { + { for props.values + .iter() + .enumerate() + .map(|(idx, value)| { + let click = props.on_select.as_ref().unwrap().reform({ + let value = value.clone(); + move |_: MouseEvent| value.clone() + }); + + html! { + if idx == props.selected { + { value } + } else { + { value } + } + } + }) } + } else { + { "No Completions" } + } + }; + + html! { <>{ body } } +} + +fn filter_values( + input: &str, + values: &Rc>>>, + exclude: &HashSet, +) -> Vec { + let input = input.to_lowercase(); + if let Some(values) = &*values.borrow() { + values + .iter() + .filter(|x| x.to_lowercase().contains(&input) && !exclude.contains(x.as_str())) + .take(10) + .cloned() + .collect::>() + } else { + vec![] } } diff --git a/rust/perspective-viewer/src/rust/components/form/code_editor.rs b/rust/perspective-viewer/src/rust/components/form/code_editor.rs index b6010e274e..e2457411df 100644 --- a/rust/perspective-viewer/src/rust/components/form/code_editor.rs +++ b/rust/perspective-viewer/src/rust/components/form/code_editor.rs @@ -18,9 +18,9 @@ use web_sys::*; use yew::prelude::*; use crate::components::form::highlight::highlight; +use crate::components::function_dropdown::{FunctionDropDownElement, FunctionDropDownPortal}; use crate::components::style::LocalStyle; use crate::css; -use crate::custom_elements::FunctionDropDownElement; use crate::exprtk::{Cursor, tokenize}; use crate::utils::*; @@ -45,6 +45,10 @@ pub struct CodeEditorProps { #[prop_or_default] pub error: Option, + + /// Selected theme name, threaded for PortalModal consumers. + #[prop_or_default] + pub theme: String, } /// A syntax-highlighted text editor component. @@ -128,6 +132,8 @@ pub fn code_editor(props: &CodeEditorProps) -> Html { |deps| scroll_sync(&deps.0, &deps.1, &deps.2), ); + let portal_dropdown = filter_dropdown.clone(); + // Blur if this element is not in the tree use_effect_with(filter_dropdown.clone(), |filter_dropdown| { clone!(filter_dropdown); @@ -191,6 +197,10 @@ pub fn code_editor(props: &CodeEditorProps) -> Html {
+ } } diff --git a/rust/perspective-viewer/src/rust/components/function_dropdown.rs b/rust/perspective-viewer/src/rust/components/function_dropdown.rs index c6a9026ec2..ac2a09ee18 100644 --- a/rust/perspective-viewer/src/rust/components/function_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/function_dropdown.rs @@ -10,142 +10,215 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use perspective_client::config::CompletionItemSuggestion; +use std::cell::RefCell; +use std::rc::Rc; + +use perspective_client::config::{COMPLETIONS, CompletionItemSuggestion}; +use perspective_js::utils::ApiResult; use web_sys::*; +use yew::html::ImplicitClone; use yew::prelude::*; -use super::modal::*; -use crate::utils::WeakScope; +use super::portal::PortalModal; +use crate::utils::*; static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/function-dropdown.css")); -#[derive(Properties, PartialEq)] -pub struct FunctionDropDownProps { - #[prop_or_default] - pub weak_link: WeakScope, +#[derive(Default)] +struct FunctionDropDownState { + values: Vec, + selected: usize, + on_select: Option>, + target: Option, } -impl ModalLink for FunctionDropDownProps { - fn weak_link(&self) -> &'_ WeakScope { - &self.weak_link - } +#[derive(Clone, Default)] +pub struct FunctionDropDownElement { + state: Rc>, + notify: Rc>, } -pub enum FunctionDropDownMsg { - SetValues(Vec), - SetCallback(Callback), - ItemDown, - ItemUp, - ItemSelect, +impl PartialEq for FunctionDropDownElement { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.state, &other.state) + } } -pub struct FunctionDropDown { - values: Option>, - selected: usize, - on_select: Option>, -} +impl ImplicitClone for FunctionDropDownElement {} -impl Component for FunctionDropDown { - type Message = FunctionDropDownMsg; - type Properties = FunctionDropDownProps; +impl FunctionDropDownElement { + pub fn reautocomplete(&self) { + self.notify.emit(()); + } - fn create(ctx: &Context) -> Self { - ctx.set_modal_link(); - Self { - values: Some(vec![]), - selected: 0, - on_select: None, + pub fn autocomplete( + &self, + input: String, + target: HtmlElement, + callback: Callback, + ) -> ApiResult<()> { + let values = filter_values(&input); + if values.is_empty() { + self.hide()?; + } else { + let mut s = self.state.borrow_mut(); + s.values = values; + s.selected = 0; + s.on_select = Some(callback); + s.target = Some(target); + drop(s); + self.notify.emit(()); } + + Ok(()) } - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - FunctionDropDownMsg::SetCallback(callback) => { - self.on_select = Some(callback); - false - }, - FunctionDropDownMsg::SetValues(values) => { - self.values = Some(values); - self.selected = 0; - true - }, - FunctionDropDownMsg::ItemSelect => { - if let Some(ref values) = self.values { - match values.get(self.selected) { - None => { - console::error_1(&"Selected out-of-bounds".into()); - false - }, - Some(x) => { - self.on_select.as_ref().unwrap().emit(*x); - false - }, - } - } else { - console::error_1(&"No Values".into()); - false - } - }, - FunctionDropDownMsg::ItemDown => { - self.selected += 1; - if let Some(ref values) = self.values - && self.selected >= values.len() - { - self.selected = 0; - }; - - true - }, - FunctionDropDownMsg::ItemUp => { - if let Some(ref values) = self.values - && self.selected < 1 - { - self.selected = values.len(); - } - - self.selected -= 1; - true - }, + pub fn item_select(&self) { + let state = self.state.borrow(); + if let Some(value) = state.values.get(state.selected) + && let Some(ref cb) = state.on_select + { + cb.emit(*value); } } - fn changed(&mut self, _ctx: &Context, _old: &Self::Properties) -> bool { - false + pub fn item_down(&self) { + let mut state = self.state.borrow_mut(); + state.selected += 1; + if state.selected >= state.values.len() { + state.selected = 0; + } + + drop(state); + self.notify.emit(()); } - fn view(&self, _ctx: &Context) -> Html { - let body = html! { - if let Some(ref values) = self.values { - if !values.is_empty() { - { for values - .iter() - .enumerate() - .map(|(idx, value)| { - let click = self.on_select.as_ref().unwrap().reform({ - let value = *value; - move |_: MouseEvent| value - }); - - html! { - if idx == self.selected { -
- { value.label } -
- { value.documentation } -
- } else { -
- { value.label } -
- { value.documentation } -
- } - } - }) } - } - } + pub fn item_up(&self) { + let mut state = self.state.borrow_mut(); + if state.selected < 1 { + state.selected = state.values.len(); + } + + state.selected -= 1; + drop(state); + self.notify.emit(()); + } + + pub fn hide(&self) -> ApiResult<()> { + self.state.borrow_mut().target = None; + self.notify.emit(()); + Ok(()) + } +} + +#[derive(Properties, PartialEq)] +pub struct FunctionDropDownPortalProps { + pub element: FunctionDropDownElement, + pub theme: String, +} + +pub struct FunctionDropDownPortal { + _sub: Subscription, +} + +impl Component for FunctionDropDownPortal { + type Message = (); + type Properties = FunctionDropDownPortalProps; + + fn create(ctx: &Context) -> Self { + let link = ctx.link().clone(); + let sub = ctx + .props() + .element + .notify + .add_listener(move |()| link.send_message(())); + Self { _sub: sub } + } + + fn update(&mut self, _ctx: &Context, _msg: ()) -> bool { + true + } + + fn view(&self, ctx: &Context) -> Html { + let state = ctx.props().element.state.borrow(); + let target = state.target.clone(); + let on_close = { + let element = ctx.props().element.clone(); + Callback::from(move |()| { + let _ = element.hide(); + }) }; - html! { <>{ body } } + if target.is_some() { + let values = state.values.clone(); + let selected = state.selected; + let on_select = state.on_select.clone(); + drop(state); + + html! { + + + + } + } else { + html! {} + } } } + +#[derive(Properties, PartialEq)] +struct FunctionDropDownViewProps { + values: Vec, + selected: usize, + on_select: Option>, +} + +#[function_component] +fn FunctionDropDownView(props: &FunctionDropDownViewProps) -> Html { + let body = html! { + if !props.values.is_empty() { + { for props.values + .iter() + .enumerate() + .map(|(idx, value)| { + let click = props.on_select.as_ref().unwrap().reform({ + let value = *value; + move |_: MouseEvent| value + }); + + html! { + if idx == props.selected { +
+ { value.label } +
+ { value.documentation } +
+ } else { +
+ { value.label } +
+ { value.documentation } +
+ } + } + }) } + } + }; + + html! { <>{ body } } +} + +fn filter_values(input: &str) -> Vec { + let input = input.to_lowercase(); + COMPLETIONS + .iter() + .filter(|x| x.label.to_lowercase().starts_with(&input)) + .cloned() + .collect::>() +} diff --git a/rust/perspective-viewer/src/rust/components/main_panel.rs b/rust/perspective-viewer/src/rust/components/main_panel.rs index 54fc5be1d8..789067b6e5 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel.rs @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use perspective_js::utils::*; use wasm_bindgen::prelude::*; use yew::prelude::*; @@ -51,7 +49,7 @@ pub struct MainPanelProps { /// Value props from root's `PresentationProps`, threaded to `StatusBar`. pub is_settings_open: bool, pub selected_theme: Option, - pub available_themes: Rc>, + pub available_themes: PtrEqRc>, pub is_workspace: bool, /// State @@ -160,6 +158,7 @@ impl Component for MainPanel { }); }) }; + html! {
{ let opacity = if visible { "" } else { ";opacity:0" }; self.css = format!(":host{{top:{top}px;left:{left}px{opacity}}}"); - self.rev_vert.0.set(rev_vert); + self.rev_vert.set(rev_vert); true }, ModalMsg::SubMsg(msg) => { @@ -117,6 +117,12 @@ pub struct ModalOrientation(Rc>); impl ImplicitClone for ModalOrientation {} +impl ModalOrientation { + pub fn set(&self, value: bool) { + self.0.set(value); + } +} + impl From for bool { fn from(x: ModalOrientation) -> Self { x.0.get() diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index e22bfd56c7..0ad45d31d5 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -10,12 +10,11 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use yew::prelude::*; use super::style::LocalStyle; use crate::css; +use crate::utils::PtrEqRc; /// Pure value props — no engine handles, no PubSub subscriptions. /// The parent passes updated values whenever the renderer state changes. @@ -25,7 +24,7 @@ pub struct PluginSelectorProps { pub plugin_name: Option, /// Flat list of all registered plugin names (all categories merged). - pub available_plugins: Rc>, + pub available_plugins: PtrEqRc>, /// Called when the user selects a different plugin. pub on_select_plugin: Callback, diff --git a/rust/perspective-viewer/src/rust/components/portal.rs b/rust/perspective-viewer/src/rust/components/portal.rs new file mode 100644 index 0000000000..f80db0aa15 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/portal.rs @@ -0,0 +1,274 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use std::cell::Cell; +use std::rc::Rc; + +use perspective_js::utils::global; +use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; +use web_sys::*; +use yew::prelude::*; + +use crate::components::modal::ModalOrientation; +use crate::components::style::StyleProvider; +use crate::utils::*; + +#[derive(Properties, PartialEq)] +pub struct PortalModalProps { + pub children: Children, + + /// The element to position relative to. `None` means closed. + pub target: Option, + + /// Whether the portal manages its own focus and closes on blur. + #[prop_or(true)] + pub own_focus: bool, + + /// Called when the portal closes (blur, etc). + #[prop_or_default] + pub on_close: Callback<()>, + + pub tag_name: &'static str, + + pub theme: String, +} + +pub enum PortalModalMsg { + Reposition, +} + +pub struct PortalModal { + host: HtmlElement, + shadow_root: Element, + top: f64, + left: f64, + visible: bool, + rev_vert: ModalOrientation, + anchor: Rc>, + _blur_closure: Option>, +} + +impl PortalModal { + fn attach_to_body(&self) { + if !self.host.is_connected() { + let _ = global::body().append_child(&self.host); + } + } + + fn detach_from_body(&mut self) { + if self.host.is_connected() { + let _ = global::body().remove_child(&self.host); + } + + if let Some(closure) = self._blur_closure.as_ref() { + self.host + .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref()) + .unwrap() + } + + self._blur_closure = None; + } + + fn position_against_target(&mut self, target: &HtmlElement) { + let target_rect = target.get_bounding_client_rect(); + let height = target_rect.height(); + let width = target_rect.width(); + let top = target_rect.top(); + let left = target_rect.left(); + + if !self.visible { + // First pass: position at default anchor, invisible + self.top = top + height - 1.0; + self.left = left; + self.visible = false; + } else { + // Second pass: compute actual anchor and reposition + let anchor = calc_relative_position(&self.host, top, left, height, width); + self.anchor.set(anchor); + let modal_rect = self.host.get_bounding_client_rect(); + let (new_top, new_left) = calc_anchor_position(anchor, &target_rect, &modal_rect); + self.top = new_top; + self.left = new_left; + self.rev_vert.set(anchor.is_rev_vert()); + } + } + + fn setup_blur_handler(&mut self, ctx: &Context) { + let on_close = { + let target = ctx.props().target.clone(); + ctx.props().on_close.reform(move |_| { + if let Some(target) = &target { + target.class_list().remove_1("modal-target").unwrap(); + } + }) + }; + + let closure = Closure::wrap(Box::new(move |_: FocusEvent| { + on_close.emit(()); + }) as Box); + + let _ = self + .host + .add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref()); + + self._blur_closure = Some(closure); + } +} + +impl Component for PortalModal { + type Message = PortalModalMsg; + type Properties = PortalModalProps; + + fn create(ctx: &Context) -> Self { + let host: HtmlElement = global::document() + .create_element(ctx.props().tag_name) + .unwrap() + .unchecked_into(); + + host.style().set_property("position", "fixed").unwrap(); + host.style().set_property("z-index", "10000").unwrap(); + let init = ShadowRootInit::new(ShadowRootMode::Open); + let shadow_root = if let Some(elem) = host.shadow_root() { + elem + } else { + host.attach_shadow(&init).unwrap() + } + .unchecked_into::(); + + Self { + host, + shadow_root, + top: 0.0, + left: 0.0, + visible: false, + rev_vert: Default::default(), + anchor: Default::default(), + _blur_closure: None, + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + PortalModalMsg::Reposition => { + self.visible = true; + true + }, + } + } + + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + let new_target = &ctx.props().target; + let old_target = &old_props.target; + + match (old_target, new_target, self._blur_closure.as_ref()) { + (None, Some(_), Some(closure)) => { + self.visible = false; + self.host + .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref()) + .unwrap(); + + self._blur_closure = None; + }, + (None, Some(_), None) => { + self.visible = false; + self._blur_closure = None; + }, + (Some(_), None, _) => { + self.detach_from_body(); + return true; + }, + _ => {}, + } + + true + } + + fn view(&self, ctx: &Context) -> Html { + let target = &ctx.props().target; + if target.is_none() { + return html! {}; + } + + let opacity = if self.visible { "" } else { ";opacity:0" }; + let css = format!( + ":host{{top:{}px;left:{}px{}}}", + self.top, self.left, opacity + ); + + let portal_content = html! { + <> + + context={self.rev_vert.clone()}> + + { for ctx.props().children.iter() } + + > + + }; + + yew::create_portal(portal_content, self.shadow_root.clone()) + } + + fn rendered(&mut self, ctx: &Context, _first_render: bool) { + if let Some(target) = &ctx.props().target { + if !self.host.is_connected() { + let theme = ctx.props().theme.as_str(); + self.host.set_attribute("theme", theme).unwrap(); + + // First render with a target: attach to body, position invisible + self.position_against_target(target); + self.attach_to_body(); + + // Propagate theme from target + if let Some(theme) = target.get_attribute("theme") { + let _ = self.host.set_attribute("theme", &theme); + } + + target.class_list().add_1("modal-target").unwrap(); + + if ctx.props().own_focus { + self.host.set_attribute("tabindex", "0").unwrap(); + self.setup_blur_handler(ctx); + } + + // Schedule second positioning pass + let link = ctx.link().clone(); + wasm_bindgen_futures::spawn_local(async move { + request_animation_frame().await; + link.send_message(PortalModalMsg::Reposition); + }); + } else if self.visible { + // Second pass: reposition with correct anchor + self.position_against_target(target); + + if ctx.props().own_focus && self._blur_closure.is_some() { + let _ = self.host.focus(); + } + } + } + } + + fn destroy(&mut self, ctx: &Context) { + if let Some(target) = &ctx.props().target { + target.class_list().remove_1("modal-target").unwrap(); + if target.get_attribute("theme").is_some() { + let _ = self.host.remove_attribute("theme"); + } + + let event = CustomEvent::new("-perspective-close-expression").unwrap(); + let _ = target.dispatch_event(&event); + } + + self.detach_from_body(); + } +} diff --git a/rust/perspective-viewer/src/rust/components/settings_panel.rs b/rust/perspective-viewer/src/rust/components/settings_panel.rs index 4cc7418ed3..17e8d02d62 100644 --- a/rust/perspective-viewer/src/rust/components/settings_panel.rs +++ b/rust/perspective-viewer/src/rust/components/settings_panel.rs @@ -38,20 +38,23 @@ pub struct SettingsPanelProps { /// Value props threaded from the root's `RendererProps` / `SessionProps`. pub plugin_name: Option, - pub available_plugins: Rc>, + pub available_plugins: PtrEqRc>, pub has_table: bool, pub named_column_count: usize, - pub view_config: Rc, + pub view_config: PtrEqRc, /// Column currently being dragged (if any) — threaded to show drag /// highlights without per-component `DragDrop` PubSub subscriptions. pub drag_column: Option, /// Cloned session metadata snapshot — threaded from `SessionProps` /// so that metadata changes trigger re-renders via prop diffing. - pub metadata: Rc, + pub metadata: SessionMetadataRc, /// Snapshot of the column-settings sidebar state — threaded from /// `PresentationProps` so that open/close triggers re-renders. pub open_column_settings: OpenColumnSettings, + /// Selected theme name, threaded for PortalModal consumers. + pub selected_theme: Option, + /// State pub dragdrop: DragDrop, pub session: Session, @@ -70,6 +73,7 @@ impl PartialEq for SettingsPanelProps { && self.drag_column == rhs.drag_column && self.metadata == rhs.metadata && self.open_column_settings == rhs.open_column_settings + && self.selected_theme == rhs.selected_theme } } @@ -117,25 +121,26 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html { if !session.is_errored() { let metadata = renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); + let prev_metadata = renderer.metadata(); let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); let rollup_features = session_metadata .get_features() .map(|x| x.get_group_rollup_modes()) .unwrap(); + let group_rollups = requirements.get_group_rollups(&rollup_features); - let all_columns: Vec<_> = session_metadata - .get_table_columns() - .into_iter() - .flatten() - .cloned() - .map(Some) - .collect(); let mut update = ViewConfigUpdate { group_rollup_mode: group_rollups.first().cloned(), ..ViewConfigUpdate::default() }; - update.set_update_column_defaults(&session_metadata, &all_columns, requirements); + + update.set_update_column_defaults( + &session_metadata, + &session.get_view_config().columns, + requirements, + ); + if session.update_view_config(update).is_ok() { clone!(renderer, session); ApiFuture::spawn(async move { @@ -143,6 +148,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html { renderer.draw(session.validate().await?.create_view()).await }); } + presentation.set_open_column_settings(None); } }) @@ -170,6 +176,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html { view_config={props.view_config.clone()} drag_column={props.drag_column.clone()} metadata={props.metadata.clone()} + selected_theme={props.selected_theme.clone()} {dragdrop} renderer={renderer.clone()} session={session.clone()} diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index e741b6fe9b..f4e48a01a7 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -10,22 +10,24 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - +use wasm_bindgen_futures::spawn_local; use web_sys::*; use yew::prelude::*; use super::status_indicator::StatusIndicator; use super::style::LocalStyle; use crate::components::containers::select::*; +use crate::components::copy_dropdown::CopyDropDownMenu; +use crate::components::export_dropdown::ExportDropDownMenu; +use crate::components::portal::PortalModal; use crate::components::status_bar_counter::StatusBarRowsCounter; -use crate::custom_elements::copy_dropdown::*; -use crate::custom_elements::export_dropdown::*; use crate::custom_events::CustomEvents; +use crate::js::*; use crate::presentation::Presentation; use crate::renderer::*; use crate::session::*; use crate::tasks::*; +use crate::utils::*; use crate::*; #[derive(Clone, Properties)] @@ -55,7 +57,7 @@ pub struct StatusBarProps { /// visibility_changed subscriptions. pub is_settings_open: bool, pub selected_theme: Option, - pub available_themes: Rc>, + pub available_themes: PtrEqRc>, /// Whether this viewer is hosted inside a ``. pub is_workspace: bool, @@ -118,6 +120,8 @@ pub enum StatusBarMsg { Reset(MouseEvent), Export, Copy, + CloseExport, + CloseCopy, Noop, Eject, SetTheme(String), @@ -137,6 +141,8 @@ pub struct StatusBar { /// change (blur / Enter). Reset to the prop value whenever the prop /// changes. title: Option, + copy_target: Option, + export_target: Option, } impl Component for StatusBar { @@ -150,6 +156,8 @@ impl Component for StatusBar { input_ref: NodeRef::default(), statusbar_ref: NodeRef::default(), title: ctx.props().title.clone(), + copy_target: None, + export_target: None, } } @@ -196,14 +204,20 @@ impl Component for StatusBar { false }, StatusBarMsg::Export => { - let target = self.export_ref.cast::().into_apierror()?; - ExportDropDownMenuElement::new_from_model(ctx.props()).open(target); - false + self.export_target = self.export_ref.cast::(); + true }, StatusBarMsg::Copy => { - let target = self.copy_ref.cast::().into_apierror()?; - CopyDropDownMenuElement::new_from_model(ctx.props()).open(target); - false + self.copy_target = self.copy_ref.cast::(); + true + }, + StatusBarMsg::CloseExport => { + self.export_target = None; + true + }, + StatusBarMsg::CloseCopy => { + self.copy_target = None; + true }, StatusBarMsg::Eject => { ctx.props().presentation().on_eject.emit(()); @@ -307,6 +321,43 @@ impl Component for StatusBar { || is_settings_open || presentation.is_active(&self.input_ref.cast::()); + let on_copy_select = { + let props = ctx.props().clone(); + let link = ctx.link().clone(); + Callback::from(move |x: ExportFile| { + let props = props.clone(); + let link = link.clone(); + spawn_local(async move { + let mime = x.method.mimetype(x.is_chart); + let task = props.export_method_to_blob(x.method); + let result = copy_to_clipboard(task, mime).await; + crate::maybe_log!({ + result?; + link.send_message(StatusBarMsg::CloseCopy); + }) + }) + }) + }; + + let on_export_select = { + let props = ctx.props().clone(); + let link = ctx.link().clone(); + Callback::from(move |x: ExportFile| { + if !x.name.is_empty() { + clone!(props, link); + spawn_local(async move { + let val = props.export_method_to_blob(x.method).await.unwrap(); + let is_chart = props.renderer().is_chart(); + download(&x.as_filename(is_chart), &val).unwrap(); + link.send_message(StatusBarMsg::CloseExport); + }) + } + }) + }; + + let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy); + let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport); + if is_settings { html! { <> @@ -386,6 +437,28 @@ impl Component for StatusBar {
}
+ + + + + + } } else if let Some(x) = ctx.props().on_settings.as_ref() { @@ -405,7 +478,7 @@ impl Component for StatusBar { #[derive(Properties, PartialEq)] struct ThemeSelectorProps { pub theme: Option, - pub themes: Rc>, + pub themes: PtrEqRc>, pub on_reset: Callback<()>, pub on_change: Callback, } diff --git a/rust/perspective-viewer/src/rust/components/viewer.rs b/rust/perspective-viewer/src/rust/components/viewer.rs index 6070d0410b..bd0f789655 100644 --- a/rust/perspective-viewer/src/rust/components/viewer.rs +++ b/rust/perspective-viewer/src/rust/components/viewer.rs @@ -169,7 +169,7 @@ impl Component for PerspectiveViewer { let subscriptions = create_subscriptions(ctx); let session_props = ctx.props().session.to_props(); let renderer_props = ctx.props().renderer.to_props(None); - let presentation_props = ctx.props().presentation.to_props(std::rc::Rc::new(vec![])); + let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![])); // Memoized callback for column settings drawer let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings { @@ -183,7 +183,7 @@ impl Component for PerspectiveViewer { // subscription is registered. { let presentation = ctx.props().presentation.clone(); - let cb = ctx.link().callback(move |themes: Rc>| { + let cb = ctx.link().callback(move |themes: PtrEqRc>| { UpdatePresentation(Box::new(presentation.to_props(themes))) }); @@ -522,6 +522,7 @@ impl Component for PerspectiveViewer { {drag_column} metadata={metadata.clone()} open_column_settings={self.presentation_props.open_column_settings.clone()} + selected_theme={self.presentation_props.selected_theme.clone()} {dragdrop} {presentation} {renderer} @@ -550,6 +551,7 @@ impl Component for PerspectiveViewer { plugin_name={self.renderer_props.plugin_name.clone()} {metadata} view_config={self.session_props.config.clone()} + selected_theme={self.presentation_props.selected_theme.clone()} {custom_events} {presentation} {renderer} @@ -750,7 +752,7 @@ fn create_subscriptions(ctx: &Context) -> Vec { let cb_theme = { let pres = presentation.clone(); ctx.link() - .callback(move |(themes, _): (std::rc::Rc>, _)| { + .callback(move |(themes, _): (PtrEqRc>, _)| { UpdatePresentation(Box::new(pres.to_props(themes))) }) }; diff --git a/rust/perspective-viewer/src/rust/custom_elements/column_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/column_dropdown.rs deleted file mode 100644 index 600f96d60f..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/column_dropdown.rs +++ /dev/null @@ -1,123 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use std::collections::HashSet; - -use perspective_client::clone; -use perspective_client::config::Expression; -use perspective_js::json; -use perspective_js::utils::global; -use wasm_bindgen::JsCast; -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::html::ImplicitClone; -use yew::{Callback, props}; - -use crate::components::column_dropdown::*; -use crate::components::column_selector::InPlaceColumn; -use crate::custom_elements::modal::*; -use crate::session::Session; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct ColumnDropDownElement { - modal: ModalElement, - session: Session, -} - -impl ImplicitClone for ColumnDropDownElement {} - -impl ColumnDropDownElement { - pub fn new(session: Session) -> Self { - let dropdown = global::document() - .create_element("perspective-dropdown") - .unwrap() - .unchecked_into::(); - - let props = props!(ColumnDropDownProps {}); - let modal = ModalElement::new(dropdown, props, false, None); - Self { modal, session } - } - - pub fn autocomplete( - &self, - target: HtmlInputElement, - exclude: HashSet, - callback: Callback, - ) -> Option<()> { - let input = target.value(); - let metadata = self.session.metadata(); - let mut values: Vec = vec![]; - let small_input = input.to_lowercase(); - for col in metadata.get_table_columns()? { - if !exclude.contains(col) && col.to_lowercase().contains(&small_input) { - values.push(InPlaceColumn::Column(col.to_owned())); - } - } - - for col in self.session.metadata().get_expression_columns() { - if !exclude.contains(col) && col.to_lowercase().contains(&small_input) { - values.push(InPlaceColumn::Column(col.to_owned())); - } - } - - clone!(self.modal, self.session); - ApiFuture::spawn(async move { - if !exclude.contains(&input) { - let is_expr = session.validate_expr(&input).await?.is_none(); - - if is_expr { - values.push(InPlaceColumn::Expression(Expression::new( - None, - input.into(), - ))); - } - } - - let classes = modal.custom_element.class_list(); - let no_results = json!(["no-results"]); - if values.is_empty() { - classes.add(&no_results).unwrap(); - } else { - classes.remove(&no_results).unwrap(); - } - - modal.send_message_batch(vec![ - ColumnDropDownMsg::SetCallback(callback), - ColumnDropDownMsg::SetValues(values, target.get_bounding_client_rect().width()), - ]); - - modal.open(target.unchecked_into(), None).await - }); - - Some(()) - } - - pub fn item_select(&self) { - self.modal.send_message(ColumnDropDownMsg::ItemSelect); - } - - pub fn item_down(&self) { - self.modal.send_message(ColumnDropDownMsg::ItemDown); - } - - pub fn item_up(&self) { - self.modal.send_message(ColumnDropDownMsg::ItemUp); - } - - pub fn hide(&self) -> ApiResult<()> { - self.modal.hide() - } - - pub fn connected_callback(&self) {} -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs index 54a4ab1f85..59479b4055 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs @@ -13,24 +13,91 @@ use std::cell::RefCell; use std::rc::Rc; -use ::perspective_js::utils::{global, *}; +use perspective_js::utils::global; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::*; -use yew::*; +use yew::prelude::*; -use super::modal::*; use super::viewer::PerspectiveViewerElement; -use crate::components::copy_dropdown::{CopyDropDownMenu, CopyDropDownMenuProps}; +use crate::components::copy_dropdown::CopyDropDownMenu; +use crate::components::portal::PortalModal; +use crate::components::style::StyleProvider; use crate::js::*; +use crate::renderer::*; use crate::tasks::*; use crate::utils::*; +use crate::*; + +type TargetState = Rc>>; + +#[derive(Properties, PartialEq)] +struct CopyDropDownWrapperProps { + renderer: Renderer, + callback: Callback, + target: TargetState, + custom_element: HtmlElement, + #[prop_or_default] + theme: String, +} + +enum CopyDropDownWrapperMsg { + Open, + Close, +} + +struct CopyDropDownWrapper { + target: Option, +} + +impl Component for CopyDropDownWrapper { + type Message = CopyDropDownWrapperMsg; + type Properties = CopyDropDownWrapperProps; + + fn create(_ctx: &Context) -> Self { + Self { target: None } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + CopyDropDownWrapperMsg::Open => { + self.target = ctx.props().target.borrow().clone(); + true + }, + CopyDropDownWrapperMsg::Close => { + self.target = None; + true + }, + } + } + + fn view(&self, ctx: &Context) -> Html { + let on_close = ctx.link().callback(|_| CopyDropDownWrapperMsg::Close); + html! { + + + + + + } + } +} #[wasm_bindgen] #[derive(Clone)] pub struct CopyDropDownMenuElement { elem: HtmlElement, - modal: Rc>>>, + target: TargetState, + root: Rc>>>, } impl CustomElementMetadata for CopyDropDownMenuElement { @@ -43,24 +110,25 @@ impl CopyDropDownMenuElement { pub fn new(elem: HtmlElement) -> Self { Self { elem, - modal: Default::default(), + target: Default::default(), + root: Default::default(), } } pub fn open(&self, target: HtmlElement) { - if let Some(x) = &*self.modal.borrow() { - ApiFuture::spawn(x.clone().open(target, None)); + *self.target.borrow_mut() = Some(target); + if let Some(root) = self.root.borrow().as_ref() { + root.send_message(CopyDropDownWrapperMsg::Open); } } pub fn hide(&self) -> ApiResult<()> { - let borrowed = self.modal.borrow(); - borrowed.as_ref().into_apierror()?.hide() + if let Some(root) = self.root.borrow().as_ref() { + root.send_message(CopyDropDownWrapperMsg::Close); + } + Ok(()) } - /// Internal Only. - /// - /// Set this custom element model's raw pointer. pub fn __set_model(&self, parent: &PerspectiveViewerElement) { self.set_config_model(parent) } @@ -91,30 +159,43 @@ impl CopyDropDownMenuElement { { let callback = Callback::from({ let model = model.clone_state(); - let modal_rc = self.modal.clone(); + let target = self.target.clone(); + let root = self.root.clone(); move |x: ExportFile| { let model = model.clone(); - let modal = modal_rc.borrow().clone().unwrap(); + let target = target.clone(); + let root = root.clone(); spawn_local(async move { let mime = x.method.mimetype(x.is_chart); let task = model.export_method_to_blob(x.method); let result = copy_to_clipboard(task, mime).await; crate::maybe_log!({ result?; - modal.hide()?; + *target.borrow_mut() = None; + if let Some(root) = root.borrow().as_ref() { + root.send_message(CopyDropDownWrapperMsg::Close); + } }) }) } }); let renderer = model.renderer().clone(); - let props = props!(CopyDropDownMenuProps { + let init = ShadowRootInit::new(ShadowRootMode::Open); + let shadow_root = self + .elem + .attach_shadow(&init) + .unwrap() + .unchecked_into::(); + + let props = yew::props!(CopyDropDownWrapperProps { renderer, callback, - root: self.elem.clone() + target: self.target.clone(), + custom_element: self.elem.clone() }); - let modal = ModalElement::new(self.elem.clone(), props, true, None); - *self.modal.borrow_mut() = Some(modal); + let handle = yew::Renderer::with_root_and_props(shadow_root, props).render(); + *self.root.borrow_mut() = Some(handle); } } diff --git a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs index c24e8b8edc..a347bd0322 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs @@ -17,20 +17,89 @@ use perspective_js::utils::global; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::*; -use yew::*; +use yew::prelude::*; use super::viewer::PerspectiveViewerElement; -use crate::components::export_dropdown::*; -use crate::custom_elements::modal::*; +use crate::components::export_dropdown::ExportDropDownMenu; +use crate::components::portal::PortalModal; +use crate::components::style::StyleProvider; +use crate::renderer::*; +use crate::session::*; use crate::tasks::*; use crate::utils::*; use crate::*; +type TargetState = Rc>>; + +#[derive(Properties, PartialEq)] +struct ExportDropDownWrapperProps { + renderer: Renderer, + session: Session, + callback: Callback, + target: TargetState, + custom_element: HtmlElement, + #[prop_or_default] + theme: String, +} + +enum ExportDropDownWrapperMsg { + Open, + Close, +} + +struct ExportDropDownWrapper { + target: Option, +} + +impl Component for ExportDropDownWrapper { + type Message = ExportDropDownWrapperMsg; + type Properties = ExportDropDownWrapperProps; + + fn create(_ctx: &Context) -> Self { + Self { target: None } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + ExportDropDownWrapperMsg::Open => { + self.target = ctx.props().target.borrow().clone(); + true + }, + ExportDropDownWrapperMsg::Close => { + self.target = None; + true + }, + } + } + + fn view(&self, ctx: &Context) -> Html { + let on_close = ctx.link().callback(|_| ExportDropDownWrapperMsg::Close); + html! { + + + + + + } + } +} + #[wasm_bindgen] #[derive(Clone)] pub struct ExportDropDownMenuElement { elem: HtmlElement, - modal: Rc>>>, + target: TargetState, + root: Rc>>>, } impl CustomElementMetadata for ExportDropDownMenuElement { @@ -43,24 +112,25 @@ impl ExportDropDownMenuElement { pub fn new(elem: HtmlElement) -> Self { Self { elem, - modal: Default::default(), + target: Default::default(), + root: Default::default(), } } pub fn open(&self, target: HtmlElement) { - if let Some(x) = &*self.modal.borrow() { - ApiFuture::spawn(x.clone().open(target, None)); + *self.target.borrow_mut() = Some(target); + if let Some(root) = self.root.borrow().as_ref() { + root.send_message(ExportDropDownWrapperMsg::Open); } } pub fn hide(&self) -> ApiResult<()> { - let borrowed = self.modal.borrow(); - borrowed.as_ref().into_apierror()?.hide() + if let Some(root) = self.root.borrow().as_ref() { + root.send_message(ExportDropDownWrapperMsg::Close); + } + Ok(()) } - /// Internal Only. - /// - /// Set this custom element model's raw pointer. pub fn __set_model(&self, parent: &PerspectiveViewerElement) { self.set_config_model(parent) } @@ -91,15 +161,19 @@ impl ExportDropDownMenuElement { { let callback = Callback::from({ let model = model.clone_state(); - let modal_rc = self.modal.clone(); + let target = self.target.clone(); + let root = self.root.clone(); move |x: ExportFile| { if !x.name.is_empty() { - clone!(modal_rc, model); + clone!(target, root, model); spawn_local(async move { let val = model.export_method_to_blob(x.method).await.unwrap(); let is_chart = model.renderer().is_chart(); download(&x.as_filename(is_chart), &val).unwrap(); - modal_rc.borrow().clone().unwrap().hide().unwrap(); + *target.borrow_mut() = None; + if let Some(root) = root.borrow().as_ref() { + root.send_message(ExportDropDownWrapperMsg::Close); + } }) } } @@ -107,14 +181,22 @@ impl ExportDropDownMenuElement { let renderer = model.renderer().clone(); let session = model.session().clone(); - let props = props!(ExportDropDownMenuProps { + let init = ShadowRootInit::new(ShadowRootMode::Open); + let shadow_root = self + .elem + .attach_shadow(&init) + .unwrap() + .unchecked_into::(); + + let props = yew::props!(ExportDropDownWrapperProps { renderer, session, callback, - root: self.elem.clone() + target: self.target.clone(), + custom_element: self.elem.clone(), }); - let modal = ModalElement::new(self.elem.clone(), props, true, None); - *self.modal.borrow_mut() = Some(modal); + let handle = yew::Renderer::with_root_and_props(shadow_root, props).render(); + *self.root.borrow_mut() = Some(handle); } } diff --git a/rust/perspective-viewer/src/rust/custom_elements/filter_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/filter_dropdown.rs deleted file mode 100644 index bce36d6af9..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/filter_dropdown.rs +++ /dev/null @@ -1,179 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use std::cell::RefCell; -use std::collections::HashSet; -use std::rc::Rc; - -use perspective_client::clone; -use perspective_js::utils::global; -use wasm_bindgen::JsCast; -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::html::ImplicitClone; -use yew::*; - -use crate::components::filter_dropdown::*; -use crate::custom_elements::modal::*; -use crate::session::Session; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct FilterDropDownElement { - modal: ModalElement, - session: Session, - column: Rc>>, - values: Rc>>>, - target: Rc>>, -} - -impl PartialEq for FilterDropDownElement { - fn eq(&self, other: &Self) -> bool { - self.column == other.column && self.values == other.values && self.target == other.target - } -} - -impl ImplicitClone for FilterDropDownElement {} - -impl FilterDropDownElement { - pub fn new(session: Session) -> Self { - let dropdown = global::document() - .create_element("perspective-dropdown") - .unwrap() - .unchecked_into::(); - - let column: Rc>> = Rc::new(RefCell::new(None)); - let props = props!(FilterDropDownProps {}); - let modal = ModalElement::new(dropdown, props, false, None); - let values = Rc::new(RefCell::new(None)); - Self { - modal, - session, - column, - values, - target: Default::default(), - } - } - - pub fn reautocomplete(&self) { - ApiFuture::spawn( - self.modal - .clone() - .open(self.target.borrow().clone().unwrap(), None), - ); - } - - pub fn autocomplete( - &self, - column: (usize, String), - input: String, - exclude: HashSet, - target: HtmlElement, - callback: Callback, - ) { - let current_column = self.column.borrow().clone(); - match current_column { - Some(filter_col) if filter_col == column => { - let values = filter_values(&input, &self.values, &exclude); - if values.len() == 1 && values[0] == input { - self.hide().unwrap(); - } else { - self.modal.send_message_batch(vec![ - FilterDropDownMsg::SetCallback(callback), - FilterDropDownMsg::SetValues(values), - ]); - - if let Some(x) = self.target.borrow().clone() - && !self.modal.is_open() - { - ApiFuture::spawn(self.modal.clone().open(x, None)) - } - } - }, - _ => { - ApiFuture::spawn({ - clone!( - self.modal, - self.session, - self.values, - old_column = self.column, - old_target = self.target - ); - async move { - let all_values = session.get_column_values(column.1.clone()).await?; - *values.borrow_mut() = Some(all_values); - let filter_values = filter_values(&input, &values, &exclude); - if filter_values.len() == 1 && filter_values[0] == input { - *old_column.borrow_mut() = Some(column); - *old_target.borrow_mut() = Some(target.clone()); - let filter_values = self::filter_values("", &values, &exclude); - modal.send_message_batch(vec![ - FilterDropDownMsg::SetCallback(callback), - FilterDropDownMsg::SetValues(filter_values), - ]); - - modal.hide() - } else { - *old_column.borrow_mut() = Some(column); - *old_target.borrow_mut() = Some(target.clone()); - modal.send_message_batch(vec![ - FilterDropDownMsg::SetCallback(callback), - FilterDropDownMsg::SetValues(filter_values), - ]); - - modal.open(target, None).await - } - } - }); - }, - } - } - - pub fn item_select(&self) { - self.modal.send_message(FilterDropDownMsg::ItemSelect); - } - - pub fn item_down(&self) { - self.modal.send_message(FilterDropDownMsg::ItemDown); - } - - pub fn item_up(&self) { - self.modal.send_message(FilterDropDownMsg::ItemUp); - } - - pub fn hide(&self) -> ApiResult<()> { - let result = self.modal.hide(); - drop(self.column.borrow_mut().take()); - result - } - - pub fn connected_callback(&self) {} -} - -fn filter_values( - input: &str, - values: &Rc>>>, - exclude: &HashSet, -) -> Vec { - let input = input.to_lowercase(); - if let Some(values) = &*values.borrow() { - values - .iter() - .filter(|x| x.to_lowercase().contains(&input) && !exclude.contains(x.as_str())) - .take(10) - .cloned() - .collect::>() - } else { - vec![] - } -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs deleted file mode 100644 index 0100c2848a..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs +++ /dev/null @@ -1,115 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use std::cell::RefCell; -use std::rc::Rc; - -use perspective_client::config::{COMPLETIONS, CompletionItemSuggestion}; -use perspective_js::utils::global; -use wasm_bindgen::JsCast; -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::html::ImplicitClone; -use yew::*; - -use crate::components::function_dropdown::*; -use crate::custom_elements::modal::*; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct FunctionDropDownElement { - modal: ModalElement, - target: Rc>>, -} - -impl PartialEq for FunctionDropDownElement { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -impl ImplicitClone for FunctionDropDownElement {} - -impl FunctionDropDownElement { - pub fn reautocomplete(&self) { - ApiFuture::spawn( - self.modal - .clone() - .open(self.target.borrow().clone().unwrap(), None), - ); - } - - pub fn autocomplete( - &self, - input: String, - target: HtmlElement, - callback: Callback, - ) -> ApiResult<()> { - let values = filter_values(&input); - if values.is_empty() { - self.modal.hide()?; - } else { - self.modal.send_message_batch(vec![ - FunctionDropDownMsg::SetCallback(callback), - FunctionDropDownMsg::SetValues(values), - ]); - - ApiFuture::spawn(self.modal.clone().open(target, None)); - } - - Ok(()) - } - - pub fn item_select(&self) { - self.modal.send_message(FunctionDropDownMsg::ItemSelect); - } - - pub fn item_down(&self) { - self.modal.send_message(FunctionDropDownMsg::ItemDown); - } - - pub fn item_up(&self) { - self.modal.send_message(FunctionDropDownMsg::ItemUp); - } - - pub fn hide(&self) -> ApiResult<()> { - self.modal.hide() - } - - pub fn connected_callback(&self) {} -} - -impl Default for FunctionDropDownElement { - fn default() -> Self { - let dropdown = global::document() - .create_element("perspective-dropdown") - .unwrap() - .unchecked_into::(); - - let props = props!(FunctionDropDownProps {}); - let modal = ModalElement::new(dropdown, props, false, None); - Self { - modal, - target: Default::default(), - } - } -} - -fn filter_values(input: &str) -> Vec { - let input = input.to_lowercase(); - COMPLETIONS - .iter() - .filter(|x| x.label.to_lowercase().starts_with(&input)) - .cloned() - .collect::>() -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/mod.rs b/rust/perspective-viewer/src/rust/custom_elements/mod.rs index 340709e496..17ef5d61bd 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/mod.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/mod.rs @@ -13,15 +13,8 @@ //! Each file in `custom_elements` exports a single struct which will be the //! public [`wasm_bindgen`] API to a JavaScript Custom Element. -mod column_dropdown; pub mod copy_dropdown; pub mod debug_plugin; pub mod export_dropdown; -mod filter_dropdown; -mod function_dropdown; pub mod modal; pub mod viewer; - -pub use self::column_dropdown::*; -pub use self::filter_dropdown::*; -pub use self::function_dropdown::*; diff --git a/rust/perspective-viewer/src/rust/custom_elements/modal.rs b/rust/perspective-viewer/src/rust/custom_elements/modal.rs index 440185df4b..0ab7e798c5 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/modal.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/modal.rs @@ -56,81 +56,6 @@ where on_blur: Option>, } -/// Anchor point enum, `ModalCornerTargetCorner` -#[derive(Clone, Copy, Debug, Default)] -enum ModalAnchor { - BottomRightTopLeft, - BottomRightBottomLeft, - BottomRightTopRight, - BottomLeftTopLeft, - TopRightTopLeft, - TopRightBottomRight, - - #[default] - TopLeftBottomLeft, -} - -impl ModalAnchor { - const fn is_rev_vert(&self) -> bool { - matches!( - self, - Self::BottomLeftTopLeft - | Self::BottomRightBottomLeft - | Self::BottomRightTopLeft - | Self::BottomRightTopRight - ) - } -} - -/// Given the bounds of the target element as previous computed, as well as the -/// browser's viewport and the bounds of the already-connected -/// `` element itself, determine a new (top, left) -/// coordinates that keeps the element on-screen. -fn calc_relative_position( - elem: &HtmlElement, - _top: f64, - left: f64, - height: f64, - width: f64, -) -> ModalAnchor { - let window = global::window(); - let rect = elem.get_bounding_client_rect(); - let inner_width = window.inner_width().unwrap().as_f64().unwrap(); - let inner_height = window.inner_height().unwrap().as_f64().unwrap(); - let rect_top = rect.top(); - let rect_height = rect.height(); - let rect_width = rect.width(); - let rect_left = rect.left(); - - let elem_over_y = inner_height < rect_top + rect_height; - let elem_over_x = inner_width < rect_left + rect_width; - let target_over_x = inner_width < rect_left + width; - let target_over_y = inner_height < rect_top + height; - - // modal/target - match (elem_over_y, elem_over_x, target_over_x, target_over_y) { - (true, _, true, true) => ModalAnchor::BottomRightTopLeft, - (true, _, true, false) => ModalAnchor::BottomRightBottomLeft, - (true, true, false, _) => { - if left + width - rect_width > 0.0 { - ModalAnchor::BottomRightTopRight - } else { - ModalAnchor::BottomLeftTopLeft - } - }, - (true, false, false, _) => ModalAnchor::BottomLeftTopLeft, - (false, true, true, _) => ModalAnchor::TopRightTopLeft, - (false, true, false, _) => { - if left + width - rect_width > 0.0 { - ModalAnchor::TopRightBottomRight - } else { - ModalAnchor::TopLeftBottomLeft - } - }, - _ => ModalAnchor::TopLeftBottomLeft, - } -} - impl ModalElement where T: Component, @@ -172,31 +97,10 @@ where } } - fn calc_anchor_position(&self, target: &HtmlElement) -> (f64, f64) { - let elem = target.unchecked_ref::(); - let rect = elem.get_bounding_client_rect(); - let height = rect.height(); - let width = rect.width(); - let top = rect.top(); - let left = rect.left(); - - let self_rect = self.custom_element.get_bounding_client_rect(); - let rect_height = self_rect.height(); - let rect_width = self_rect.width(); - - match self.anchor.get() { - ModalAnchor::BottomRightTopLeft => (top - rect_height, left - rect_width + 1.0), - ModalAnchor::BottomRightBottomLeft => { - (top - rect_height + height, left - rect_width + 1.0) - }, - ModalAnchor::BottomRightTopRight => { - (top - rect_height + 1.0, left + width - rect_width) - }, - ModalAnchor::BottomLeftTopLeft => (top - rect_height + 1.0, left), - ModalAnchor::TopRightTopLeft => (top, left - rect_width + 1.0), - ModalAnchor::TopRightBottomRight => (top + height - 1.0, left + width - rect_width), - ModalAnchor::TopLeftBottomLeft => ((top + height - 1.0), left), - } + fn calc_anchor_pos(&self, target: &HtmlElement) -> (f64, f64) { + let target_rect = target.get_bounding_client_rect(); + let modal_rect = self.custom_element.get_bounding_client_rect(); + calc_anchor_position(self.anchor.get(), &target_rect, &modal_rect) } async fn open_within_viewport(&self, target: HtmlElement) -> ApiResult<()> { @@ -229,7 +133,7 @@ where width, )); - let (top, left) = self.calc_anchor_position(&target); + let (top, left) = self.calc_anchor_pos(&target); let msg = ModalMsg::SetPos { top, left, @@ -310,7 +214,7 @@ where let target = target.clone(); let anchor = self.anchor.clone(); *self.resize_sub.borrow_mut() = Some(resize.add_listener(move |()| { - let (top, left) = this.calc_anchor_position(&target); + let (top, left) = this.calc_anchor_pos(&target); let msg = ModalMsg::SetPos { top, left, diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index 0676602ea4..40ba55ebb3 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -39,6 +39,17 @@ use crate::tasks::*; use crate::utils::*; use crate::*; +#[derive(serde::Deserialize, Default)] +struct ResizeOptions { + dimensions: Option, +} + +#[derive(serde::Deserialize, Clone, Copy)] +struct ResizeDimensions { + width: f64, + height: f64, +} + /// The `` custom element. /// /// # JavaScript Examples @@ -574,11 +585,26 @@ impl PerspectiveViewerElement { PerspectiveViewerMsg::ToggleSettingsComplete(settings, sender), ); + let task = if let OptionalUpdate::Update(_) = &decoded_update.table { + Some(this.session.reset(ResetOptions { + config: true, + expressions: true, + stats: true, + ..ResetOptions::default() + })) + } else { + None + }; + let result = this .restore_and_render(decoded_update.clone(), { clone!(this, decoded_update.table); async move { if let OptionalUpdate::Update(name) = table { + if let Some(task) = task { + task.await?; + } + this.session.set_table(name).await?; this.session .update_column_defaults(&this.renderer.metadata()); @@ -780,24 +806,25 @@ impl PerspectiveViewerElement { /// /// # Arguments /// - /// - `force` - If [`Self::resize`] is called with `false` or without an - /// argument, and _auto-size_ mode is enabled via [`Self::setAutoSize`], - /// [`Self::resize`] will log a warning and auto-disable auto-size mode. + /// - `options` - An optional object with the following fields: + /// - `dimensions` - An optional object `{width, height}` providing + /// explicit size hints (in pixels) for the plugin container. When + /// provided, the plugin element will be temporarily sized to these + /// dimensions during resize, then reset. /// /// # JavaScript Examples /// /// ```javascript - /// await viewer.resize(true) + /// await viewer.resize() + /// await viewer.resize({dimensions: {width: 800, height: 600}}) /// ``` #[wasm_bindgen] - pub fn resize(&self, force: Option) -> ApiFuture<()> { - if !force.unwrap_or_default() && self.resize_handle.borrow().is_some() { - let msg: JsValue = "`resize(false)` called, disabling auto-size. It can be \ - re-enabled with `setAutoSize(true)`." - .into(); - web_sys::console::warn_1(&msg); - *self.resize_handle.borrow_mut() = None; - } + pub fn resize(&self, options: Option) -> ApiFuture<()> { + let opts: ResizeOptions = options + .map(|v| v.into_serde_ext()) + .transpose() + .unwrap_or_default() + .unwrap_or_default(); let state = self.clone_state(); ApiFuture::new_throttled(async move { @@ -805,6 +832,11 @@ impl PerspectiveViewerElement { state .update_and_render(ViewConfigUpdate::default())? .await?; + } else if let Some(dims) = opts.dimensions { + state + .renderer() + .resize_with_dimensions(dims.width, dims.height) + .await?; } else { state.renderer().resize().await?; } diff --git a/rust/perspective-viewer/src/rust/custom_events.rs b/rust/perspective-viewer/src/rust/custom_events.rs index 1bd41ca48e..9392470af3 100644 --- a/rust/perspective-viewer/src/rust/custom_events.rs +++ b/rust/perspective-viewer/src/rust/custom_events.rs @@ -188,6 +188,10 @@ impl CustomEvents { { self.0.0.dispatch_event(name, event) } + + pub fn dispatch_raw_event(&self, event: &web_sys::CustomEvent) -> ApiResult { + self.0.0.elem.dispatch_event(event).map_err(|e| e.into()) + } } impl CustomEventsDataRc { diff --git a/rust/perspective-viewer/src/rust/lib.rs b/rust/perspective-viewer/src/rust/lib.rs index 4bdb2b2aba..a8d07d74cf 100644 --- a/rust/perspective-viewer/src/rust/lib.rs +++ b/rust/perspective-viewer/src/rust/lib.rs @@ -106,8 +106,8 @@ pub fn js_init() { pub fn bootstrap_web_components(psp: &JsValue) { define_web_component::(psp); define_web_component::(psp); - define_web_component::(psp); define_web_component::(psp); + define_web_component::(psp); } /// Defining the web components needs an extern struct to reference the diff --git a/rust/perspective-viewer/src/rust/presentation.rs b/rust/perspective-viewer/src/rust/presentation.rs index 1e09503c89..0eec91bb82 100644 --- a/rust/perspective-viewer/src/rust/presentation.rs +++ b/rust/perspective-viewer/src/rust/presentation.rs @@ -56,7 +56,7 @@ pub struct PresentationHandle { pub on_is_workspace_changed: RefCell>>, pub settings_before_open_changed: PubSub, pub column_settings_open_changed: PubSub<(bool, Option)>, - pub theme_config_updated: PubSub<(Rc>, Option)>, + pub theme_config_updated: PubSub<(PtrEqRc>, Option)>, pub on_eject: PubSub<()>, } @@ -184,7 +184,7 @@ impl Presentation { /// Get the available theme names from the browser environment by parsing /// readable stylesheets. This method is memoized - the state can be /// flushed by calling `reset()`. - pub async fn get_available_themes(&self) -> ApiResult>> { + pub async fn get_available_themes(&self) -> ApiResult>> { let mut data = self.0.theme_data.lock().await; if data.themes.is_none() { await_dom_loaded().await?; @@ -213,7 +213,9 @@ impl Presentation { changed } - pub async fn get_selected_theme_config(&self) -> ApiResult<(Rc>, Option)> { + pub async fn get_selected_theme_config( + &self, + ) -> ApiResult<(PtrEqRc>, Option)> { let themes = self.get_available_themes().await?; let name = self.0.viewer_elem.get_attribute("theme"); let index = name @@ -325,7 +327,7 @@ impl Presentation { /// /// `available_themes` must be provided by the caller because theme /// detection is async and therefore not available synchronously here. - pub fn to_props(&self, available_themes: Rc>) -> PresentationProps { + pub fn to_props(&self, available_themes: PtrEqRc>) -> PresentationProps { let theme_attr = self.0.viewer_elem.get_attribute("theme"); let selected_theme = theme_attr.as_deref().and_then(|name| { available_themes diff --git a/rust/perspective-viewer/src/rust/presentation/props.rs b/rust/perspective-viewer/src/rust/presentation/props.rs index 8c0d9b7ed6..51ee980ebd 100644 --- a/rust/perspective-viewer/src/rust/presentation/props.rs +++ b/rust/perspective-viewer/src/rust/presentation/props.rs @@ -10,9 +10,8 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use crate::presentation::OpenColumnSettings; +use crate::utils::PtrEqRc; /// Value-semantic snapshot of the presentation/UI state used by the root /// component to drive `is_settings_open`, `selected_theme`, and @@ -27,7 +26,7 @@ pub struct PresentationProps { pub is_settings_open: bool, /// Detected theme names, in discovery order. - pub available_themes: Rc>, + pub available_themes: PtrEqRc>, /// The currently selected theme name, if any theme is active. pub selected_theme: Option, diff --git a/rust/perspective-viewer/src/rust/renderer.rs b/rust/perspective-viewer/src/rust/renderer.rs index 905d546b4e..66393a081f 100644 --- a/rust/perspective-viewer/src/rust/renderer.rs +++ b/rust/perspective-viewer/src/rust/renderer.rs @@ -328,6 +328,31 @@ impl Renderer { .await } + pub async fn resize_with_dimensions(&self, width: f64, height: f64) -> ApiResult<()> { + let draw_mutex = self.draw_lock(); + let timer = self.render_timer(); + draw_mutex + .debounce(async { + set_timeout(timer.get_throttle()).await?; + let plugin = self.get_active_plugin()?; + let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref(); + let rect = main_panel.get_bounding_client_rect(); + if (height - rect.height()).abs() > 0.5 || (width - rect.width()).abs() > 0.5 { + let new_width = format!("{}px", width); + let new_height = format!("{}px", height); + main_panel.style().set_property("width", &new_width)?; + main_panel.style().set_property("height", &new_height)?; + let result = plugin.resize().await; + main_panel.style().set_property("width", "")?; + main_panel.style().set_property("height", "")?; + result?; + } + + Ok(()) + }) + .await + } + /// This will take a future which _should_ create a new view and then will /// draw it. As the `session` closure is asynchronous, it can be cancelled /// by returning `None`. @@ -521,7 +546,7 @@ impl Renderer { plugin_name: None, requirements: ViewConfigRequirements::default(), render_limits, - available_plugins: Rc::new(vec![]), + available_plugins: PtrEqRc::new(vec![]), } } } diff --git a/rust/perspective-viewer/src/rust/renderer/props.rs b/rust/perspective-viewer/src/rust/renderer/props.rs index 6681765d23..8d042b6f56 100644 --- a/rust/perspective-viewer/src/rust/renderer/props.rs +++ b/rust/perspective-viewer/src/rust/renderer/props.rs @@ -10,10 +10,9 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use crate::js::plugin::ViewConfigRequirements; use crate::renderer::limits::RenderLimits; +use crate::utils::PtrEqRc; /// Value-semantic snapshot of the renderer state read by components. /// @@ -31,5 +30,5 @@ pub struct RendererProps { pub render_limits: Option, /// Names of all registered plugins, in registration order. - pub available_plugins: Rc>, + pub available_plugins: PtrEqRc>, } diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index 900550808d..2a46556fb1 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -32,7 +32,7 @@ use yew::html::ImplicitClone; use yew::prelude::*; use self::metadata::*; -pub use self::metadata::{MetadataRef, SessionMetadata}; +pub use self::metadata::{MetadataRef, SessionMetadata, SessionMetadataRc}; pub use self::props::SessionProps; pub use self::view_subscription::ViewStats; use self::view_subscription::*; @@ -541,7 +541,7 @@ impl Session { use self::column_defaults_update::*; config_update.set_update_column_defaults( &self.metadata(), - &self.all_columns().into_iter().map(Some).collect::>(), + &self.get_view_config().columns, requirements, ) } @@ -746,12 +746,12 @@ impl Session { pub fn to_props(&self) -> SessionProps { let data = self.borrow(); SessionProps { - config: Rc::new(data.config.clone()), + config: PtrEqRc::new(data.config.clone()), stats: data.stats.clone(), has_table: data.table.is_some(), error: data.error.clone(), title: data.title.clone(), - metadata: Rc::new(data.metadata.clone()), + metadata: PtrEqRc::new(data.metadata.clone()), } } } diff --git a/rust/perspective-viewer/src/rust/session/metadata.rs b/rust/perspective-viewer/src/rust/session/metadata.rs index 7738566fa9..cdd7c1fed0 100644 --- a/rust/perspective-viewer/src/rust/session/metadata.rs +++ b/rust/perspective-viewer/src/rust/session/metadata.rs @@ -17,6 +17,7 @@ use std::ops::{Deref, DerefMut}; use perspective_client::config::*; use perspective_js::apierror; +use crate::utils::PtrEqRc; use crate::*; #[derive(Clone, PartialEq)] @@ -32,6 +33,8 @@ struct SessionViewExpressionMetadata { #[derive(Clone, Default, PartialEq)] pub struct SessionMetadata(Option); +pub type SessionMetadataRc = PtrEqRc; + impl std::fmt::Debug for SessionMetadata { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("SessionMetadata { .. }") diff --git a/rust/perspective-viewer/src/rust/session/props.rs b/rust/perspective-viewer/src/rust/session/props.rs index c14dc7327c..7389e4b046 100644 --- a/rust/perspective-viewer/src/rust/session/props.rs +++ b/rust/perspective-viewer/src/rust/session/props.rs @@ -10,15 +10,14 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use std::rc::Rc; - use perspective_client::config::*; use crate::js::plugin::ViewConfigRequirements; use crate::session::column_defaults_update::ViewConfigUpdateExt; use crate::session::drag_drop_update::ViewConfigExt as DragDropExt; +use crate::session::metadata::SessionMetadataRc; use crate::session::replace_expression_update::ViewConfigExt as ReplaceExprExt; -use crate::session::{SessionMetadata, TableErrorState, ViewStats}; +use crate::session::{TableErrorState, ViewStats}; use crate::utils::*; /// Value-semantic snapshot of the session state read by the root component. @@ -29,7 +28,7 @@ use crate::utils::*; #[derive(Clone, Debug, PartialEq, Default)] pub struct SessionProps { /// The current `ViewConfig` driving the active `View`. - pub config: Rc, + pub config: PtrEqRc, /// Row/column statistics for the status bar. pub stats: Option, @@ -48,7 +47,7 @@ pub struct SessionProps { /// `to_props()` call. Components read column types, features, /// expression info, etc. from this snapshot instead of borrowing /// `Session`'s `RefCell` directly. - pub metadata: Rc, + pub metadata: SessionMetadataRc, } impl SessionProps { diff --git a/rust/perspective-viewer/src/rust/utils/mod.rs b/rust/perspective-viewer/src/rust/utils/mod.rs index d906e4cf40..3e06d5c644 100644 --- a/rust/perspective-viewer/src/rust/utils/mod.rs +++ b/rust/perspective-viewer/src/rust/utils/mod.rs @@ -21,7 +21,9 @@ mod custom_element; mod datetime; mod debounce; mod hooks; +mod modal_position; mod number_format; +mod ptr_eq_rc; mod pubsub; mod weak_scope; @@ -33,8 +35,10 @@ pub use custom_element::*; pub use datetime::*; pub use debounce::*; pub use hooks::*; +pub use modal_position::*; pub use number_format::*; pub use perspective_client::clone; +pub use ptr_eq_rc::*; pub use pubsub::*; pub use weak_scope::*; diff --git a/rust/perspective-viewer/src/rust/utils/modal_position.rs b/rust/perspective-viewer/src/rust/utils/modal_position.rs new file mode 100644 index 0000000000..c6615f7813 --- /dev/null +++ b/rust/perspective-viewer/src/rust/utils/modal_position.rs @@ -0,0 +1,110 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use perspective_js::utils::global; +use web_sys::*; + +/// Anchor point enum, `ModalCornerTargetCorner` +#[derive(Clone, Copy, Debug, Default)] +pub enum ModalAnchor { + BottomRightTopLeft, + BottomRightBottomLeft, + BottomRightTopRight, + BottomLeftTopLeft, + TopRightTopLeft, + TopRightBottomRight, + + #[default] + TopLeftBottomLeft, +} + +impl ModalAnchor { + pub const fn is_rev_vert(&self) -> bool { + matches!( + self, + Self::BottomLeftTopLeft + | Self::BottomRightBottomLeft + | Self::BottomRightTopLeft + | Self::BottomRightTopRight + ) + } +} + +/// Given the bounds of the target element as previously computed, as well as +/// the browser's viewport and the bounds of the already-connected modal element +/// itself, determine the best anchor point to keep the element on-screen. +pub fn calc_relative_position( + elem: &HtmlElement, + _top: f64, + left: f64, + height: f64, + width: f64, +) -> ModalAnchor { + let window = global::window(); + let rect = elem.get_bounding_client_rect(); + let inner_width = window.inner_width().unwrap().as_f64().unwrap(); + let inner_height = window.inner_height().unwrap().as_f64().unwrap(); + let rect_top = rect.top(); + let rect_height = rect.height(); + let rect_width = rect.width(); + let rect_left = rect.left(); + + let elem_over_y = inner_height < rect_top + rect_height; + let elem_over_x = inner_width < rect_left + rect_width; + let target_over_x = inner_width < rect_left + width; + let target_over_y = inner_height < rect_top + height; + + // modal/target + match (elem_over_y, elem_over_x, target_over_x, target_over_y) { + (true, _, true, true) => ModalAnchor::BottomRightTopLeft, + (true, _, true, false) => ModalAnchor::BottomRightBottomLeft, + (true, true, false, _) => { + if left + width - rect_width > 0.0 { + ModalAnchor::BottomRightTopRight + } else { + ModalAnchor::BottomLeftTopLeft + } + }, + (true, false, false, _) => ModalAnchor::BottomLeftTopLeft, + (false, true, true, _) => ModalAnchor::TopRightTopLeft, + (false, true, false, _) => { + if left + width - rect_width > 0.0 { + ModalAnchor::TopRightBottomRight + } else { + ModalAnchor::TopLeftBottomLeft + } + }, + _ => ModalAnchor::TopLeftBottomLeft, + } +} + +/// Calculate the (top, left) position for a modal element given an anchor +/// point, target element bounding rect, and the modal element's own bounding +/// rect. +pub fn calc_anchor_position(anchor: ModalAnchor, target: &DomRect, modal: &DomRect) -> (f64, f64) { + let height = target.height(); + let width = target.width(); + let top = target.top(); + let left = target.left(); + let rect_height = modal.height(); + let rect_width = modal.width(); + + match anchor { + ModalAnchor::BottomRightTopLeft => (top - rect_height, left - rect_width + 1.0), + ModalAnchor::BottomRightBottomLeft => (top - rect_height + height, left - rect_width + 1.0), + ModalAnchor::BottomRightTopRight => (top - rect_height + 1.0, left + width - rect_width), + ModalAnchor::BottomLeftTopLeft => (top - rect_height + 1.0, left), + ModalAnchor::TopRightTopLeft => (top, left - rect_width + 1.0), + ModalAnchor::TopRightBottomRight => (top + height - 1.0, left + width - rect_width), + ModalAnchor::TopLeftBottomLeft => (top + height - 1.0, left), + } +} diff --git a/rust/perspective-viewer/src/rust/utils/ptr_eq_rc.rs b/rust/perspective-viewer/src/rust/utils/ptr_eq_rc.rs new file mode 100644 index 0000000000..727bb68199 --- /dev/null +++ b/rust/perspective-viewer/src/rust/utils/ptr_eq_rc.rs @@ -0,0 +1,74 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use std::ops::Deref; +use std::rc::Rc; + +use yew::html::{ImplicitClone, IntoPropValue}; + +/// A thin wrapper around `Rc` whose `PartialEq` uses pointer identity +/// (`Rc::ptr_eq`) instead of deep structural comparison. This makes it +/// suitable for Yew `Properties` fields that hold large, cheaply-shared +/// snapshots (e.g. `ViewConfig`, `SessionMetadata`, `Vec`). +pub struct PtrEqRc(Rc); + +impl PtrEqRc { + pub fn new(val: T) -> Self { + Self(Rc::new(val)) + } +} + +impl Clone for PtrEqRc { + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +impl PartialEq for PtrEqRc { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} + +impl Deref for PtrEqRc { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl From for PtrEqRc { + fn from(rc: T) -> Self { + Self(Rc::new(rc)) + } +} + +impl Default for PtrEqRc { + fn default() -> Self { + Self(Rc::default()) + } +} + +impl std::fmt::Debug for PtrEqRc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl ImplicitClone for PtrEqRc {} + +impl IntoPropValue> for Rc { + fn into_prop_value(self) -> PtrEqRc { + PtrEqRc(self) + } +} diff --git a/rust/perspective-viewer/src/rust/utils/pubsub.rs b/rust/perspective-viewer/src/rust/utils/pubsub.rs index 38f8235297..e68a2ecb56 100644 --- a/rust/perspective-viewer/src/rust/utils/pubsub.rs +++ b/rust/perspective-viewer/src/rust/utils/pubsub.rs @@ -84,7 +84,9 @@ impl PubSubInternal { /// publishers, without leaking callbacks as listeners are dropped. /// /// Unlike `mpsc` etc., `PubSub` has no internal queue and is completely -/// synchronous. +/// synchronous. Explicitly does not implement clone, as this is intended as +/// RAII, even though the internal data structures are `Clone` because they +/// need to be sent to listeners. #[derive(Derivative)] #[derivative(Default(bound = ""))] pub struct PubSub(Rc>); diff --git a/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts b/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts index 1e6dc18d82..be2f24ba15 100644 --- a/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts +++ b/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts @@ -42,7 +42,7 @@ test.describe("Cancellable Views", () => { const view = await viewer.getView(); await view.delete(); - await viewer.resize(true); + await viewer.resize({ force: true }); }); const contents = await get_contents(page); diff --git a/tools/test/results.tar.gz b/tools/test/results.tar.gz index bb24cb7a98..7ae0d34b0d 100644 Binary files a/tools/test/results.tar.gz and b/tools/test/results.tar.gz differ