diff --git a/CHANGELOG.md b/CHANGELOG.md index e29a57521..5126ee9ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - `0` can be converted to anything with units - Support power of unit in intermediate expression. (eg: `3px * width / height` is now supported but used to be an error) - Support for `else if` + - The path fill rule can now be specified using `Path::fill-rule`. ### Fixed - `Image::image-fit`'s `cover` and `contains` varient are fixed to match the CSS spec diff --git a/api/sixtyfps-cpp/include/sixtyfps.h b/api/sixtyfps-cpp/include/sixtyfps.h index dd8cfeeb2..f39ad0b89 100644 --- a/api/sixtyfps-cpp/include/sixtyfps.h +++ b/api/sixtyfps-cpp/include/sixtyfps.h @@ -81,6 +81,7 @@ using cbindgen_private::ImageFit; using cbindgen_private::KeyEvent; using cbindgen_private::EventResult; using cbindgen_private::KeyboardModifiers; +using cbindgen_private::FillRule; namespace private_api { using ItemTreeNode = cbindgen_private::ItemTreeNode; diff --git a/docs/builtin_elements.md b/docs/builtin_elements.md index 491abb702..9f855cc65 100644 --- a/docs/builtin_elements.md +++ b/docs/builtin_elements.md @@ -183,6 +183,7 @@ accordingly. ### Common Path Properties * **`fill`** (*brush*): The color for filling the shape of the path. +* **`fill-rule`** (enum *[`FillRule`](#fillrule)*): The fill rule to use for the path. (default value: `nonzero`) * **`stroke`** (*brush*): The color for drawing the outline of the path. * **`stroke-width`** (*length*): The width of the outline. * **`width`** (*length*): If non-zero, the path will be scaled to fit into the specified width. @@ -640,3 +641,11 @@ This enum describes whether an event was rejected or accepted by an event handle * **`EventResult.reject`**: The event is rejected by this event handler and may then be handled by parent item * **`EventResult.accept`**: The event is accepted and won't be processed further +## `FillRule` + +This enum describes the different ways of deciding what the inside of a shape described by a path shall be. + +### Values + +* **`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). diff --git a/sixtyfps_compiler/builtins.60 b/sixtyfps_compiler/builtins.60 index 2c6282dce..5cf6988ad 100644 --- a/sixtyfps_compiler/builtins.60 +++ b/sixtyfps_compiler/builtins.60 @@ -275,6 +275,7 @@ export Path := _ { property height; property fill; property fill_color <=> fill; + property fill_rule; property stroke; property stroke_color <=> stroke; property stroke_width; diff --git a/sixtyfps_compiler/generator/cpp.rs b/sixtyfps_compiler/generator/cpp.rs index b09c5261e..9c7224342 100644 --- a/sixtyfps_compiler/generator/cpp.rs +++ b/sixtyfps_compiler/generator/cpp.rs @@ -2353,9 +2353,7 @@ fn compile_path_events(events: &crate::expression_tree::PathEvents) -> (Vec { - debug_assert_eq!(coordinates.first(), Some(&first)); - debug_assert_eq!(coordinates.last(), Some(&last)); + Event::End { close, .. } => { if *close { "sixtyfps::PathEvent::EndClosed" } else { diff --git a/sixtyfps_compiler/generator/rust.rs b/sixtyfps_compiler/generator/rust.rs index 9e9eea57f..24b2b91ac 100644 --- a/sixtyfps_compiler/generator/rust.rs +++ b/sixtyfps_compiler/generator/rust.rs @@ -2012,9 +2012,7 @@ fn compile_path_events(events: &crate::expression_tree::PathEvents) -> TokenStre coordinates.push(to); quote!(sixtyfps::re_exports::PathEvent::Cubic) } - Event::End { last, first, close } => { - debug_assert_eq!(coordinates.first(), Some(&first)); - debug_assert_eq!(coordinates.last(), Some(&last)); + Event::End { close, .. } => { if *close { quote!(sixtyfps::re_exports::PathEvent::EndClosed) } else { diff --git a/sixtyfps_compiler/typeregister.rs b/sixtyfps_compiler/typeregister.rs index c8a23749d..efb711070 100644 --- a/sixtyfps_compiler/typeregister.rs +++ b/sixtyfps_compiler/typeregister.rs @@ -145,6 +145,7 @@ impl TypeRegister { ); declare_enum("ImageFit", &["fill", "contain", "cover"]); declare_enum("EventResult", &["reject", "accept"]); + declare_enum("FillRule", &["nonzero", "evenodd"]); 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 6d7233eb3..04ed08abd 100644 --- a/sixtyfps_runtime/corelib/items.rs +++ b/sixtyfps_runtime/corelib/items.rs @@ -564,6 +564,20 @@ ItemVTable_static! { pub static ClipVTable for Clip } +#[derive(Copy, Clone, Debug, PartialEq, strum_macros::EnumString, strum_macros::Display)] +#[repr(C)] +#[allow(non_camel_case_types)] +pub enum FillRule { + nonzero, + evenodd, +} + +impl Default for FillRule { + fn default() -> Self { + Self::nonzero + } +} + /// The implementation of the `Path` element #[repr(C)] #[derive(FieldOffsets, Default, SixtyFPSElement)] @@ -575,6 +589,7 @@ pub struct Path { pub height: Property, pub elements: Property, pub fill: Property, + pub fill_rule: Property, pub stroke: Property, pub stroke_width: Property, pub cached_rendering_data: CachedRenderingData, diff --git a/sixtyfps_runtime/corelib/rtti.rs b/sixtyfps_runtime/corelib/rtti.rs index 1a447885b..9a4c2638f 100644 --- a/sixtyfps_runtime/corelib/rtti.rs +++ b/sixtyfps_runtime/corelib/rtti.rs @@ -45,6 +45,7 @@ declare_ValueType![ crate::input::KeyEvent, crate::items::EventResult, crate::Brush, + crate::items::FillRule, ]; /// What kind of animation is on a binding diff --git a/sixtyfps_runtime/interpreter/dynamic_component.rs b/sixtyfps_runtime/interpreter/dynamic_component.rs index 4b43aeabd..bb4407664 100644 --- a/sixtyfps_runtime/interpreter/dynamic_component.rs +++ b/sixtyfps_runtime/interpreter/dynamic_component.rs @@ -645,6 +645,7 @@ fn generate_component<'id>( "TextWrap" => property_info::(), "TextOverflow" => property_info::(), "ImageFit" => property_info::(), + "FillRule" => property_info::(), _ => panic!("unkown enum"), }, _ => panic!("bad type"), diff --git a/sixtyfps_runtime/interpreter/eval.rs b/sixtyfps_runtime/interpreter/eval.rs index 2b2315e24..e0768a253 100644 --- a/sixtyfps_runtime/interpreter/eval.rs +++ b/sixtyfps_runtime/interpreter/eval.rs @@ -253,6 +253,7 @@ declare_value_enum_conversion!(corelib::layout::LayoutAlignment, LayoutAlignment declare_value_enum_conversion!(corelib::items::ImageFit, ImageFit); declare_value_enum_conversion!(corelib::input::KeyEventType, KeyEventType); declare_value_enum_conversion!(corelib::items::EventResult, EventResult); +declare_value_enum_conversion!(corelib::items::FillRule, FillRule); impl TryFrom for Value { type Error = (); @@ -994,9 +995,7 @@ fn convert_from_lyon_path<'a>( coordinates.push(to); PathEvent::Cubic } - Event::End { last, first, close } => { - debug_assert_eq!(coordinates.first(), Some(&first)); - debug_assert_eq!(coordinates.last(), Some(&last)); + Event::End { close, .. } => { if *close { PathEvent::EndClosed } else { diff --git a/sixtyfps_runtime/rendering_backends/gl/lib.rs b/sixtyfps_runtime/rendering_backends/gl/lib.rs index 4fc101012..f6e7a2bf1 100644 --- a/sixtyfps_runtime/rendering_backends/gl/lib.rs +++ b/sixtyfps_runtime/rendering_backends/gl/lib.rs @@ -18,7 +18,8 @@ use sixtyfps_corelib::graphics::{ }; use sixtyfps_corelib::item_rendering::{CachedRenderingData, ItemRenderer}; use sixtyfps_corelib::items::{ - ImageFit, Item, TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap, + FillRule, ImageFit, Item, TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, + TextWrap, }; use sixtyfps_corelib::properties::Property; use sixtyfps_corelib::window::ComponentWindow; @@ -820,7 +821,14 @@ impl ItemRenderer for GLItemRenderer { } } - let fill_paint = self.brush_to_paint(path.fill(), &mut fpath); + let fill_paint = self.brush_to_paint(path.fill(), &mut fpath).map(|mut fill_paint| { + fill_paint.set_fill_rule(match path.fill_rule() { + FillRule::nonzero => femtovg::FillRule::NonZero, + FillRule::evenodd => femtovg::FillRule::EvenOdd, + }); + fill_paint + }); + let border_paint = self.brush_to_paint(path.stroke(), &mut fpath).map(|mut paint| { paint.set_line_width(path.stroke_width()); paint diff --git a/sixtyfps_runtime/rendering_backends/qt/key_generated.rs b/sixtyfps_runtime/rendering_backends/qt/key_generated.rs index 34244ba2e..95ab9e3ad 100644 --- a/sixtyfps_runtime/rendering_backends/qt/key_generated.rs +++ b/sixtyfps_runtime/rendering_backends/qt/key_generated.rs @@ -9,7 +9,7 @@ LICENSE END */ /*! Generated with ```sh -bindgen /usr/include/qt/QtCore/qnamespace.h --whitelist-type Qt::Key --whitelist-type Qt::KeyboardModifier --whitelist-type Qt::AlignmentFlag --whitelist-type Qt::TextFlag -o sixtyfps_runtime/rendering_backends/qt/key_generated.rs -- -I /usr/include/qt -xc++ +bindgen /usr/include/qt/QtCore/qnamespace.h --whitelist-type Qt::Key --whitelist-type Qt::KeyboardModifier --whitelist-type Qt::AlignmentFlag --whitelist-type Qt::TextFlag --whitelist-type Qt::FillRule -o sixtyfps_runtime/rendering_backends/qt/key_generated.rs -- -I /usr/include/qt -xc++ ``` then add licence header and this doc */ @@ -17,7 +17,7 @@ then add licence header and this doc #![allow(non_camel_case_types)] #![allow(non_upper_case_globals)] -/* automatically generated by rust-bindgen 0.56.0 */ +/* automatically generated by rust-bindgen 0.57.0 */ pub const Qt_KeyboardModifier_NoModifier: Qt_KeyboardModifier = 0; pub const Qt_KeyboardModifier_ShiftModifier: Qt_KeyboardModifier = 33554432; @@ -528,3 +528,6 @@ pub const Qt_Key_Key_Camera: Qt_Key = 17825824; pub const Qt_Key_Key_CameraFocus: Qt_Key = 17825825; pub const Qt_Key_Key_unknown: Qt_Key = 33554431; pub type Qt_Key = ::std::os::raw::c_uint; +pub const Qt_FillRule_OddEvenFill: Qt_FillRule = 0; +pub const Qt_FillRule_WindingFill: Qt_FillRule = 1; +pub type Qt_FillRule = ::std::os::raw::c_uint; diff --git a/sixtyfps_runtime/rendering_backends/qt/qt_window.rs b/sixtyfps_runtime/rendering_backends/qt/qt_window.rs index 619e2d901..fe46fef11 100644 --- a/sixtyfps_runtime/rendering_backends/qt/qt_window.rs +++ b/sixtyfps_runtime/rendering_backends/qt/qt_window.rs @@ -13,7 +13,7 @@ use items::{ImageFit, TextHorizontalAlignment, TextVerticalAlignment}; use sixtyfps_corelib::graphics::{Brush, FontRequest, Point, Rect, RenderingCache}; use sixtyfps_corelib::input::{InternalKeyCode, KeyEvent, KeyEventType, MouseEventType}; use sixtyfps_corelib::item_rendering::{CachedRenderingData, ItemRenderer}; -use sixtyfps_corelib::items::{self, ItemRef, TextOverflow, TextWrap}; +use sixtyfps_corelib::items::{self, FillRule, ItemRef, TextOverflow, TextWrap}; use sixtyfps_corelib::properties::PropertyTracker; use sixtyfps_corelib::slice::Slice; use sixtyfps_corelib::window::PlatformWindow; @@ -187,6 +187,12 @@ impl QPainterPath { self->closeSubpath(); }} } + + pub fn set_fill_rule(&mut self, rule: key_generated::Qt_FillRule) { + cpp! { unsafe [self as "QPainterPath*", rule as "Qt::FillRule" ] { + self->setFillRule(rule); + }} + } } /// Given a position offset and an object of a given type that has x,y,width,height properties, @@ -383,6 +389,12 @@ impl ItemRenderer for QtItemRenderer<'_> { y: (pos.y + path.y() + offset.y) as _, }; let mut painter_path = QPainterPath::default(); + + painter_path.set_fill_rule(match path.fill_rule() { + FillRule::nonzero => key_generated::Qt_FillRule_WindingFill, + FillRule::evenodd => key_generated::Qt_FillRule_OddEvenFill, + }); + for x in path_events.iter() { impl From for qttypes::QPointF { fn from(p: Point) -> Self { diff --git a/tests/cases/examples/path_fill_rule.60 b/tests/cases/examples/path_fill_rule.60 new file mode 100644 index 000000000..4c1420cb1 --- /dev/null +++ b/tests/cases/examples/path_fill_rule.60 @@ -0,0 +1,39 @@ +/* LICENSE BEGIN + This file is part of the SixtyFPS Project -- https://sixtyfps.io + Copyright (c) 2020 Olivier Goffart + Copyright (c) 2020 Simon Hausmann + + SPDX-License-Identifier: GPL-3.0-only + This file is also available under commercial licensing terms. + Please contact info@sixtyfps.io for more information. +LICENSE END */ + +PathFillRule := Window { + GridLayout { + Row { + Text { + text: "The rectangle to the right should have a hole in the center"; + } + Path { + commands: "M210,0 h90 v90 h-90 z M230,20 v50 h50 v-50 z"; + fill: black; + fill-rule: evenodd; + stroke: red; + stroke-width: 1px; + } + } + Row { + Text { + text: "The rectangle to the right should be filled in the center"; + } + Path { + commands: "M210,0 h90 v90 h-90 z M230,20 v50 h50 v-50 z"; + fill: black; + fill-rule: nonzero; + stroke: red; + stroke-width: 1px; + } + } + } +} + diff --git a/xtask/src/cbindgen.rs b/xtask/src/cbindgen.rs index b41adc95a..5c73c35bb 100644 --- a/xtask/src/cbindgen.rs +++ b/xtask/src/cbindgen.rs @@ -63,6 +63,7 @@ fn gen_corelib(root_dir: &Path, include_dir: &Path) -> anyhow::Result<()> { "TextInput", "Clip", "BoxShadow", + "FillRule", ] .iter() .map(|x| x.to_string())