diff --git a/api/sixtyfps-cpp/cbindgen.rs b/api/sixtyfps-cpp/cbindgen.rs index fafda11fe..bb2b77781 100644 --- a/api/sixtyfps-cpp/cbindgen.rs +++ b/api/sixtyfps-cpp/cbindgen.rs @@ -116,6 +116,7 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> { "ImageFit", "FillRule", "StandardButtonKind", + "DialogButtonRole", "PointerEventKind", "PointerEventButton", "PointerEvent", diff --git a/docs/builtin_elements.md b/docs/builtin_elements.md index 412f0e797..102539bab 100644 --- a/docs/builtin_elements.md +++ b/docs/builtin_elements.md @@ -17,6 +17,7 @@ These properties are valid on all visible items children with transparency. 0 is fully transparent (invisible), and 1 is fully opaque. (default: 1) * **`visible`** (*bool*): When set to `false`, the element and all his children will not be drawn and not react to mouse input (default: `true`) +* **`dialog-button-role`** (*enum DialogButtonRole*): Specify that this is a button in a `Dialog`. ### Drop Shadows @@ -656,13 +657,15 @@ Example := Window { Dialog is like a window, but it has buttons that are automatically laid out. A Dialog should have one main element for the content, that is not a button. -And the window can have any number of `StandardButton` widgets. +And the window can have any number of `StandardButton` widgets or other button +with the `dialog-button-role` property. The button will be layed out in an order that depends on the platform. -The `kind` property of the `StandardButton`s needs to be set to a specific value. It cannot be a complex expression, -and there cannot be several button of the same kind. +The `kind` property of the `StandardButton`s and the ``dialog-button-role` properties needs to be set to a specific value, +it cannot be a complex expression. +There cannot be several StandardButton of the same kind. -If A callback `_clicked` is automatically added for each button which does not have an explicit +If A callback `_clicked` is automatically added for each StandardButton which does not have an explicit callback handler, so it can be handled from the native code. (e.g. if there is a button of kind `cancel`, a `cancel_clicked` callback will be added) When viewed with the `sixtyfps-viewer` program, the `ok`, `cancel`, and `close` button will cause the dialog to close. @@ -675,13 +678,17 @@ When viewed with the `sixtyfps-viewer` program, the `ok`, `cancel`, and `close` ### Example ```60 -import { StandardButton } from "sixtyfps_widgets.60"; +import { StandardButton, Button } from "sixtyfps_widgets.60"; Example := Dialog { Text { text: "This is a dialog box"; } StandardButton { kind: ok; } StandardButton { kind: cancel; } + Button { + text: "More Info"; + dialog-button-role: action; + } } ``` @@ -789,3 +796,19 @@ This enum describes the different ways of deciding what the inside of a shape de * **`FillRule.nonzero`**: The ["nonzero" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#nonzero). * **`FillRule.evenodd`**: The ["evenodd" fill rule as defined in SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#evenodd). + +## `DialogButtonRole` + +This enum represent the value of the `dialog-button-role` property which can be added to +any element within a `Dialog` to put that item in the button row, and its exact position +depends on the role and the platform. + +### Values + +* **`none`**: This is not a button means to go in the row of button of the dialog +* **`accept`**: This is the role of the main button to click to accept the dialog. e.g. "Ok" or "Yes" +* **`reject`**: This is the role of the main button to click to reject the dialog. e.g. "Cancel" or "No" +* **`apply`**: This is the role of the "Apply" button +* **`reset`**: This is the role of the "Reset" button +* **`help`**: This is the role of the "Help" button +* **`action`**: This is the role of any other button that perform another action. \ No newline at end of file diff --git a/sixtyfps_compiler/layout.rs b/sixtyfps_compiler/layout.rs index cb1f5965d..a88bae61e 100644 --- a/sixtyfps_compiler/layout.rs +++ b/sixtyfps_compiler/layout.rs @@ -396,7 +396,7 @@ pub struct GridLayout { /// When this GridLyout is actually the layout of a Dialog, then the cells start with all the buttons, /// and this variable contains their roles. The string is actually one of the values from the sixtyfps_corelib::layout::DialogButtonRole - pub dialog_button_roles: Option>, + pub dialog_button_roles: Option>, } impl GridLayout { diff --git a/sixtyfps_compiler/passes/lower_layout.rs b/sixtyfps_compiler/passes/lower_layout.rs index 3d8d77d2d..2a59b95c2 100644 --- a/sixtyfps_compiler/passes/lower_layout.rs +++ b/sixtyfps_compiler/passes/lower_layout.rs @@ -405,19 +405,24 @@ fn lower_dialog_layout( let mut seen_buttons = HashSet::new(); let layout_children = std::mem::take(&mut dialog_element.borrow_mut().children); for layout_child in &layout_children { - let is_button = layout_child.borrow().property_declarations.get("kind").map_or(false, |pd| { + let dialog_button_role_binding = + layout_child.borrow_mut().bindings.remove("dialog-button-role"); + let is_button = if let Some(role_binding) = dialog_button_role_binding { + if let Expression::EnumerationValue(val) = &role_binding.expression { + let en = &val.enumeration; + debug_assert_eq!(en.name, "DialogButtonRole"); + button_roles.push(en.values[val.value].clone()); + if val.value == 0 { + diag.push_error("The `dialog-button-role` cannot be set explicitly to none".into(), &role_binding); + } + } else { + diag.push_error("The `dialog-button-role` property must be known at compile-time".into(), &role_binding); + } + true + } else if layout_child.borrow().property_declarations.get("kind").map_or(false, |pd| { matches!(&pd.property_type, Type::Enumeration(e) if e.name == "StandardButtonKind") - }); - - if is_button { - grid.add_element_with_coord( - layout_child, - (1, button_roles.len() as u16), - (1, 1), - &layout_cache_prop_h, - &layout_cache_prop_v, - diag, - ); + }) { + // layout_child is a StandardButton match layout_child.borrow().bindings.get("kind") { None => diag.push_error( "The `kind` property of the StandardButton in a Dialog must be set".into(), @@ -429,20 +434,20 @@ fn lower_dialog_layout( debug_assert_eq!(en.name, "StandardButtonKind"); let kind = &en.values[val.value]; let role = match kind.as_str() { - "ok" => "Accept", - "cancel" => "Reject", - "apply" => "Apply", - "close" => "Reject", - "reset" => "Reset", - "help" => "Help", - "yes" => "Accept", - "no" => "Reject", - "abort" => "Reject", - "retry" => "Accept", - "ignore" => "Accept", + "ok" => "accept", + "cancel" => "reject", + "apply" => "apply", + "close" => "reject", + "reset" => "reset", + "help" => "help", + "yes" => "accept", + "no" => "reject", + "abort" => "reject", + "retry" => "accept", + "ignore" => "accept", _ => unreachable!(), }; - button_roles.push(role); + button_roles.push(role.into()); if !seen_buttons.insert(val.value) { diag.push_error("Duplicated `kind`: There are two StandardButton in this Dialog with the same kind".into(), binding); } else if Rc::ptr_eq( @@ -487,6 +492,20 @@ fn lower_dialog_layout( } } } + true + } else { + false + }; + + if is_button { + grid.add_element_with_coord( + layout_child, + (1, button_roles.len() as u16), + (1, 1), + &layout_cache_prop_h, + &layout_cache_prop_v, + diag, + ); } else if main_widget.is_some() { diag.push_error( "A Dialog can have only one child element that is not a StandardButton".into(), @@ -755,5 +774,8 @@ fn check_no_layout_properties(item: &ElementRc, diag: &mut BuildDiagnostics) { if matches!(prop.as_ref(), "col" | "row" | "colspan" | "rowspan") { diag.push_error(format!("{} used outside of a GridLayout", prop), expr); } + if matches!(prop.as_ref(), "dialog-button-role") { + diag.push_error(format!("{} used outside of a Dialog", prop), expr); + } } } diff --git a/sixtyfps_compiler/tests/syntax/basic/dialog.60 b/sixtyfps_compiler/tests/syntax/basic/dialog.60 index 9c7f39d83..e71de925e 100644 --- a/sixtyfps_compiler/tests/syntax/basic/dialog.60 +++ b/sixtyfps_compiler/tests/syntax/basic/dialog.60 @@ -40,12 +40,25 @@ MyDiag3 := Dialog { } +MyDialog4 := Dialog { + StandardButton { kind: ok; } + Rectangle { dialog-button-role: accept; } + Rectangle { dialog-button-role: none; } +// ^error{The `dialog-button-role` cannot be set explicitly to none} + Rectangle { dialog-button-role: true ? accept : reject; } +// ^error{The `dialog-button-role` property must be known at compile-time} + Rectangle { + Rectangle { dialog-button-role: accept; } +// ^error{dialog-button-role used outside of a Dialog} + } +} Test := Rectangle { MyDiag1 {} MyDiag2 {} // FIXME: not the best place for the error // ^error{A Dialog must have a single child element that is not StandardButton} MyDiag3 {} + MyDialog4 {} } diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index 6d450c18d..7aff84436 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -43,6 +43,24 @@ const RESERVED_LAYOUT_PROPERTIES: &[(&str, Type)] = &[ ("rowspan", Type::Int32), ]; +thread_local! { + pub static DIALOG_BUTTON_ROLE_ENUM: Type = + Type::Enumeration(Rc::new(Enumeration { + name: "DialogButtonRole".into(), + values: IntoIterator::into_iter([ + "none".to_owned(), + "accept".to_owned(), + "reject".to_owned(), + "apply".to_owned(), + "reset".to_owned(), + "action".to_owned(), + "help".to_owned(), + ]) + .collect(), + default_value: 0, + })); +} + const RESERVED_OTHER_PROPERTIES: &[(&str, Type)] = &[ ("clip", Type::Bool), ("opacity", Type::Float32), @@ -67,6 +85,7 @@ pub fn reserved_properties() -> impl Iterator { .chain(std::array::IntoIter::new([ ("forward-focus", Type::ElementReference), ("focus", BuiltinFunction::SetFocusItem.ty()), + ("dialog-button-role", DIALOG_BUTTON_ROLE_ENUM.with(|e| e.clone())), ])) } @@ -183,6 +202,7 @@ impl TypeRegister { ); declare_enum("PointerEventKind", &["cancel", "down", "up"]); declare_enum("PointerEventButton", &["none", "left", "right", "middle"]); + register.insert_type(DIALOG_BUTTON_ROLE_ENUM.with(|x| x.clone())); register.supported_property_animation_types.insert(Type::Float32.to_string()); register.supported_property_animation_types.insert(Type::Int32.to_string()); diff --git a/sixtyfps_runtime/corelib/items.rs b/sixtyfps_runtime/corelib/items.rs index 16f59223e..9059f5441 100644 --- a/sixtyfps_runtime/corelib/items.rs +++ b/sixtyfps_runtime/corelib/items.rs @@ -1297,6 +1297,27 @@ impl Default for StandardButtonKind { } } +#[derive( + Copy, Clone, Eq, PartialEq, Hash, Debug, strum_macros::EnumString, strum_macros::ToString, +)] +#[repr(C)] +#[allow(non_camel_case_types)] +pub enum DialogButtonRole { + none, + accept, + reject, + apply, + reset, + action, + help, +} + +impl Default for DialogButtonRole { + fn default() -> Self { + Self::none + } +} + #[derive(Copy, Clone, Debug, PartialEq, strum_macros::EnumString, strum_macros::Display)] #[repr(C)] #[allow(non_camel_case_types)] diff --git a/sixtyfps_runtime/corelib/layout.rs b/sixtyfps_runtime/corelib/layout.rs index 64faf0605..bf82b28d6 100644 --- a/sixtyfps_runtime/corelib/layout.rs +++ b/sixtyfps_runtime/corelib/layout.rs @@ -11,7 +11,7 @@ LICENSE END */ // cspell:ignore coord -use crate::{slice::Slice, SharedVector}; +use crate::{items::DialogButtonRole, slice::Slice, SharedVector}; /// Vertical or Horizontal orientation #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -719,18 +719,6 @@ pub fn solve_path_layout(data: &PathLayoutData, repeater_indexes: Slice) -> result } -#[derive( - Copy, Clone, Eq, PartialEq, Hash, Debug, strum_macros::EnumString, strum_macros::ToString, -)] -#[repr(u8)] -pub enum DialogButtonRole { - Accept, - Reject, - Apply, - Reset, - Help, -} - /// Given the cells of a layout of a Dialog, re-order the button according to the platform /// /// This function assume that the `roles` contains the roles of the button which are the first `cells` @@ -753,19 +741,21 @@ pub fn reorder_dialog_button_layout(cells: &mut [GridLayoutCellData], roles: &[D let mut idx = 0; if cfg!(windows) { - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reset); idx += 1; - add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); + add_buttons(cells, roles, &mut idx, DialogButtonRole::accept); + add_buttons(cells, roles, &mut idx, DialogButtonRole::action); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reject); + add_buttons(cells, roles, &mut idx, DialogButtonRole::apply); + add_buttons(cells, roles, &mut idx, DialogButtonRole::help); } else if cfg!(target_os = "macos") { - add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); + add_buttons(cells, roles, &mut idx, DialogButtonRole::help); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reset); + add_buttons(cells, roles, &mut idx, DialogButtonRole::apply); + add_buttons(cells, roles, &mut idx, DialogButtonRole::action); idx += 1; - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reject); + add_buttons(cells, roles, &mut idx, DialogButtonRole::accept); // assume some unix check if XDG_CURRENT_DESKTOP stats with K } else if std::env::var("XDG_CURRENT_DESKTOP") @@ -774,20 +764,22 @@ pub fn reorder_dialog_button_layout(cells: &mut [GridLayoutCellData], roles: &[D .map_or(false, |x| x.to_ascii_uppercase() == b'K') { // KDE variant - add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); + add_buttons(cells, roles, &mut idx, DialogButtonRole::help); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reset); idx += 1; - add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); + add_buttons(cells, roles, &mut idx, DialogButtonRole::action); + add_buttons(cells, roles, &mut idx, DialogButtonRole::accept); + add_buttons(cells, roles, &mut idx, DialogButtonRole::apply); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reject); } else { // GNOME variant and fallback for WASM build - add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); + add_buttons(cells, roles, &mut idx, DialogButtonRole::help); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reset); idx += 1; - add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); - add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); + add_buttons(cells, roles, &mut idx, DialogButtonRole::action); + add_buttons(cells, roles, &mut idx, DialogButtonRole::apply); + add_buttons(cells, roles, &mut idx, DialogButtonRole::reject); + add_buttons(cells, roles, &mut idx, DialogButtonRole::accept); } } diff --git a/sixtyfps_runtime/corelib/rtti.rs b/sixtyfps_runtime/corelib/rtti.rs index 1c8dbaa86..a95c7277b 100644 --- a/sixtyfps_runtime/corelib/rtti.rs +++ b/sixtyfps_runtime/corelib/rtti.rs @@ -47,6 +47,7 @@ declare_ValueType![ crate::items::EventResult, crate::Brush, crate::items::FillRule, + crate::items::DialogButtonRole, crate::items::StandardButtonKind, crate::graphics::Point, crate::items::PointerEvent, diff --git a/sixtyfps_runtime/interpreter/api.rs b/sixtyfps_runtime/interpreter/api.rs index 19399c955..03aa952c6 100644 --- a/sixtyfps_runtime/interpreter/api.rs +++ b/sixtyfps_runtime/interpreter/api.rs @@ -315,6 +315,7 @@ declare_value_enum_conversion!(sixtyfps_corelib::items::FillRule, FillRule); declare_value_enum_conversion!(sixtyfps_corelib::items::StandardButtonKind, StandardButtonKind); declare_value_enum_conversion!(sixtyfps_corelib::items::PointerEventKind, PointerEventKind); declare_value_enum_conversion!(sixtyfps_corelib::items::PointerEventButton, PointerEventButton); +declare_value_enum_conversion!(sixtyfps_corelib::items::DialogButtonRole, DialogButtonRole); impl From for Value { fn from(value: sixtyfps_corelib::animations::Instant) -> Self { diff --git a/sixtyfps_runtime/interpreter/dynamic_component.rs b/sixtyfps_runtime/interpreter/dynamic_component.rs index bce443694..bd8bf0871 100644 --- a/sixtyfps_runtime/interpreter/dynamic_component.rs +++ b/sixtyfps_runtime/interpreter/dynamic_component.rs @@ -892,6 +892,7 @@ pub(crate) fn generate_component<'id>( "StandardButtonKind" => { property_info::() } + "DialogButtonRole" => property_info::(), "PointerEventButton" => { property_info::() } diff --git a/sixtyfps_runtime/interpreter/eval_layout.rs b/sixtyfps_runtime/interpreter/eval_layout.rs index be46f2eab..ed2eff0a1 100644 --- a/sixtyfps_runtime/interpreter/eval_layout.rs +++ b/sixtyfps_runtime/interpreter/eval_layout.rs @@ -16,6 +16,7 @@ use sixtyfps_compilerlib::langtype::Type; use sixtyfps_compilerlib::layout::{Layout, LayoutConstraints, LayoutGeometry, Orientation}; use sixtyfps_compilerlib::namedreference::NamedReference; use sixtyfps_compilerlib::object_tree::ElementRc; +use sixtyfps_corelib::items::DialogButtonRole; use sixtyfps_corelib::layout::{self as core_layout}; use sixtyfps_corelib::model::RepeatedComponent; use sixtyfps_corelib::slice::Slice; @@ -98,7 +99,7 @@ pub(crate) fn solve_layout( { let roles = buttons_roles .iter() - .map(|r| core_layout::DialogButtonRole::from_str(r).unwrap()) + .map(|r| DialogButtonRole::from_str(r).unwrap()) .collect::>(); core_layout::reorder_dialog_button_layout(&mut cells, &roles); } diff --git a/tests/cases/elements/dialog.60 b/tests/cases/elements/dialog.60 index 9664f35c9..cc7ecc09b 100644 --- a/tests/cases/elements/dialog.60 +++ b/tests/cases/elements/dialog.60 @@ -8,7 +8,7 @@ Please contact info@sixtyfps.io for more information. LICENSE END */ -import { StandardButton, GridBox } from "sixtyfps_widgets.60"; +import { StandardButton, Button, GridBox } from "sixtyfps_widgets.60"; TestCase := Dialog { Rectangle { @@ -23,6 +23,10 @@ TestCase := Dialog { StandardButton { kind: apply; } StandardButton { kind: reset; } StandardButton { kind: yes; } + Button { + text: "Action"; + dialog-button-role: action; + } } /*