Allow normal Element to be in the Dialog's button row with dialog-button-role

This commit is contained in:
Olivier Goffart 2021-10-04 18:35:36 +02:00 committed by Olivier Goffart
parent fcf55084ab
commit ae3e1b4680
13 changed files with 165 additions and 65 deletions

View file

@ -116,6 +116,7 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> {
"ImageFit", "ImageFit",
"FillRule", "FillRule",
"StandardButtonKind", "StandardButtonKind",
"DialogButtonRole",
"PointerEventKind", "PointerEventKind",
"PointerEventButton", "PointerEventButton",
"PointerEvent", "PointerEvent",

View file

@ -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) 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 * **`visible`** (*bool*): When set to `false`, the element and all his children will not be drawn
and not react to mouse input (default: `true`) and not react to mouse input (default: `true`)
* **`dialog-button-role`** (*enum DialogButtonRole*): Specify that this is a button in a `Dialog`.
### Drop Shadows ### Drop Shadows
@ -656,13 +657,15 @@ Example := Window {
Dialog is like a window, but it has buttons that are automatically laid out. 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. 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 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, The `kind` property of the `StandardButton`s and the ``dialog-button-role` properties needs to be set to a specific value,
and there cannot be several button of the same kind. it cannot be a complex expression.
There cannot be several StandardButton of the same kind.
If A callback `<kind>_clicked` is automatically added for each button which does not have an explicit If A callback `<kind>_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`, 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) 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. 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 ### Example
```60 ```60
import { StandardButton } from "sixtyfps_widgets.60"; import { StandardButton, Button } from "sixtyfps_widgets.60";
Example := Dialog { Example := Dialog {
Text { Text {
text: "This is a dialog box"; text: "This is a dialog box";
} }
StandardButton { kind: ok; } StandardButton { kind: ok; }
StandardButton { kind: cancel; } 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.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). * **`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.

View file

@ -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, /// 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 /// 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<Vec<&'static str>>, pub dialog_button_roles: Option<Vec<String>>,
} }
impl GridLayout { impl GridLayout {

View file

@ -405,19 +405,24 @@ fn lower_dialog_layout(
let mut seen_buttons = HashSet::new(); let mut seen_buttons = HashSet::new();
let layout_children = std::mem::take(&mut dialog_element.borrow_mut().children); let layout_children = std::mem::take(&mut dialog_element.borrow_mut().children);
for layout_child in &layout_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") matches!(&pd.property_type, Type::Enumeration(e) if e.name == "StandardButtonKind")
}); }) {
// layout_child is a StandardButton
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,
);
match layout_child.borrow().bindings.get("kind") { match layout_child.borrow().bindings.get("kind") {
None => diag.push_error( None => diag.push_error(
"The `kind` property of the StandardButton in a Dialog must be set".into(), "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"); debug_assert_eq!(en.name, "StandardButtonKind");
let kind = &en.values[val.value]; let kind = &en.values[val.value];
let role = match kind.as_str() { let role = match kind.as_str() {
"ok" => "Accept", "ok" => "accept",
"cancel" => "Reject", "cancel" => "reject",
"apply" => "Apply", "apply" => "apply",
"close" => "Reject", "close" => "reject",
"reset" => "Reset", "reset" => "reset",
"help" => "Help", "help" => "help",
"yes" => "Accept", "yes" => "accept",
"no" => "Reject", "no" => "reject",
"abort" => "Reject", "abort" => "reject",
"retry" => "Accept", "retry" => "accept",
"ignore" => "Accept", "ignore" => "accept",
_ => unreachable!(), _ => unreachable!(),
}; };
button_roles.push(role); button_roles.push(role.into());
if !seen_buttons.insert(val.value) { if !seen_buttons.insert(val.value) {
diag.push_error("Duplicated `kind`: There are two StandardButton in this Dialog with the same kind".into(), binding); diag.push_error("Duplicated `kind`: There are two StandardButton in this Dialog with the same kind".into(), binding);
} else if Rc::ptr_eq( } 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() { } else if main_widget.is_some() {
diag.push_error( diag.push_error(
"A Dialog can have only one child element that is not a StandardButton".into(), "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") { if matches!(prop.as_ref(), "col" | "row" | "colspan" | "rowspan") {
diag.push_error(format!("{} used outside of a GridLayout", prop), expr); 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);
}
} }
} }

View file

@ -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 { Test := Rectangle {
MyDiag1 {} MyDiag1 {}
MyDiag2 {} // FIXME: not the best place for the error MyDiag2 {} // FIXME: not the best place for the error
// ^error{A Dialog must have a single child element that is not StandardButton} // ^error{A Dialog must have a single child element that is not StandardButton}
MyDiag3 {} MyDiag3 {}
MyDialog4 {}
} }

View file

@ -43,6 +43,24 @@ const RESERVED_LAYOUT_PROPERTIES: &[(&str, Type)] = &[
("rowspan", Type::Int32), ("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)] = &[ const RESERVED_OTHER_PROPERTIES: &[(&str, Type)] = &[
("clip", Type::Bool), ("clip", Type::Bool),
("opacity", Type::Float32), ("opacity", Type::Float32),
@ -67,6 +85,7 @@ pub fn reserved_properties() -> impl Iterator<Item = (&'static str, Type)> {
.chain(std::array::IntoIter::new([ .chain(std::array::IntoIter::new([
("forward-focus", Type::ElementReference), ("forward-focus", Type::ElementReference),
("focus", BuiltinFunction::SetFocusItem.ty()), ("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("PointerEventKind", &["cancel", "down", "up"]);
declare_enum("PointerEventButton", &["none", "left", "right", "middle"]); 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::Float32.to_string());
register.supported_property_animation_types.insert(Type::Int32.to_string()); register.supported_property_animation_types.insert(Type::Int32.to_string());

View file

@ -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)] #[derive(Copy, Clone, Debug, PartialEq, strum_macros::EnumString, strum_macros::Display)]
#[repr(C)] #[repr(C)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]

View file

@ -11,7 +11,7 @@ LICENSE END */
// cspell:ignore coord // cspell:ignore coord
use crate::{slice::Slice, SharedVector}; use crate::{items::DialogButtonRole, slice::Slice, SharedVector};
/// Vertical or Horizontal orientation /// Vertical or Horizontal orientation
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
@ -719,18 +719,6 @@ pub fn solve_path_layout(data: &PathLayoutData, repeater_indexes: Slice<u32>) ->
result 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 /// 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` /// 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; let mut idx = 0;
if cfg!(windows) { if cfg!(windows) {
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); add_buttons(cells, roles, &mut idx, DialogButtonRole::reset);
idx += 1; idx += 1;
add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); add_buttons(cells, roles, &mut idx, DialogButtonRole::accept);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); 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::Help); add_buttons(cells, roles, &mut idx, DialogButtonRole::apply);
add_buttons(cells, roles, &mut idx, DialogButtonRole::help);
} else if cfg!(target_os = "macos") { } else if cfg!(target_os = "macos") {
add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); add_buttons(cells, roles, &mut idx, DialogButtonRole::help);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); add_buttons(cells, roles, &mut idx, DialogButtonRole::reset);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); add_buttons(cells, roles, &mut idx, DialogButtonRole::apply);
add_buttons(cells, roles, &mut idx, DialogButtonRole::action);
idx += 1; idx += 1;
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); add_buttons(cells, roles, &mut idx, DialogButtonRole::reject);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); add_buttons(cells, roles, &mut idx, DialogButtonRole::accept);
// assume some unix check if XDG_CURRENT_DESKTOP stats with K // assume some unix check if XDG_CURRENT_DESKTOP stats with K
} else if std::env::var("XDG_CURRENT_DESKTOP") } 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') .map_or(false, |x| x.to_ascii_uppercase() == b'K')
{ {
// KDE variant // KDE variant
add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); add_buttons(cells, roles, &mut idx, DialogButtonRole::help);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); add_buttons(cells, roles, &mut idx, DialogButtonRole::reset);
idx += 1; idx += 1;
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::accept);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); add_buttons(cells, roles, &mut idx, DialogButtonRole::apply);
add_buttons(cells, roles, &mut idx, DialogButtonRole::reject);
} else { } else {
// GNOME variant and fallback for WASM build // GNOME variant and fallback for WASM build
add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); add_buttons(cells, roles, &mut idx, DialogButtonRole::help);
add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); add_buttons(cells, roles, &mut idx, DialogButtonRole::reset);
idx += 1; idx += 1;
add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); 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::Accept); add_buttons(cells, roles, &mut idx, DialogButtonRole::reject);
add_buttons(cells, roles, &mut idx, DialogButtonRole::accept);
} }
} }

View file

@ -47,6 +47,7 @@ declare_ValueType![
crate::items::EventResult, crate::items::EventResult,
crate::Brush, crate::Brush,
crate::items::FillRule, crate::items::FillRule,
crate::items::DialogButtonRole,
crate::items::StandardButtonKind, crate::items::StandardButtonKind,
crate::graphics::Point, crate::graphics::Point,
crate::items::PointerEvent, crate::items::PointerEvent,

View file

@ -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::StandardButtonKind, StandardButtonKind);
declare_value_enum_conversion!(sixtyfps_corelib::items::PointerEventKind, PointerEventKind); 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::PointerEventButton, PointerEventButton);
declare_value_enum_conversion!(sixtyfps_corelib::items::DialogButtonRole, DialogButtonRole);
impl From<sixtyfps_corelib::animations::Instant> for Value { impl From<sixtyfps_corelib::animations::Instant> for Value {
fn from(value: sixtyfps_corelib::animations::Instant) -> Self { fn from(value: sixtyfps_corelib::animations::Instant) -> Self {

View file

@ -892,6 +892,7 @@ pub(crate) fn generate_component<'id>(
"StandardButtonKind" => { "StandardButtonKind" => {
property_info::<sixtyfps_corelib::items::StandardButtonKind>() property_info::<sixtyfps_corelib::items::StandardButtonKind>()
} }
"DialogButtonRole" => property_info::<sixtyfps_corelib::items::DialogButtonRole>(),
"PointerEventButton" => { "PointerEventButton" => {
property_info::<sixtyfps_corelib::items::PointerEventButton>() property_info::<sixtyfps_corelib::items::PointerEventButton>()
} }

View file

@ -16,6 +16,7 @@ use sixtyfps_compilerlib::langtype::Type;
use sixtyfps_compilerlib::layout::{Layout, LayoutConstraints, LayoutGeometry, Orientation}; use sixtyfps_compilerlib::layout::{Layout, LayoutConstraints, LayoutGeometry, Orientation};
use sixtyfps_compilerlib::namedreference::NamedReference; use sixtyfps_compilerlib::namedreference::NamedReference;
use sixtyfps_compilerlib::object_tree::ElementRc; use sixtyfps_compilerlib::object_tree::ElementRc;
use sixtyfps_corelib::items::DialogButtonRole;
use sixtyfps_corelib::layout::{self as core_layout}; use sixtyfps_corelib::layout::{self as core_layout};
use sixtyfps_corelib::model::RepeatedComponent; use sixtyfps_corelib::model::RepeatedComponent;
use sixtyfps_corelib::slice::Slice; use sixtyfps_corelib::slice::Slice;
@ -98,7 +99,7 @@ pub(crate) fn solve_layout(
{ {
let roles = buttons_roles let roles = buttons_roles
.iter() .iter()
.map(|r| core_layout::DialogButtonRole::from_str(r).unwrap()) .map(|r| DialogButtonRole::from_str(r).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
core_layout::reorder_dialog_button_layout(&mut cells, &roles); core_layout::reorder_dialog_button_layout(&mut cells, &roles);
} }

View file

@ -8,7 +8,7 @@
Please contact info@sixtyfps.io for more information. Please contact info@sixtyfps.io for more information.
LICENSE END */ LICENSE END */
import { StandardButton, GridBox } from "sixtyfps_widgets.60"; import { StandardButton, Button, GridBox } from "sixtyfps_widgets.60";
TestCase := Dialog { TestCase := Dialog {
Rectangle { Rectangle {
@ -23,6 +23,10 @@ TestCase := Dialog {
StandardButton { kind: apply; } StandardButton { kind: apply; }
StandardButton { kind: reset; } StandardButton { kind: reset; }
StandardButton { kind: yes; } StandardButton { kind: yes; }
Button {
text: "Action";
dialog-button-role: action;
}
} }
/* /*