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