//! The rule dialogs: validation, conditional format, the format list. use ely_gpui_component::{ buttons::{Button, ButtonVariant}, data_display::DescriptionList, forms::{Choice, Input, Select, Switch, TextInput}, overlays::Dialog as Card, }; use ely_lok::sheet::{Format, Validation}; use gpui::{App, Entity, ParentElement as _, SharedString, Styled as _, Window, div}; use super::Fields; use crate::book::Book; use crate::dialog::{Dialog, edit}; /// What a cell may hold. const KINDS: [(&str, &str); 8] = [ ("any", "table.rules.any"), ("whole", "table.rules.whole"), ("decimal", "table.rules.decimal"), ("date", "table.rules.date"), ("time", "table.rules.time"), ("textlength", "table.rules.textlength"), ("list", "table.rules.list"), ("custom", "table.rules.custom"), ]; const OPERATORS: [(&str, &str); 8] = [ ("between", "table.rules.between"), ("notbetween", "table.rules.notbetween"), ("equal", "table.rules.equal"), ("notequal", "table.rules.notequal"), ("greater", "table.rules.greater"), ("less", "table.rules.less"), ("greaterequal", "table.rules.greaterequal"), ("lessequal", "table.rules.lessequal"), ]; const ALERTS: [(&str, &str); 3] = [ ("stop", "table.rules.stop"), ("warning", "table.rules.warning"), ("info", "table.rules.info"), ]; /// Conditions a cell rule tests; more than validation has. const CONDITIONS: [(&str, &str); 18] = [ ("greater", "table.rules.greater"), ("less", "table.rules.less"), ("between", "table.rules.between"), ("notbetween", "table.rules.notbetween"), ("equal", "table.rules.equal"), ("notequal", "table.rules.notequal"), ("greaterequal", "table.rules.greaterequal"), ("lessequal", "table.rules.lessequal"), ("duplicate", "table.rules.duplicate"), ("unique", "table.rules.unique"), ("contains", "table.rules.contains"), ("beginswith", "table.rules.beginswith"), ("endswith", "table.rules.endswith"), ("top10", "table.rules.top"), ("bottom10", "table.rules.bottom"), ("aboveaverage", "table.rules.above_average"), ("belowaverage", "table.rules.below_average"), ("error", "table.rules.error"), ]; /// The kernel's cell styles a rule can apply. const STYLES: [(&str, &str); 10] = [ ("Good", "table.style.good"), ("Neutral", "table.style.neutral"), ("Bad", "table.style.bad"), ("Warning", "table.style.warning"), ("Error", "table.style.error"), ("Accent", "table.style.accent"), ("Accent 1", "table.style.accent1"), ("Accent 2", "table.style.accent2"), ("Accent 3", "table.style.accent3"), ("Note", "table.style.note"), ]; const FORMATS: [(&str, &str); 3] = [ ("cell", "table.rules.cell"), ("scale", "table.rules.scale"), ("databar", "table.rules.databar"), ]; /// Color scales: label, low to high. pub(super) const SCALES: [(&str, &[&str]); 4] = [ ("table.rules.scale_rgy", &["#F8696B", "#FFEB84", "#63BE7B"]), ("table.rules.scale_gyr", &["#63BE7B", "#FFEB84", "#F8696B"]), ("table.rules.scale_wr", &["#FFFFFF", "#F8696B"]), ("table.rules.scale_wg", &["#FFFFFF", "#63BE7B"]), ]; /// Data bar colors. pub(super) const BARS: [(&str, &str); 4] = [ ("table.rules.bar_blue", "#638EC6"), ("table.rules.bar_green", "#63C384"), ("table.rules.bar_red", "#FF555A"), ("table.rules.bar_orange", "#FFB628"), ]; /// The table's value for a picked choice. fn find(table: &[(&'static str, &'static str)], value: &str) -> &'static str { table .iter() .find(|(key, _)| *key == value) .map(|(key, _)| *key) .expect("a listed choice") } /// A select over `table`; `set` takes the pick. fn pick( this: &Entity, id: &'static str, (table, selected): (&[(&'static str, &'static str)], &str), set: fn(&mut Dialog, &'static str), cx: &App, ) -> Select { let choices = table .iter() .map(|(value, key)| Choice::new(*value, Book::text(cx, key, &[]))); let (this, table) = (this.clone(), table.to_vec()); Select::new(id, choices) .selected(selected.to_string()) .on_change(move |value, _, cx| { let value = find(&table, value); edit(&this, cx, |dialog, _, _| set(dialog, value)) }) } /// A switch; `set` takes its state. fn switch( this: &Entity, id: &'static str, on: bool, set: fn(&mut Validation, bool), ) -> Switch { let this = this.clone(); Switch::new(id, on).on_change(move |on, _, cx| { edit(&this, cx, |dialog, _, _| { if let Dialog::Validation { rule, .. } = dialog { set(rule, on); } }) }) } fn field(input: &Entity) -> gpui::Div { div().w_full().max_w_64().child(Input::new(input)) } fn rule_of(dialog: &mut Dialog) -> &mut Validation { match dialog { Dialog::Validation { rule, .. } => rule, _ => unreachable!("a validation dialog"), } } pub fn validation_dialog( this: Entity, (rule, fields): (&Validation, &Fields), dismiss: impl Fn(&mut Window, &mut App) + 'static, cx: &App, ) -> Card { let label = |key: &str| Book::text(cx, key, &[]); let mut rows = DescriptionList::new().item( label("table.rules.allow"), pick( &this, "table-rule-kind", (&KINDS, &rule.kind), |dialog, kind| rule_of(dialog).kind = kind.into(), cx, ), ); match rule.kind.as_str() { "whole" | "decimal" | "date" | "time" | "textlength" => { rows = rows .item( label("table.rules.data"), pick( &this, "table-rule-operator", (&OPERATORS, &rule.operator), |dialog, op| rule_of(dialog).operator = op.into(), cx, ), ) .item(label("table.rules.value"), field(&fields.value1)); if matches!(rule.operator.as_str(), "between" | "notbetween") { rows = rows.item(label("table.rules.and"), field(&fields.value2)); } } "list" => { rows = rows .item(label("table.rules.entries"), field(&fields.list)) .item( label("table.rules.dropdown"), switch(&this, "table-rule-dropdown", rule.dropdown, |rule, on| { rule.dropdown = on }), ); } "custom" => rows = rows.item(label("table.rules.formula"), field(&fields.value1)), _ => {} } if rule.kind != "any" { rows = rows.item( label("table.rules.blank"), switch(&this, "table-rule-blank", rule.ignore_blank, |rule, on| { rule.ignore_blank = on }), ); } rows = rows.item( label("table.rules.help"), switch(&this, "table-rule-help", rule.show_input, |rule, on| { rule.show_input = on }), ); if rule.show_input { rows = rows .item(label("table.rules.title"), field(&fields.input_title)) .item(label("table.rules.message"), field(&fields.input_message)); } if rule.kind != "any" { rows = rows.item( label("table.rules.alert"), switch(&this, "table-rule-alert", rule.show_error, |rule, on| { rule.show_error = on }), ); if rule.show_error { rows = rows .item( label("table.rules.style"), pick( &this, "table-rule-style", (&ALERTS, &rule.error_style), |dialog, style| rule_of(dialog).error_style = style.into(), cx, ), ) .item(label("table.rules.title"), field(&fields.error_title)) .item(label("table.rules.message"), field(&fields.error_message)); } } let go = label("table.rules.apply"); Card::new("table-rule", label("table.rules.validation_title"), dismiss) .child(rows) .action(ely_office::cancel(cx)) .action(move |_| { Button::new("table-rule-go", go) .variant(ButtonVariant::Primary) .on_click(move |_, _, cx| this.update(cx, |book, cx| book.rule_asked(cx))) }) } /// The open conditional dialog's parts. pub struct Shown<'a> { pub kind: &'a str, pub operator: &'a str, pub style: &'a str, pub preset: usize, pub value1: &'a Entity, pub value2: &'a Entity, } fn set_conditional( dialog: &mut Dialog, change: impl FnOnce(&mut &'static str, &mut &'static str, &mut &'static str, &mut usize), ) { if let Dialog::Conditional { kind, operator, style, preset, .. } = dialog { change(kind, operator, style, preset); } } pub fn conditional_dialog( this: Entity, shown: Shown, dismiss: impl Fn(&mut Window, &mut App) + 'static, cx: &App, ) -> Card { let label = |key: &str| Book::text(cx, key, &[]); let mut rows = DescriptionList::new().item( label("table.rules.kind"), pick( &this, "table-format-kind", (&FORMATS, shown.kind), |dialog, value| { set_conditional(dialog, |kind, _, _, preset| { *kind = value; *preset = 0; }) }, cx, ), ); match shown.kind { "scale" | "databar" => { let table: Vec<(SharedString, SharedString)> = if shown.kind == "scale" { SCALES .iter() .enumerate() .map(|(at, (key, _))| (at.to_string().into(), label(key))) .collect() } else { BARS.iter() .enumerate() .map(|(at, (key, _))| (at.to_string().into(), label(key))) .collect() }; let picked = this.clone(); let presets = Select::new( "table-format-preset", table .into_iter() .map(|(value, text)| Choice::new(value, text)), ) .selected(shown.preset.to_string()) .on_change(move |value, _, cx| { let at: usize = value.parse().expect("presets are indexes"); edit(&picked, cx, |dialog, _, _| { set_conditional(dialog, |_, _, _, preset| *preset = at) }) }); rows = rows.item(label("table.rules.colors"), presets); } _ => { rows = rows.item( label("table.rules.condition"), pick( &this, "table-format-operator", (&CONDITIONS, shown.operator), |dialog, value| set_conditional(dialog, |_, operator, _, _| *operator = value), cx, ), ); match shown.operator { "duplicate" | "unique" | "aboveaverage" | "belowaverage" | "error" => {} "between" | "notbetween" => { rows = rows .item(label("table.rules.value"), field(shown.value1)) .item(label("table.rules.and"), field(shown.value2)); } _ => rows = rows.item(label("table.rules.value"), field(shown.value1)), } rows = rows.item( label("table.rules.cell_style"), pick( &this, "table-format-style", (&STYLES, shown.style), |dialog, value| set_conditional(dialog, |_, _, style, _| *style = value), cx, ), ); } } let go = label("table.rules.add"); Card::new( "table-format", label("table.rules.conditional_title"), dismiss, ) .child(rows) .action(ely_office::cancel(cx)) .action(move |_| { Button::new("table-format-go", go) .variant(ButtonVariant::Primary) .on_click(move |_, _, cx| this.update(cx, |book, cx| book.conditional_asked(cx))) }) } /// One format as the list shows it. fn entry(format: &Format, cx: &App) -> SharedString { let kind = FORMATS .iter() .find(|(value, _)| *value == format.kind) .map_or_else( || format.kind.clone().into(), |(_, key)| Book::text(cx, key, &[]), ); let style = STYLES .iter() .find(|(value, _)| *value == format.style) .map_or_else( || format.style.clone().into(), |(_, key)| Book::text(cx, key, &[]), ); let args = [ ("range", format.range.as_str()), ("kind", kind.as_ref()), ("style", style.as_ref()), ("value", format.value.as_str()), ]; match format.kind.as_str() { "cell" => Book::text(cx, "table.rules.entry_cell", &args), _ => Book::text(cx, "table.rules.entry", &args), } } pub fn formats_dialog( this: Entity, (formats, chosen): (&[Format], usize), dismiss: impl Fn(&mut Window, &mut App) + 'static, cx: &App, ) -> Card { let choices = formats .iter() .enumerate() .map(|(at, format)| Choice::new(at.to_string(), entry(format, cx))); let picked = this.clone(); let list = Select::new("table-formats-pick", choices) .selected(chosen.to_string()) .on_change(move |value, _, cx| { let at: usize = value.parse().expect("formats are listed by index"); edit(&picked, cx, |dialog, _, _| { if let Dialog::Formats { chosen, .. } = dialog { *chosen = at; } }) }); let go = Book::text(cx, "table.rules.remove", &[]); Card::new( "table-formats", Book::text(cx, "table.rules.formats_title", &[]), dismiss, ) .child(list) .action(ely_office::cancel(cx)) .action(move |_| { Button::new("table-formats-remove", go) .variant(ButtonVariant::Danger) .on_click(move |_, _, cx| this.update(cx, |book, cx| book.format_removed(cx))) }) }