1use std::collections::HashSet;
14use std::rc::Rc;
15
16use perspective_client::config::{ColumnType, WindowFrame, WindowSort, WindowSortDir, WindowSpec};
17use wasm_bindgen::JsCast;
18use web_sys::{DragEvent, HtmlInputElement, MouseEvent};
19use yew::prelude::*;
20
21use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
22use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
23use crate::components::containers::dragdrop_list::{
24 DragContext, DragDropList, DragDropListItemProps,
25};
26use crate::components::containers::select::{Select, SelectItem};
27use crate::presentation::Presentation;
28use crate::session::{Session, SessionMetadataRc};
29use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
30
31fn op_spec(
35 metadata: &SessionMetadataRc,
36 source: &str,
37 op: &str,
38) -> Option<perspective_client::proto::WindowAggregateArgs> {
39 let ty = metadata.get_column_table_type(source)?;
40 metadata.get_window_aggregate(ty, op)
41}
42
43fn frame_label(frame: &str) -> &'static str {
45 match frame {
46 "rows" => "Rows",
47 "range" => "Range",
48 _ => "Cumulative",
49 }
50}
51
52fn frame_name(label: &str) -> &'static str {
53 match label {
54 "Rows" => "rows",
55 "Range" => "range",
56 _ => "cumulative",
57 }
58}
59
60fn is_orderable_for_range(ty: ColumnType) -> bool {
61 matches!(
62 ty,
63 ColumnType::Integer | ColumnType::Float | ColumnType::Date | ColumnType::Datetime
64 )
65}
66
67#[derive(Clone, Properties)]
68pub struct WindowEditorProps {
69 pub metadata: SessionMetadataRc,
70
71 pub initial: Option<WindowSpec>,
73
74 pub on_change: Callback<Option<WindowSpec>>,
78
79 pub reset_count: u8,
81
82 pub presentation: Presentation,
86
87 pub session: Session,
90
91 #[prop_or_default]
94 pub selected_theme: Option<String>,
95}
96
97impl PartialEq for WindowEditorProps {
98 fn eq(&self, other: &Self) -> bool {
99 self.metadata == other.metadata
100 && self.initial == other.initial
101 && self.reset_count == other.reset_count
102 && self.selected_theme == other.selected_theme
103 }
104}
105
106#[derive(Clone, PartialEq)]
114struct WindowDraft {
115 op: String,
116 source: String,
117 order_by: String,
118 order_desc: bool,
119 partition_by: Vec<String>,
120 frame_type: String,
121 frame_rows: u32,
122 frame_range: f64,
123 offset: u32,
124 alpha: f64,
125}
126
127impl Default for WindowDraft {
128 fn default() -> Self {
129 Self {
130 op: String::default(),
131 source: String::default(),
132 order_by: String::default(),
133 order_desc: false,
134 partition_by: vec![],
135 frame_type: String::default(),
136 frame_rows: 1,
137 frame_range: 1.0,
138 offset: 1,
139 alpha: 1.0,
140 }
141 }
142}
143
144impl WindowDraft {
145 fn from_spec(spec: &WindowSpec) -> Self {
146 let (frame_type, frame_rows, frame_range) = match spec.frame {
147 Some(WindowFrame::Rows(n)) => ("Rows", n, 1.0),
148 Some(WindowFrame::Range(x)) => ("Range", 1, x),
149 Some(WindowFrame::Cumulative) | None => ("Cumulative", 1, 1.0),
150 };
151
152 Self {
153 op: spec.aggregate.clone(),
154 source: spec.column.clone(),
155 order_by: spec
156 .order_by
157 .as_ref()
158 .map(|x| x.0.clone())
159 .unwrap_or_default(),
160 order_desc: spec
161 .order_by
162 .as_ref()
163 .map(|x| x.1 == WindowSortDir::Desc)
164 .unwrap_or_default(),
165 partition_by: spec.partition_by.clone(),
166 frame_type: frame_type.to_string(),
167 frame_rows,
168 frame_range,
169 offset: spec.offset.unwrap_or(1),
170 alpha: spec.alpha.unwrap_or(1.0),
171 }
172 }
173
174 fn new_default() -> Self {
175 Self {
176 op: "sum".to_string(),
177 frame_type: "Cumulative".to_string(),
178 ..Self::default()
179 }
180 }
181
182 fn validate(&self, metadata: &SessionMetadataRc) -> Result<WindowSpec, String> {
183 let op = self.op.clone();
184
185 let table_column = |col: &String| {
191 metadata
192 .get_table_columns()
193 .into_iter()
194 .flatten()
195 .any(|x| x == col)
196 };
197
198 if self.source.is_empty() {
199 return Err("Missing Column".to_string());
200 }
201
202 if !table_column(&self.source) {
203 return Err(format!(
205 "\"{}\" must be a table column to source a window",
206 self.source
207 ));
208 }
209
210 let source_ty = metadata
211 .get_column_table_type(&self.source)
212 .ok_or_else(|| format!("Unknown source column \"{}\"", self.source))?;
213
214 let declared = metadata
219 .get_window_aggregate(source_ty, &op)
220 .ok_or_else(|| {
221 format!("\"{op}\" is not a supported window aggregate for this column")
222 })?;
223
224 let order_ty = if self.order_by.is_empty() {
229 if metadata
230 .get_features()
231 .map(|x| x.unordered)
232 .unwrap_or_default()
233 {
234 return Err("Missing Order By".to_string());
235 }
236
237 None
238 } else {
239 if !table_column(&self.order_by) {
240 return Err(format!(
242 "\"{}\" must be a table column to order by",
243 self.order_by
244 ));
245 }
246
247 Some(
248 metadata
249 .get_column_table_type(&self.order_by)
250 .ok_or_else(|| format!("Unknown order by column \"{}\"", self.order_by))?,
251 )
252 };
253
254 for col in self.partition_by.iter() {
255 if !table_column(col) {
257 return Err(format!(
258 "\"{}\" must be a table column to partition by",
259 col
260 ));
261 }
262 }
263
264 let frame = if declared.frames.is_empty() {
267 None
268 } else {
269 let chosen = frame_name(&self.frame_type);
270 if !declared.frames.iter().any(|x| x == chosen) {
271 return Err(format!("\"{op}\" does not support a {chosen} frame"));
272 }
273
274 match self.frame_type.as_str() {
275 "Rows" => Some(WindowFrame::Rows(self.frame_rows)),
276 "Range" => {
277 let Some(order_ty) = order_ty else {
280 return Err("Range frames require an order by column".to_string());
281 };
282
283 if !is_orderable_for_range(order_ty) {
284 return Err("Range frames require a numeric, date or datetime order by \
285 column"
286 .to_string());
287 }
288
289 Some(WindowFrame::Range(self.frame_range))
290 },
291 _ => None,
292 }
293 };
294
295 let offset = (declared.offset && self.offset != 1).then_some(self.offset);
299 let alpha = declared.alpha.then_some(self.alpha);
300
301 let mut partition_by = self.partition_by.clone();
302 partition_by.retain(|col| !col.is_empty());
303
304 Ok(WindowSpec {
305 column: self.source.clone(),
306 aggregate: op,
307 partition_by,
308 order_by: (!self.order_by.is_empty()).then(|| {
309 WindowSort(
310 self.order_by.clone(),
311 if self.order_desc {
312 WindowSortDir::Desc
313 } else {
314 WindowSortDir::Asc
315 },
316 )
317 }),
318 frame,
319 offset,
320 alpha,
321 })
322 }
323}
324
325#[derive(Clone, Debug)]
326pub enum WindowEditorMsg {
327 SetOp(String),
328 ClearSource,
329 ClearOrderBy,
330 ToggleOrderDir,
331 RemovePartition(usize),
332 SetFrameType(String),
333 SetFrameRows(f64),
337 SetFrameRange(f64),
338 SetOffset(f64),
339 SetAlpha(f64),
340 Drop(String, DragTarget, Option<DragTarget>),
344 New(DragTarget, InPlaceColumn),
345 DragEnter(DragTarget, usize),
346 DragLeave(DragTarget),
347}
348
349struct WindowSourceContext;
354struct WindowOrderByContext;
355struct WindowPartitionByContext;
356
357impl DragContext<WindowEditorMsg> for WindowSourceContext {
358 fn close(_index: usize) -> WindowEditorMsg {
359 WindowEditorMsg::ClearSource
360 }
361
362 fn dragenter(index: usize) -> WindowEditorMsg {
363 WindowEditorMsg::DragEnter(DragTarget::WindowSource, index)
364 }
365
366 fn dragleave() -> WindowEditorMsg {
367 WindowEditorMsg::DragLeave(DragTarget::WindowSource)
368 }
369
370 fn create(col: InPlaceColumn) -> WindowEditorMsg {
371 WindowEditorMsg::New(DragTarget::WindowSource, col)
372 }
373
374 fn is_self_move(effect: DragTarget) -> bool {
375 effect == DragTarget::WindowSource
376 }
377}
378
379impl DragContext<WindowEditorMsg> for WindowOrderByContext {
380 fn close(_index: usize) -> WindowEditorMsg {
381 WindowEditorMsg::ClearOrderBy
382 }
383
384 fn dragenter(index: usize) -> WindowEditorMsg {
385 WindowEditorMsg::DragEnter(DragTarget::WindowOrderBy, index)
386 }
387
388 fn dragleave() -> WindowEditorMsg {
389 WindowEditorMsg::DragLeave(DragTarget::WindowOrderBy)
390 }
391
392 fn create(col: InPlaceColumn) -> WindowEditorMsg {
393 WindowEditorMsg::New(DragTarget::WindowOrderBy, col)
394 }
395
396 fn is_self_move(effect: DragTarget) -> bool {
397 effect == DragTarget::WindowOrderBy
398 }
399}
400
401impl DragContext<WindowEditorMsg> for WindowPartitionByContext {
402 fn close(index: usize) -> WindowEditorMsg {
403 WindowEditorMsg::RemovePartition(index)
404 }
405
406 fn dragenter(index: usize) -> WindowEditorMsg {
407 WindowEditorMsg::DragEnter(DragTarget::WindowPartitionBy, index)
408 }
409
410 fn dragleave() -> WindowEditorMsg {
411 WindowEditorMsg::DragLeave(DragTarget::WindowPartitionBy)
412 }
413
414 fn create(col: InPlaceColumn) -> WindowEditorMsg {
415 WindowEditorMsg::New(DragTarget::WindowPartitionBy, col)
416 }
417
418 fn is_self_move(effect: DragTarget) -> bool {
419 effect == DragTarget::WindowPartitionBy
420 }
421}
422
423#[derive(Clone, Properties)]
430pub struct WindowSlotColumnProps {
431 pub column: String,
432 pub column_type: Option<ColumnType>,
433
434 pub action: DragTarget,
436
437 pub presentation: Presentation,
438
439 #[prop_or_default]
440 pub aggregate: Option<Html>,
441
442 #[prop_or_default]
445 pub trailing: Html,
446}
447
448impl PartialEq for WindowSlotColumnProps {
449 fn eq(&self, other: &Self) -> bool {
450 self.column == other.column
451 && self.column_type == other.column_type
452 && self.action == other.action
453 && self.aggregate == other.aggregate
454 && self.trailing == other.trailing
455 }
456}
457
458impl DragDropListItemProps for WindowSlotColumnProps {
459 type Item = String;
460
461 fn get_item(&self) -> String {
462 self.column.clone()
463 }
464}
465
466pub struct WindowSlotColumn;
467
468impl Component for WindowSlotColumn {
469 type Message = ();
470 type Properties = WindowSlotColumnProps;
471
472 fn create(_ctx: &Context<Self>) -> Self {
473 Self
474 }
475
476 fn view(&self, ctx: &Context<Self>) -> Html {
477 let dragstart = Callback::from({
478 let column = ctx.props().column.clone();
479 let presentation = ctx.props().presentation.clone();
480 let action = ctx.props().action;
481 move |event: DragEvent| {
482 presentation.set_drag_image(&event).unwrap();
483 presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
484 }
485 });
486
487 let dragend = Callback::from({
488 let presentation = ctx.props().presentation.clone();
489 move |_: DragEvent| presentation.notify_drag_end()
490 });
491
492 html! {
493 <div class="column-selector-column">
494 <ColumnSelectorColumnRow
495 name={ctx.props().column.clone()}
496 col_type={ctx.props().column_type}
497 aggregate={ctx.props().aggregate.clone()}
498 trailing={ctx.props().trailing.clone()}
499 wrapper_class={classes!["column-selector-column-title"]}
500 ondragstart={Some(dragstart)}
501 ondragend={Some(dragend)}
502 />
503 </div>
504 }
505 }
506}
507
508pub struct WindowEditor {
509 draft: WindowDraft,
510 error: Option<String>,
511 column_dropdown: ColumnDropDownElement,
512 _drop_sub: Subscription,
513}
514
515impl WindowEditor {
516 fn initialize(ctx: &Context<Self>) -> WindowDraft {
517 ctx.props()
518 .initial
519 .as_ref()
520 .map(WindowDraft::from_spec)
521 .unwrap_or_else(WindowDraft::new_default)
522 }
523
524 fn emit(&mut self, ctx: &Context<Self>) {
525 match self.draft.validate(&ctx.props().metadata) {
526 Ok(spec) => {
527 self.error = None;
528 ctx.props().on_change.emit(Some(spec));
529 },
530 Err(msg) => {
531 self.error = Some(msg);
532 ctx.props().on_change.emit(None);
533 },
534 }
535 }
536}
537
538impl Component for WindowEditor {
539 type Message = WindowEditorMsg;
540 type Properties = WindowEditorProps;
541
542 fn create(ctx: &Context<Self>) -> Self {
543 let _drop_sub = {
544 let link = ctx.link().clone();
545 ctx.props().presentation.drop_received.add_listener(
546 move |(column, target, effect, _index): (String, DragTarget, DragEffect, usize)| {
547 let staged_origin = match effect {
548 DragEffect::Move(origin) if origin.is_staged() => Some(origin),
549 _ => None,
550 };
551
552 if target.is_staged() || staged_origin.is_some() {
553 link.send_message(WindowEditorMsg::Drop(column, target, staged_origin));
554 }
555 },
556 )
557 };
558
559 let mut this = Self {
560 draft: Self::initialize(ctx),
561 error: None,
562 column_dropdown: ColumnDropDownElement::new(ctx.props().session.clone()),
563 _drop_sub,
564 };
565
566 this.emit(ctx);
567 this
568 }
569
570 fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
571 if ctx.props().reset_count != old_props.reset_count
572 || ctx.props().initial != old_props.initial
573 {
574 self.draft = Self::initialize(ctx);
575 self.emit(ctx);
576 true
577 } else {
578 false
579 }
580 }
581
582 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
583 match msg {
584 WindowEditorMsg::DragEnter(target, index) => {
585 return ctx.props().presentation.notify_drag_enter(target, index);
586 },
587 WindowEditorMsg::DragLeave(target) => {
588 ctx.props().presentation.notify_drag_leave(target);
589 return true;
590 },
591 WindowEditorMsg::New(target, InPlaceColumn::Column(column)) => {
592 ctx.link()
593 .send_message(WindowEditorMsg::Drop(column, target, None));
594 return false;
595 },
596 WindowEditorMsg::New(_, InPlaceColumn::Expression(_)) => {
597 return false;
600 },
601 WindowEditorMsg::Drop(column, target, origin) => {
602 match origin {
605 Some(DragTarget::WindowSource) => self.draft.source = String::default(),
606 Some(DragTarget::WindowOrderBy) => self.draft.order_by = String::default(),
607 Some(DragTarget::WindowPartitionBy) => {
608 self.draft.partition_by.retain(|x| x != &column)
609 },
610 _ => {},
611 }
612
613 let is_table_column = ctx
617 .props()
618 .metadata
619 .get_table_columns()
620 .into_iter()
621 .flatten()
622 .any(|x| *x == column);
623
624 match target {
625 _ if target.is_staged() && !is_table_column => {},
626 DragTarget::WindowSource => {
627 self.draft.source = column;
628
629 let available = ctx
633 .props()
634 .metadata
635 .get_column_table_type(&self.draft.source)
636 .and_then(|ty| {
637 ctx.props()
638 .metadata
639 .get_features()
640 .map(|x| x.get_window_aggregates(ty))
641 })
642 .unwrap_or_default();
643
644 if !available.iter().any(|x| x.name == self.draft.op)
645 && let Some(first) = available.first()
646 {
647 self.draft.op = first.name.clone();
648 }
649 },
650 DragTarget::WindowOrderBy => self.draft.order_by = column,
651 DragTarget::WindowPartitionBy
652 if !column.is_empty() && !self.draft.partition_by.contains(&column) =>
653 {
654 self.draft.partition_by.push(column);
655 },
656 _ => {},
659 }
660 },
661 WindowEditorMsg::SetOp(op) => {
662 self.draft.op = op;
663
664 if let Some(declared) =
668 op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op)
669 && !declared.frames.is_empty()
670 && !declared
671 .frames
672 .iter()
673 .any(|x| x == frame_name(&self.draft.frame_type))
674 && let Some(first) = declared.frames.first()
675 {
676 self.draft.frame_type = frame_label(first).to_string();
677 }
678 },
679 WindowEditorMsg::ClearSource => self.draft.source = String::default(),
680 WindowEditorMsg::ClearOrderBy => self.draft.order_by = String::default(),
681 WindowEditorMsg::ToggleOrderDir => self.draft.order_desc = !self.draft.order_desc,
682 WindowEditorMsg::RemovePartition(idx) => {
683 if idx < self.draft.partition_by.len() {
684 self.draft.partition_by.remove(idx);
685 }
686 },
687 WindowEditorMsg::SetFrameType(x) => self.draft.frame_type = x,
688 WindowEditorMsg::SetFrameRows(x) => {
689 self.draft.frame_rows = if x.is_finite() && x >= 0.0 {
690 x as u32
691 } else {
692 1
693 };
694 },
695 WindowEditorMsg::SetFrameRange(x) => {
696 self.draft.frame_range = if x.is_finite() && x > 0.0 { x } else { 1.0 };
697 },
698 WindowEditorMsg::SetOffset(x) => {
699 self.draft.offset = if x.is_finite() && x >= 0.0 {
700 x as u32
701 } else {
702 1
703 };
704 },
705 WindowEditorMsg::SetAlpha(x) => {
706 self.draft.alpha = if x.is_finite() && x > 0.0 && x <= 1.0 {
707 x
708 } else {
709 1.0
710 };
711 },
712 }
713
714 self.emit(ctx);
715 true
716 }
717
718 fn view(&self, ctx: &Context<Self>) -> Html {
719 let declared = op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op);
720
721 let ops: Rc<Vec<SelectItem<String>>> = Rc::new(
726 ctx.props()
727 .metadata
728 .get_column_table_type(&self.draft.source)
729 .and_then(|ty| {
730 ctx.props()
731 .metadata
732 .get_features()
733 .map(|x| x.get_window_aggregates(ty))
734 })
735 .unwrap_or_default()
736 .into_iter()
737 .map(|x| SelectItem::Option(x.name.clone()))
738 .collect(),
739 );
740
741 let on_number_input = |f: fn(f64) -> WindowEditorMsg| {
745 ctx.link().callback(move |event: InputEvent| {
746 let value = event
747 .target()
748 .and_then(|t| t.dyn_into::<HtmlInputElement>().ok())
749 .map(|x| x.value_as_number())
750 .unwrap_or(f64::NAN);
751 f(value)
752 })
753 };
754
755 let frame_types: Rc<Vec<SelectItem<String>>> = Rc::new(
759 declared
760 .as_ref()
761 .map(|x| x.frames.clone())
762 .unwrap_or_default()
763 .iter()
764 .map(|x| SelectItem::Option(frame_label(x).to_string()))
765 .collect(),
766 );
767
768 let non_table: HashSet<String> = ctx
774 .props()
775 .metadata
776 .get_expression_columns()
777 .chain(ctx.props().metadata.get_window_columns())
778 .cloned()
779 .collect();
780
781 let is_invalid_drag = |target: DragTarget| {
782 ctx.props()
783 .presentation
784 .is_dragover(target)
785 .map(|(_, col)| non_table.contains(&col))
786 .unwrap_or_default()
787 };
788
789 let source_exclude: HashSet<String> = std::iter::once(self.draft.source.clone())
792 .chain(non_table.iter().cloned())
793 .collect();
794
795 let source_list = html! {
796 <DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>
797 name="window-source"
798 parent={ctx.link().clone()}
799 presentation={ctx.props().presentation.clone()}
800 column_dropdown={self.column_dropdown.clone()}
801 exclude={source_exclude}
802 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowSource)}
803 is_invalid={is_invalid_drag(DragTarget::WindowSource)}
804 single_slot=true
805 >
806 { for (!self.draft.source.is_empty()).then(|| yew::html_nested! {
807 <WindowSlotColumn
808 column={self.draft.source.clone()}
809 column_type={ctx.props().metadata.get_column_table_type(&self.draft.source)}
810 action={DragTarget::WindowSource}
811 presentation={ctx.props().presentation.clone()}
812 aggregate={html! {
813 <div class="aggregate-selector-wrapper">
814 <Select<String>
815 wrapper_class="aggregate-selector"
816 values={ops.clone()}
817 selected={self.draft.op.clone()}
818 on_select={ctx.link().callback(WindowEditorMsg::SetOp)}
819 />
820 </div>
821 }}
822 />
823 }) }
824 </DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>>
825 };
826
827 let order_exclude: HashSet<String> = std::iter::once(self.draft.order_by.clone())
828 .chain(non_table.iter().cloned())
829 .collect();
830 let order_list = html! {
831 <DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>
832 name="window-order-by"
833 parent={ctx.link().clone()}
834 presentation={ctx.props().presentation.clone()}
835 column_dropdown={self.column_dropdown.clone()}
836 exclude={order_exclude}
837 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowOrderBy)}
838 is_invalid={is_invalid_drag(DragTarget::WindowOrderBy)}
839 single_slot=true
840 >
841 { for (!self.draft.order_by.is_empty()).then(|| {
842 let dir = if self.draft.order_desc {
845 WindowSortDir::Desc
846 } else {
847 WindowSortDir::Asc
848 };
849
850 let onmousedown = ctx
851 .link()
852 .callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir);
853
854 yew::html_nested! {
855 <WindowSlotColumn
856 column={self.draft.order_by.clone()}
857 column_type={ctx.props().metadata.get_column_table_type(&self.draft.order_by)}
858 action={DragTarget::WindowOrderBy}
859 presentation={ctx.props().presentation.clone()}
860 trailing={html! {
861 <span
862 class={format!("sort-icon {}", dir)}
863 {onmousedown}
864 />
865 }}
866 />
867 }
868 }) }
869 </DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>>
870 };
871
872 let partition_exclude: HashSet<String> = self
873 .draft
874 .partition_by
875 .iter()
876 .cloned()
877 .chain(non_table.iter().cloned())
878 .collect();
879 let partition_list = html! {
880 <DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>
881 name="window-partition-by"
882 parent={ctx.link().clone()}
883 presentation={ctx.props().presentation.clone()}
884 column_dropdown={self.column_dropdown.clone()}
885 exclude={partition_exclude}
886 is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowPartitionBy)}
887 is_invalid={is_invalid_drag(DragTarget::WindowPartitionBy)}
888 >
889 { for self.draft.partition_by.iter().map(|col| yew::html_nested! {
890 <PivotColumn
891 column={col.clone()}
892 column_type={ctx.props().metadata.get_column_table_type(col)}
893 action={DragTarget::WindowPartitionBy}
894 presentation={ctx.props().presentation.clone()}
895 />
896 }) }
897 </DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>>
898 };
899
900 let show_frame = declared
901 .as_ref()
902 .map(|x| !x.frames.is_empty())
903 .unwrap_or_default();
904 let show_offset = declared.as_ref().map(|x| x.offset).unwrap_or_default();
905 let show_alpha = declared.as_ref().map(|x| x.alpha).unwrap_or_default();
906
907 html! {
908 <>
909 <div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
910 <div id="window-editor-container">
911 if show_frame {
912 <div class="column-style-label"><label id="window-frame-label" /></div>
913 <div class="row">
914 <Select<String>
915 id="window-frame-type"
916 values={frame_types}
917 selected={self.draft.frame_type.clone()}
918 on_select={ctx.link().callback(WindowEditorMsg::SetFrameType)}
919 />
920 </div>
921 if self.draft.frame_type == "Rows" {
922 <div class="row">
923 <input
924 type="number"
925 class="parameter"
926 min="0"
927 value={self.draft.frame_rows.to_string()}
928 oninput={on_number_input(WindowEditorMsg::SetFrameRows)}
929 />
930 </div>
931 }
932 if self.draft.frame_type == "Range" {
933 <div class="row">
934 <input
935 type="number"
936 class="parameter"
937 min="0"
938 value={self.draft.frame_range.to_string()}
939 oninput={on_number_input(WindowEditorMsg::SetFrameRange)}
940 />
941 </div>
942 }
943 }
944 if show_offset {
945 <div class="column-style-label">
946 <label id="window-offset-label" class="indent" />
947 </div>
948 <div class="row">
949 <input
950 type="number"
951 class="parameter"
952 min="0"
953 value={self.draft.offset.to_string()}
954 oninput={on_number_input(WindowEditorMsg::SetOffset)}
955 />
956 </div>
957 }
958 if show_alpha {
959 <div class="column-style-label">
960 <label id="window-alpha-label" class="indent" />
961 </div>
962 <div class="row">
963 <input
964 type="number"
965 class="parameter"
966 step="0.05"
967 min="0"
968 max="1"
969 value={self.draft.alpha.to_string()}
970 oninput={on_number_input(WindowEditorMsg::SetAlpha)}
971 />
972 </div>
973 }
974 if let Some(error) = &self.error {
975 <div class="row window-editor-error">{ error.clone() }</div>
976 }
977 </div>
978 <ColumnDropDownPortal
979 element={self.column_dropdown.clone()}
980 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
981 />
982 </>
983 }
984 }
985}