From 4e9239ab770a7183dea566a4c8d3fb57645e2c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 21 Mar 2018 16:06:57 +0100 Subject: [PATCH 001/868] Added parsing for font-family and image properties --- assets/fonts/weblysleekuil.ttf | Bin 0 -> 676412 bytes assets/images/cat_image.jpg | Bin 0 -> 552998 bytes examples/debug.rs | 18 +++ examples/test_content.css | 6 + src/app.rs | 19 ++- src/css.rs | 3 +- src/css_parser.rs | 223 ++++++++++++++++++++++++++++----- src/display_list.rs | 13 +- src/dom.rs | 11 +- src/lib.rs | 3 +- src/resources.rs | 16 ++- 11 files changed, 260 insertions(+), 52 deletions(-) create mode 100644 assets/fonts/weblysleekuil.ttf create mode 100644 assets/images/cat_image.jpg diff --git a/assets/fonts/weblysleekuil.ttf b/assets/fonts/weblysleekuil.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a4666b5346387090dc909d62d19adc65cb5070db GIT binary patch literal 676412 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%0DJjK^A?Z~7_n?4VBfs~z{Y#$LKW=cSauAg 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%0D#Jp(rsIV2A8be(dK6yWn$th1?W**wkTU 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≀R&>0XJA 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
O5{AZA-cNtZl3(jumBM^6bGCR$SEsEjyt7lN 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%0D;D8z0~@{xA&>jZSEPjv_ebQcMd8%sr&=y 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 XyXFd12, + pub images: HashMap, } impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { + let mut dom = Dom::new(NodeType::Div); dom.class("__azul-native-button"); dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); @@ -35,11 +41,23 @@ fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen fn main() { let css = Css::new_from_string(TEST_CSS).unwrap(); + let mut fonts = HashMap::new(); + fonts.insert("Roboto".to_string(), azul::resources::new_font_id()); + let my_app_data = MyAppData { my_data: 0, + fonts: fonts, + images: HashMap::new(), }; let mut app = App::new(my_app_data); + + app.add_font("Webly Sleeky UI", TEST_FONT); // adds a new font to use in the CSS + // app.remove_font("Webly Sleeky UI"); // removes a font and all font instances + + app.add_image("MyImage", TEST_IMAGE); // adds an image + // app.remove_image("MyImage"); // removes an image + // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that app.create_window(WindowCreateOptions::default(), css).unwrap(); diff --git a/examples/test_content.css b/examples/test_content.css index 56dbc3b97..354f0e664 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -15,4 +15,10 @@ justify-content: space-around; align-items: center; align-content: center; +} + +* { + font-size: 15px; + font-family: "Webly Sleeky UI", sans-serif; + background: image("Cat01"); } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 8c81a4aaf..4e1af015e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,6 +9,7 @@ use std::sync::{Arc, Mutex}; use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; use glium::glutin::Event; use euclid::TypedScale; +use std::io::Read; /// Graphical application that maintains some kind of application state pub struct App { @@ -77,7 +78,6 @@ impl App { } } - 'render_loop: loop { let mut closed_windows = Vec::::new(); @@ -199,6 +199,23 @@ impl App { } } } + + /// Add an image to the internal resources + pub fn add_image, R: Read>(&mut self, _id: S, _data: R) { + + } + + pub fn remove_image>(&mut self, _id: S) { + + } + + pub fn add_font, R: Read>(&mut self, _id: S, _data: R) { + + } + + pub fn remove_font>(&mut self, _id: S) { + + } } fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { diff --git a/src/css.rs b/src/css.rs index 5290cb1f0..f773ab03d 100644 --- a/src/css.rs +++ b/src/css.rs @@ -27,7 +27,8 @@ pub struct Css { pub(crate) is_dirty: bool, /// Has the CSS changed in a way where it needs a re-layout? /// - /// Ex. if only a background color has changed, we need to redraw, but we don't need to re-layout the frame + /// Ex. if only a background color has changed, we need to redraw, but we + /// don't need to re-layout the frame pub(crate) needs_relayout: bool, } diff --git a/src/css_parser.rs b/src/css_parser.rs index c31094d06..47f0c6ae9 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -85,6 +85,11 @@ pub enum CssColorParseError<'a> { ValueParseErr(ParseIntError), } +#[derive(Debug, PartialEq)] +pub enum CssImageParseError<'a> { + UnclosedQuotes(&'a str), +} + #[derive(Debug, PartialEq)] pub enum CssBorderParseError<'a> { InvalidBorderStyle(InvalidValueErr<'a>), @@ -660,16 +665,25 @@ pub enum CssBackgroundParseError<'a> { DirectionParseError(CssDirectionParseError<'a>), GradientParseError(CssGradientStopParseError<'a>), ShapeParseError(CssShapeParseError<'a>), + ImageParseError(CssImageParseError<'a>), } impl_from!(CssDirectionParseError, CssBackgroundParseError, DirectionParseError); impl_from!(CssGradientStopParseError, CssBackgroundParseError, GradientParseError); impl_from!(CssShapeParseError, CssBackgroundParseError, ShapeParseError); +impl_from!(CssImageParseError, CssBackgroundParseError, ImageParseError); #[derive(Debug, Clone, PartialEq)] -pub enum ParsedGradient { +pub enum Background<'a> { LinearGradient(LinearGradientPreInfo), RadialGradient(RadialGradientPreInfo), + Image(CssImageId<'a>) +} + +impl<'a> From> for Background<'a> { + fn from(id: CssImageId<'a>) -> Self { + Background::Image(id) + } } #[derive(Debug, Clone, PartialEq)] @@ -772,26 +786,31 @@ impl DirectionCorner { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum BackgroundType { + LinearGradient, + RepeatingLinearGradient, + RadialGradient, + RepeatingRadialGradient, + Image, +} + + // parses a background, such as "linear-gradient(red, green)" pub fn parse_css_background<'a>(input: &'a str) --> Result> +-> Result> { - #[derive(PartialEq)] - enum GradientType { - LinearGradient, - RepeatingLinearGradient, - RadialGradient, - RepeatingRadialGradient, - } + use self::BackgroundType::*; let mut input_iter = input.splitn(2, "("); let first_item = input_iter.next(); - let gradient_type = match first_item { - Some("linear-gradient") => GradientType::LinearGradient, - Some("repeating-linear-gradient") => GradientType::RepeatingLinearGradient, - Some("radial-gradient") => GradientType::RadialGradient, - Some("repeating-radial-gradient") => GradientType::RepeatingRadialGradient, + let background_type = match first_item { + Some("linear-gradient") => LinearGradient, + Some("repeating-linear-gradient") => RepeatingLinearGradient, + Some("radial-gradient") => RadialGradient, + Some("repeating-radial-gradient") => RepeatingRadialGradient, + Some("image") => Image, _ => { return Err(CssBackgroundParseError::InvalidBackground(first_item.unwrap())); } // failure here }; @@ -811,6 +830,11 @@ pub fn parse_css_background<'a>(input: &'a str) // brace_contents contains "red, yellow, etc" let brace_contents = brace_contents.unwrap(); + if background_type == Image { + let image = parse_image(brace_contents)?; + return Ok(image.into()); + } + let mut brace_iterator = brace_contents.split(','); let mut gradient_stop_count = brace_iterator.clone().count(); @@ -828,8 +852,8 @@ pub fn parse_css_background<'a>(input: &'a str) let mut first_is_direction = false; let mut first_is_shape = false; - let is_linear_gradient = gradient_type == GradientType::LinearGradient || gradient_type == GradientType::RepeatingLinearGradient; - let is_radial_gradient = gradient_type == GradientType::RadialGradient || gradient_type == GradientType::RepeatingRadialGradient; + let is_linear_gradient = background_type == LinearGradient || background_type == RepeatingLinearGradient; + let is_radial_gradient = background_type == RadialGradient || background_type == RepeatingRadialGradient; if is_linear_gradient { if let Ok(dir) = parse_direction(first_brace_item) { @@ -919,35 +943,68 @@ pub fn parse_css_background<'a>(input: &'a str) } } - match gradient_type { - GradientType::LinearGradient => { - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + match background_type { + LinearGradient => { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: direction, extend_mode: ExtendMode::Clamp, stops: color_stops, })) }, - GradientType::RepeatingLinearGradient => { - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + RepeatingLinearGradient => { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: direction, extend_mode: ExtendMode::Repeat, stops: color_stops, })) }, - GradientType::RadialGradient => { - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { + RadialGradient => { + Ok(Background::RadialGradient(RadialGradientPreInfo { shape: shape, extend_mode: ExtendMode::Clamp, stops: color_stops, })) }, - GradientType::RepeatingRadialGradient => { - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { + RepeatingRadialGradient => { + Ok(Background::RadialGradient(RadialGradientPreInfo { shape: shape, extend_mode: ExtendMode::Repeat, stops: color_stops, })) + }, + Image => unreachable!(), + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct CssImageId<'a>(pub(crate) &'a str); + +fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError<'a>> { + + let mut double_quote_iter = input.splitn(2, '"'); + double_quote_iter.next(); + let mut single_quote_iter = input.splitn(2, '\''); + single_quote_iter.next(); + + let first_double_quote = double_quote_iter.next(); + let first_single_quote = single_quote_iter.next(); + if first_double_quote.is_some() && first_single_quote.is_some() { + return Err(CssImageParseError::UnclosedQuotes(input)); + } + if first_double_quote.is_some() { + let quote_contents = first_double_quote.unwrap(); + if !quote_contents.ends_with('"') { + return Err(CssImageParseError::UnclosedQuotes(quote_contents)); + } + Ok(CssImageId(quote_contents.trim_right_matches("\""))) + } else if first_single_quote.is_some() { + let quote_contents = first_single_quote.unwrap(); + if!quote_contents.ends_with('\'') { + return Err(CssImageParseError::UnclosedQuotes(input)); } + Ok(CssImageId(quote_contents.trim_right_matches("'"))) + } else { + Err(CssImageParseError::UnclosedQuotes(input)) } } @@ -1159,17 +1216,21 @@ pub enum LayoutAlignContent { } #[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct RectStyle { +pub(crate) struct RectStyle<'a> { /// Background color of this rectangle pub(crate) background_color: Option, /// Shadow color pub(crate) box_shadow: Option, /// Gradient (location) + stops - pub(crate) background: Option, + pub(crate) background: Option>, /// Border pub(crate) border: Option<(BorderWidths, BorderDetails)>, - /// border radius + /// Border radius pub(crate) border_radius: Option, + /// Font size + pub(crate) font_size: Option, + /// Font name / family + pub(crate) font_family: Option> } // Layout constraints for a given rectangle, such as "" @@ -1191,6 +1252,74 @@ typed_pixel_value_parser!(parse_layout_height, LayoutHeight); typed_pixel_value_parser!(parse_layout_min_height, LayoutMinHeight); typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct FontSize(pub PixelValue); + +typed_pixel_value_parser!(parse_css_font_size, FontSize); + +#[derive(Debug, PartialEq, Clone)] +pub struct FontFamily<'a> { + // parsed fonts, in order, i.e. "Webly Sleeky UI", "monospace", etc. + fonts: Vec> +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Font<'a> { + BuiltinFont(&'a str), + ExternalFont(&'a str), +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum FontFamilyParseError<'a> { + InvalidFontFamily(&'a str), + UnclosedQuotes(&'a str), +} + +// parses a "font-family" declaration, such as: +// +// "Webly Sleeky UI", monospace +// 'Webly Sleeky Ui', monospace +// sans-serif +pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result, FontFamilyParseError<'a>> { + let multiple_fonts = input.split(','); + let mut fonts = Vec::with_capacity(1); + + for font in multiple_fonts { + let font = font.trim(); + + let mut double_quote_iter = font.splitn(2, '"'); + double_quote_iter.next(); + let mut single_quote_iter = font.splitn(2, '\''); + single_quote_iter.next(); + + let first_double_quote = double_quote_iter.next(); + let first_single_quote = single_quote_iter.next(); + if first_double_quote.is_some() && first_single_quote.is_some() { + return Err(FontFamilyParseError::UnclosedQuotes(font)); + } + if first_double_quote.is_some() { + let quote_contents = first_double_quote.unwrap(); + if !quote_contents.ends_with('"') { + return Err(FontFamilyParseError::UnclosedQuotes(quote_contents)); + } + fonts.push(Font::ExternalFont(quote_contents.trim_right_matches("\""))); + } else if first_single_quote.is_some() { + let quote_contents = first_single_quote.unwrap(); + if!quote_contents.ends_with('\'') { + return Err(FontFamilyParseError::UnclosedQuotes(font)); + } + fonts.push(Font::ExternalFont(quote_contents.trim_right_matches("'"))); + } else { + // neither single nor double quote, like "monospace" or "sans-serif" + fonts.push(Font::BuiltinFont(font)); + } + } + + Ok(FontFamily { + fonts: fonts, + }) +} + multi_type_parser!(parse_layout_direction, LayoutDirection, ["row", Horizontal], ["column", Vertical]); @@ -1387,7 +1516,7 @@ fn test_parse_css_border_2() { #[test] fn test_parse_linear_gradient_1() { assert_eq!(parse_css_background("linear-gradient(red, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), extend_mode: ExtendMode::Clamp, stops: vec![GradientStopPre { @@ -1404,7 +1533,7 @@ fn test_parse_linear_gradient_1() { #[test] fn test_parse_linear_gradient_2() { assert_eq!(parse_css_background("linear-gradient(red, lime, blue, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), extend_mode: ExtendMode::Clamp, stops: vec![GradientStopPre { @@ -1429,7 +1558,7 @@ fn test_parse_linear_gradient_2() { #[test] fn test_parse_linear_gradient_3() { assert_eq!(parse_css_background("repeating-linear-gradient(50deg, blue, yellow, #00FF00)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: Direction::Angle(50.0), extend_mode: ExtendMode::Repeat, stops: vec![ @@ -1451,7 +1580,7 @@ fn test_parse_linear_gradient_3() { #[test] fn test_parse_linear_gradient_4() { assert_eq!(parse_css_background("linear-gradient(to bottom right, red, yellow)"), - Ok(ParsedGradient::LinearGradient(LinearGradientPreInfo { + Ok(Background::LinearGradient(LinearGradientPreInfo { direction: Direction::FromTo(DirectionCorner::TopLeft, DirectionCorner::BottomRight), extend_mode: ExtendMode::Clamp, stops: vec![GradientStopPre { @@ -1468,7 +1597,7 @@ fn test_parse_linear_gradient_4() { #[test] fn test_parse_radial_gradient_1() { assert_eq!(parse_css_background("radial-gradient(circle, lime, blue, yellow)"), - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { + Ok(Background::RadialGradient(RadialGradientPreInfo { shape: Shape::Circle, extend_mode: ExtendMode::Clamp, stops: vec![ @@ -1579,4 +1708,30 @@ fn test_parse_css_border_radius_4() { top_right: LayoutSize::new(50.0, 50.0), bottom_left: LayoutSize::new(5.0, 5.0), })); -} \ No newline at end of file +} + +#[test] +fn test_parse_css_font_family_1() { + assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { + fonts: vec![ + Font::ExternalFont("Webly Sleeky UI"), + Font::BuiltinFont("monospace"), + ] + })); +} + +#[test] +fn test_parse_css_font_family_2() { + assert_eq!(parse_css_font_family("'Webly Sleeky UI'"), Ok(FontFamily { + fonts: vec![ + Font::ExternalFont("Webly Sleeky UI"), + ] + })); + +} +#[test] +fn test_parse_background_image() { + assert_eq!(parse_css_background("image(\"Cat 01\")"), Ok(Background::Image( + CssImageId("Cat 01") + ))); +} diff --git a/src/display_list.rs b/src/display_list.rs index dab03e0d4..2857fcc09 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -30,7 +30,7 @@ pub(crate) struct DisplayRectangle<'a> { /// The original styled node pub(crate) styled_node: &'a StyledNode, /// The style properties of the node, parsed - pub(crate) style: RectStyle, + pub(crate) style: RectStyle<'a>, /// The layout properties of the node, parsed pub(crate) layout: RectLayout, } @@ -205,7 +205,6 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { // The pre_shadow is missing the BorderRadius & LayoutRect // TODO: do we need to pop the shadows? let border_radius = rect.style.border_radius.unwrap_or(BorderRadius::zero()); - println!("pushing shadow: \n{:#?}", pre_shadow); builder.push_box_shadow(&info, bounds, pre_shadow.offset, pre_shadow.color, pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); @@ -213,10 +212,10 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { if let Some(ref background) = rect.style.background { match *background { - ParsedGradient::RadialGradient(ref _gradient) => { + Background::RadialGradient(ref _gradient) => { }, - ParsedGradient::LinearGradient(ref gradient) => { + Background::LinearGradient(ref gradient) => { let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), @@ -225,6 +224,9 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { let (begin_pt, end_pt) = gradient.direction.to_points(&bounds); let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); + }, + Background::Image(image_id) => { + // TODO: lookup image in resources } } } @@ -264,6 +266,9 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) rect.style.background_color = parse!(constraint_list, "background-color", parse_css_color); rect.style.border = parse!(constraint_list, "border", parse_css_border); rect.style.background = parse!(constraint_list, "background", parse_css_background); + rect.style.font_size = parse!(constraint_list, "font-size", parse_css_font_size); + rect.style.font_family = parse!(constraint_list, "font-family", parse_css_font_family); + let box_shadow_opt = parse!(constraint_list, "box-shadow", parse_css_box_shadow); if let Some(box_shadow_opt) = box_shadow_opt{ rect.style.box_shadow = box_shadow_opt; diff --git a/src/dom.rs b/src/dom.rs index d047e6a6e..8997596f0 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -105,22 +105,17 @@ impl Copy for Callback { } pub enum NodeType { /// Regular div Div, - Image { - id: ImageId, - }, + /// Image: The actual contents of the image are determined by the CSS + Image, /// A label that can be (optionally) be selectable with the mouse Label { /// Text of the label text: String, - /// Font ID used for the string - font_id: FontId, }, /// Button Button { /// The text on the button label: String, - /// Font ID used for the string - font_id: FontId, }, /// Unordered list Ul, @@ -217,7 +212,7 @@ impl NodeType { use self::NodeType::*; match *self { Div => "div", - Image { .. } => "img", + Image => "img", Label { .. } => "label", Button { .. } => "button", Ul => "ul", diff --git a/src/lib.rs b/src/lib.rs index a565a9048..d68a7dc9c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,6 @@ #![deny(unused_must_use)] #![allow(dead_code)] #![allow(unused_imports)] -#![allow(unused_variables)] extern crate webrender; extern crate cassowary; @@ -81,5 +80,5 @@ pub mod prelude { WindowId, WindowPlacement}; pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, WindowCreateError, WindowDecorations, WindowMonitorTarget}; - + pub use resources::{FontId, FontInstanceId, ImageInstanceId, ImageId}; } \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index 5c3145b28..85dcc4d68 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,6 +1,13 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey}; use FastHashMap; +static LAST_FONT_ID: AtomicUsize = AtomicUsize::new(0); +static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0); + +pub struct ImageInstanceId(usize); +pub struct FontInstanceId(usize); + /// Font and image keys /// /// The idea is that azul doesn't know where the resources come from, @@ -18,10 +25,15 @@ pub(crate) struct AppResources { } /// An `ImageId` is a wrapper around webrenders `ImageKey`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ImageId(usize); /// A Font ID is a wrapper around webrenders `FontKey`. -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct FontId(usize); +pub fn new_font_id() -> FontId { + let current_font_id = LAST_FONT_ID.load(Ordering::Relaxed); + LAST_FONT_ID.store(current_font_id + 1, Ordering::Relaxed); + FontId(current_font_id) +} \ No newline at end of file From 454afabfc7081743e8a29b7a1769dbbb870bc590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 21 Mar 2018 20:21:33 +0100 Subject: [PATCH 002/868] Removed duplicated code for stripping quotes --- src/css_parser.rs | 69 ++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index 47f0c6ae9..420b3aae2 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -90,6 +90,15 @@ pub enum CssImageParseError<'a> { UnclosedQuotes(&'a str), } +#[derive(Debug, PartialEq)] +pub struct UnclosedQuotesError<'a>(pub(crate) &'a str); + +impl<'a> From> for CssImageParseError<'a> { + fn from(err: UnclosedQuotesError<'a>) -> Self { + CssImageParseError::UnclosedQuotes(err.0) + } +} + #[derive(Debug, PartialEq)] pub enum CssBorderParseError<'a> { InvalidBorderStyle(InvalidValueErr<'a>), @@ -979,8 +988,27 @@ pub fn parse_css_background<'a>(input: &'a str) #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct CssImageId<'a>(pub(crate) &'a str); +impl<'a> From> for CssImageId<'a> { + fn from(input: QuoteStripped<'a>) -> Self { + CssImageId(input.0) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(crate) struct QuoteStripped<'a>(pub(crate) &'a str); + fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError<'a>> { - + Ok(strip_quotes(input)?.into()) +} + +/// Strip quotes from an input, given that both quotes use either `"` or `'`, but not both. +/// +/// Example: +/// +/// `"Helvetica"` - valid +/// `'Helvetica'` - valid +/// `'Helvetica"` - invalid +fn strip_quotes<'a>(input: &'a str) -> Result, UnclosedQuotesError<'a>> { let mut double_quote_iter = input.splitn(2, '"'); double_quote_iter.next(); let mut single_quote_iter = input.splitn(2, '\''); @@ -989,22 +1017,22 @@ fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError< let first_double_quote = double_quote_iter.next(); let first_single_quote = single_quote_iter.next(); if first_double_quote.is_some() && first_single_quote.is_some() { - return Err(CssImageParseError::UnclosedQuotes(input)); + return Err(UnclosedQuotesError(input)); } if first_double_quote.is_some() { let quote_contents = first_double_quote.unwrap(); if !quote_contents.ends_with('"') { - return Err(CssImageParseError::UnclosedQuotes(quote_contents)); + return Err(UnclosedQuotesError(quote_contents)); } - Ok(CssImageId(quote_contents.trim_right_matches("\""))) + Ok(QuoteStripped(quote_contents.trim_right_matches("\""))) } else if first_single_quote.is_some() { let quote_contents = first_single_quote.unwrap(); if!quote_contents.ends_with('\'') { - return Err(CssImageParseError::UnclosedQuotes(input)); + return Err(UnclosedQuotesError(input)); } - Ok(CssImageId(quote_contents.trim_right_matches("'"))) + Ok(QuoteStripped(quote_contents.trim_right_matches("'"))) } else { - Err(CssImageParseError::UnclosedQuotes(input)) + Err(UnclosedQuotesError(input)) } } @@ -1275,6 +1303,12 @@ pub enum FontFamilyParseError<'a> { UnclosedQuotes(&'a str), } +impl<'a> From> for FontFamilyParseError<'a> { + fn from(err: UnclosedQuotesError<'a>) -> Self { + FontFamilyParseError::UnclosedQuotes(err.0) + } +} + // parses a "font-family" declaration, such as: // // "Webly Sleeky UI", monospace @@ -1292,25 +1326,10 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result let mut single_quote_iter = font.splitn(2, '\''); single_quote_iter.next(); - let first_double_quote = double_quote_iter.next(); - let first_single_quote = single_quote_iter.next(); - if first_double_quote.is_some() && first_single_quote.is_some() { - return Err(FontFamilyParseError::UnclosedQuotes(font)); - } - if first_double_quote.is_some() { - let quote_contents = first_double_quote.unwrap(); - if !quote_contents.ends_with('"') { - return Err(FontFamilyParseError::UnclosedQuotes(quote_contents)); - } - fonts.push(Font::ExternalFont(quote_contents.trim_right_matches("\""))); - } else if first_single_quote.is_some() { - let quote_contents = first_single_quote.unwrap(); - if!quote_contents.ends_with('\'') { - return Err(FontFamilyParseError::UnclosedQuotes(font)); - } - fonts.push(Font::ExternalFont(quote_contents.trim_right_matches("'"))); + if double_quote_iter.next().is_some() || single_quote_iter.next().is_some() { + let stripped_font = strip_quotes(font)?; + fonts.push(Font::ExternalFont(stripped_font.0)); } else { - // neither single nor double quote, like "monospace" or "sans-serif" fonts.push(Font::BuiltinFont(font)); } } From 5d8632a6e87fc12b0378f9dd0beb3725e04c23a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 11:37:21 +0100 Subject: [PATCH 003/868] Added image and font errors + allow specifying the image type (jpg, png, etc.) --- examples/debug.rs | 8 ++++---- src/app.rs | 44 +++++++++++++++++++++++++++++++++++++------- src/font.rs | 9 +++++++++ src/image.rs | 25 +++++++++++++++++++++++++ src/lib.rs | 6 ++++++ 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/font.rs create mode 100644 src/image.rs diff --git a/examples/debug.rs b/examples/debug.rs index fb6ad8889..c41f35919 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -52,11 +52,11 @@ fn main() { let mut app = App::new(my_app_data); - app.add_font("Webly Sleeky UI", TEST_FONT); // adds a new font to use in the CSS - // app.remove_font("Webly Sleeky UI"); // removes a font and all font instances + app.add_font("Webly Sleeky UI", TEST_FONT).unwrap(); + app.remove_font("Webly Sleeky UI"); - app.add_image("MyImage", TEST_IMAGE); // adds an image - // app.remove_image("MyImage"); // removes an image + app.add_image("MyImage", TEST_IMAGE, ImageType::Jpeg).unwrap(); + app.remove_image("MyImage"); // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that diff --git a/src/app.rs b/src/app.rs index 4e1af015e..2bf25af0c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,6 +10,8 @@ use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; use glium::glutin::Event; use euclid::TypedScale; use std::io::Read; +use image::{ImageType, ImageError}; +use font::FontError; /// Graphical application that maintains some kind of application state pub struct App { @@ -201,20 +203,48 @@ impl App { } /// Add an image to the internal resources - pub fn add_image, R: Read>(&mut self, _id: S, _data: R) { - + /// + /// ## Returns: + /// + /// - `Ok(Some(()))` if an image with the same ID already exists. + /// - `Ok(None)` if the image was added, but didn't exist previously. + /// - `Err(e)` if the image couldn't be decoded + pub fn add_image, R: Read>(&mut self, _id: S, _data: R, _image_type: ImageType) + -> Result, ImageError> + { + Ok(Some(())) } - pub fn remove_image>(&mut self, _id: S) { - + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn remove_image>(&mut self, _id: S) + -> Option<()> + { + Some(()) } - pub fn add_font, R: Read>(&mut self, _id: S, _data: R) { - + /// Checks if an image is currently registered and ready-to-use + pub fn has_image>(&mut self, _id: S) -> bool { + false } - pub fn remove_font>(&mut self, _id: S) { + /// Add a font (TTF or OTF) to the internal resources + /// + /// ## Returns: + /// + /// - `Ok(Some(()))` if an font with the same ID already exists. + /// - `Ok(None)` if the font was added, but didn't exist previously. + /// - `Err(e)` if the font couldn't be decoded + pub fn add_font, R: Read>(&mut self, _id: S, _data: R) + -> Result, ImageError> + { + Ok(Some(())) + } + pub fn remove_font>(&mut self, _id: S) + -> Option<()> + { + Some(()) } } diff --git a/src/font.rs b/src/font.rs new file mode 100644 index 000000000..783bdc3c2 --- /dev/null +++ b/src/font.rs @@ -0,0 +1,9 @@ +//! Module for loading and handling fonts + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum FontError { + /// Font failed to upload to the GPU + UploadError, + /// Rusttype failed to parse the font + RusttypeError, +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 000000000..922ed7ad6 --- /dev/null +++ b/src/image.rs @@ -0,0 +1,25 @@ +//! Module for loading and handling images + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ImageError { + DecodingFailed, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ImageType { + Bmp, + Gif, + Hdr, + Ico, + Jpeg, + Png, + Ppm, + Pbm, + Pgm, + Pam, + Tga, + Tiff, + WebP, + /// Try to guess the image format, unknown data + GuessImageFormat, +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d68a7dc9c..dfa61bf9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,10 @@ mod id_tree; mod ui_state; /// Dom / CSS caching mod cache; +/// Image handling +mod image; +/// Font handling +mod font; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; @@ -81,4 +85,6 @@ pub mod prelude { pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, WindowCreateError, WindowDecorations, WindowMonitorTarget}; pub use resources::{FontId, FontInstanceId, ImageInstanceId, ImageId}; + pub use font::FontError; + pub use image::{ImageType, ImageError}; } \ No newline at end of file From 170c0a00da0d2f91b1333f9ebad4c8b39d88d6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 15:49:53 +0100 Subject: [PATCH 004/868] Updated webrender --- Cargo.toml | 2 +- examples/debug.rs | 2 +- src/window.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a096aa2b2..8e997ce9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "2083e83d958dd4a230ccae5c518e4bc8fbf88009" } +webrender = { git = "https://github.com/servo/webrender", rev = "30cfecc343e407ce277d07cf09f27ad9dd1917a1" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" diff --git a/examples/debug.rs b/examples/debug.rs index c41f35919..94df3a90c 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -51,7 +51,7 @@ fn main() { }; let mut app = App::new(my_app_data); - + app.add_font("Webly Sleeky UI", TEST_FONT).unwrap(); app.remove_font("Webly Sleeky UI"); diff --git a/src/window.rs b/src/window.rs index 4af02253a..1012b8c25 100644 --- a/src/window.rs +++ b/src/window.rs @@ -489,7 +489,6 @@ impl Window { let opts = RendererOptions { resource_override_path: None, - debug: false, // pre-caching shaders means to compile all shaders on startup // this can take significant time and should be only used for testing the shaders precache_shaders: false, From f31bb817b0ccf24d78848abc34dfcab22c454f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 16:30:00 +0100 Subject: [PATCH 005/868] Added wrapper functions for adding fonts & images to the AppState & App --- src/app.rs | 108 ++++++++++++++++++++++++++++---------------- src/app_state.rs | 56 ++++++++++++++++++++--- src/display_list.rs | 9 +++- src/dom.rs | 1 - src/lib.rs | 6 +-- src/resources.rs | 51 +++++++++++++++------ 6 files changed, 167 insertions(+), 64 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2bf25af0c..c3302c7b4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use css::Css; +use resources::AppResources; use app_state::AppState; use traits::LayoutScreen; use input::hit_test_ui; @@ -75,13 +76,17 @@ impl App { // First repaint, otherwise the window would be black on startup for (idx, window) in self.windows.iter_mut().enumerate() { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, &ui_description_cache[idx], true); + render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &app_state.resources, true); window.display.swap_buffers().unwrap(); } } 'render_loop: loop { + use webrender::api::{DeviceUintSize, WorldPoint, DeviceUintPoint, + DeviceUintRect, LayoutSize, Transaction}; + use dom::UpdateScreen; + let mut closed_windows = Vec::::new(); let time_start = ::std::time::Instant::now(); @@ -109,16 +114,19 @@ impl App { if frame_event_info.should_hittest { - use webrender::api::WorldPoint; - use dom::UpdateScreen; - - let point = WorldPoint::new(frame_event_info.cur_cursor_pos.0 as f32, frame_event_info.cur_cursor_pos.1 as f32); - let hit_test_results = hit_test_ui(&window.internal.api, window.internal.document_id, Some(window.internal.pipeline_id), point); + let cursor_x = frame_event_info.cur_cursor_pos.0 as f32; + let cursor_y = frame_event_info.cur_cursor_pos.1 as f32; + let point = WorldPoint::new(cursor_x, cursor_y); + let hit_test_results = hit_test_ui(&window.internal.api, + window.internal.document_id, + Some(window.internal.pipeline_id), + point); let mut should_update_screen = UpdateScreen::DontRedraw; for item in hit_test_results.items { - if let Some(callback_list) = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0) { + let callback_list_opt = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0); + if let Some(callback_list) = callback_list_opt { // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) // currently, just invoke all actions for callback_id in callback_list.values() { @@ -147,37 +155,43 @@ impl App { frame_event_info.should_redraw_window = true; } + // Macro to avoid duplication between the new_window_size and the new_dpi_factor event + // TODO: refactor this into proper functions (when the WindowState is working) + macro_rules! update_display { + () => ( + let mut txn = Transaction::new(); + let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); + + txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); + window.internal.api.send_transaction(window.internal.document_id, txn); + render(window, ¤t_window_id, &ui_description_cache[idx], &app_state.resources, true); + + let time_end = ::std::time::Instant::now(); + debug_has_repainted = Some(time_end - time_start); + ) + } + if let Some((w, h)) = frame_event_info.new_window_size { - use webrender::api::{DeviceUintSize, DeviceUintPoint, DeviceUintRect, LayoutSize, Transaction}; window.internal.layout_size = LayoutSize::new(w as f32, h as f32); window.internal.framebuffer_size = DeviceUintSize::new(w, h); - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, ¤t_window_id, &ui_description_cache[idx], true); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); + update_display!(); continue; } if let Some(dpi) = frame_event_info.new_dpi_factor { - use webrender::api::{DeviceUintPoint, DeviceUintRect, Transaction}; window.internal.hidpi_factor = dpi; - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, ¤t_window_id, &ui_description_cache[idx], true); - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); + update_display!(); continue; } if frame_event_info.should_redraw_window { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, ¤t_window_id, &ui_description_cache[idx], frame_event_info.new_window_size.is_some()); + render(window, + ¤t_window_id, + &ui_description_cache[idx], + &app_state.resources, + frame_event_info.new_window_size.is_some()); + let time_end = ::std::time::Instant::now(); debug_has_repainted = Some(time_end - time_start); } @@ -204,47 +218,53 @@ impl App { /// Add an image to the internal resources /// - /// ## Returns: + /// ## Returns /// /// - `Ok(Some(()))` if an image with the same ID already exists. /// - `Ok(None)` if the image was added, but didn't exist previously. /// - `Err(e)` if the image couldn't be decoded - pub fn add_image, R: Read>(&mut self, _id: S, _data: R, _image_type: ImageType) + pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) -> Result, ImageError> { - Ok(Some(())) + (*self.app_state.lock().unwrap()).add_image(id, data, image_type) } + /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, _id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { - Some(()) + (*self.app_state.lock().unwrap()).remove_image(id) } /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, _id: S) -> bool { - false + pub fn has_image>(&mut self, id: S) + -> bool + { + (*self.app_state.lock().unwrap()).has_image(id) } /// Add a font (TTF or OTF) to the internal resources /// - /// ## Returns: + /// ## Returns /// /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, _id: S, _data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: R) -> Result, ImageError> { - Ok(Some(())) + (*self.app_state.lock().unwrap()).add_font(id, data) } - pub fn remove_font>(&mut self, _id: S) + /// Removes a font from the internal app resources. + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn remove_font>(&mut self, id: S) -> Option<()> { - Some(()) + (*self.app_state.lock().unwrap()).remove_font(id) } } @@ -292,13 +312,23 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { false } -fn render(window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, has_window_size_changed: bool) +fn render( + window: &mut Window, + _window_id: &WindowId, + ui_description: &UiDescription, + app_resources: &AppResources, + has_window_size_changed: bool) { use webrender::api::*; use display_list::DisplayList; let display_list = DisplayList::new_from_ui_description(ui_description); - let builder = display_list.into_display_list_builder(window.internal.pipeline_id, &mut window.solver, &mut window.css, has_window_size_changed); + let builder = display_list.into_display_list_builder( + window.internal.pipeline_id, + &mut window.solver, + &mut window.css, + app_resources, + has_window_size_changed); if let Some(new_builder) = builder { // only finalize the list if we actually need to. Otherwise just redraw the last display list diff --git a/src/app_state.rs b/src/app_state.rs index c3e7ff5d3..e992565e7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,5 +1,7 @@ use traits::LayoutScreen; -use resources::{AppResources, FontId, ImageId}; +use resources::{AppResources}; +use std::io::Read; +use image::{ImageType, ImageError}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application @@ -8,7 +10,7 @@ pub struct AppState { /// Your data (the global struct which all callbacks will have access to) pub data: T, /// Fonts and images that are currently loaded into the app - resources: AppResources, + pub(crate) resources: AppResources, } impl AppState { @@ -20,13 +22,55 @@ impl AppState { resources: AppResources::default(), } } -/* - pub(crate) fn add_font() -> Result { + /// Add an image to the internal resources + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an image with the same ID already exists. + /// - `Ok(None)` if the image was added, but didn't exist previously. + /// - `Err(e)` if the image couldn't be decoded + pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) + -> Result, ImageError> + { + self.resources.add_image(id, data, image_type) } - pub(crate) fn add_image() -> Result { + /// Removes an image from the internal app resources. + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn remove_image>(&mut self, id: S) + -> Option<()> + { + self.resources.remove_image(id) + } + + /// Checks if an image is currently registered and ready-to-use + pub fn has_image>(&mut self, id: S) + -> bool + { + self.resources.has_image(id) + } + + /// Add a font (TTF or OTF) to the internal resources + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an font with the same ID already exists. + /// - `Ok(None)` if the font was added, but didn't exist previously. + /// - `Err(e)` if the font couldn't be decoded + pub fn add_font, R: Read>(&mut self, id: S, data: R) + -> Result, ImageError> + { + self.resources.add_font(id, data) + } + /// Removes a font from the internal app resources. + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn remove_font>(&mut self, id: S) + -> Option<()> + { + self.resources.remove_font(id) } -*/ } diff --git a/src/display_list.rs b/src/display_list.rs index 2857fcc09..6f76f5988 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -2,6 +2,7 @@ #![allow(unused_macros)] use webrender::api::*; +use resources::AppResources; use traits::LayoutScreen; use constraints::{DisplayRect, CssConstraint}; use ui_description::{UiDescription, StyledNode}; @@ -90,7 +91,13 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { } } - pub fn into_display_list_builder(&self, pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, mut has_window_size_changed: bool) + pub fn into_display_list_builder( + &self, + pipeline_id: PipelineId, + ui_solver: &mut UiSolver, + css: &mut Css, + app_resources: &AppResources, + mut has_window_size_changed: bool) -> Option { let mut changeset = None; diff --git a/src/dom.rs b/src/dom.rs index 8997596f0..8de38e06c 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,5 +1,4 @@ use app_state::AppState; -use resources::{ImageId, FontId}; use traits::LayoutScreen; use std::collections::BTreeMap; use id_tree::{NodeId, Arena}; diff --git a/src/lib.rs b/src/lib.rs index dfa61bf9d..3161099b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub mod traits; /// Window handling pub mod window; /// Font & image resource handling, lookup and caching -pub mod resources; +mod resources; /// Input handling (mostly glium) mod input; /// UI Description & display list handling (webrender) @@ -84,7 +84,7 @@ pub mod prelude { WindowId, WindowPlacement}; pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, WindowCreateError, WindowDecorations, WindowMonitorTarget}; - pub use resources::{FontId, FontInstanceId, ImageInstanceId, ImageId}; pub use font::FontError; pub use image::{ImageType, ImageError}; -} \ No newline at end of file +} + diff --git a/src/resources.rs b/src/resources.rs index 85dcc4d68..82efbe699 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,13 +1,12 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey}; use FastHashMap; +use std::io::Read; +use image::{ImageType, ImageError}; static LAST_FONT_ID: AtomicUsize = AtomicUsize::new(0); static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0); -pub struct ImageInstanceId(usize); -pub struct FontInstanceId(usize); - /// Font and image keys /// /// The idea is that azul doesn't know where the resources come from, @@ -24,16 +23,40 @@ pub(crate) struct AppResources { pub(crate) fonts: FastHashMap, } -/// An `ImageId` is a wrapper around webrenders `ImageKey`. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ImageId(usize); +impl AppResources { + + /// See `AppState::add_image()` + pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) + -> Result, ImageError> + { + Ok(Some(())) + } + + /// See `AppState::remove_image()` + pub fn remove_image>(&mut self, id: S) + -> Option<()> + { + Some(()) + } -/// A Font ID is a wrapper around webrenders `FontKey`. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct FontId(usize); + /// See `AppState::has_image()` + pub fn has_image>(&mut self, id: S) + -> bool + { + false + } -pub fn new_font_id() -> FontId { - let current_font_id = LAST_FONT_ID.load(Ordering::Relaxed); - LAST_FONT_ID.store(current_font_id + 1, Ordering::Relaxed); - FontId(current_font_id) -} \ No newline at end of file + /// See `AppState::add_font()` + pub fn add_font, R: Read>(&mut self, id: S, data: R) + -> Result, ImageError> + { + Ok(Some(())) + } + + /// See `AppState::remove_font()` + pub(crate) fn remove_font>(&mut self, id: S) + -> Option<()> + { + Some(()) + } +} From 8a576d80f2865b8d490a18b13b8d864026ae3bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 17:54:11 +0100 Subject: [PATCH 006/868] Added image uploading to webrender --- Cargo.toml | 1 + src/app.rs | 13 +++-- src/app_state.rs | 13 +++-- src/image.rs | 25 --------- src/images.rs | 40 ++++++++++++++ src/lib.rs | 8 ++- src/resources.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++--- 7 files changed, 193 insertions(+), 48 deletions(-) delete mode 100644 src/image.rs create mode 100644 src/images.rs diff --git a/Cargo.toml b/Cargo.toml index 8e997ce9b..891af8f4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ twox-hash = "1.1.0" glium = "0.20.0" gleam = "0.4.20" euclid = "0.17" +image = "0.18.0" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index c3302c7b4..1d7026118 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,8 @@ use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; use glium::glutin::Event; use euclid::TypedScale; use std::io::Read; -use image::{ImageType, ImageError}; +use images::{ImageType}; +use image::ImageError; use font::FontError; /// Graphical application that maintains some kind of application state @@ -223,7 +224,7 @@ impl App { /// - `Ok(Some(()))` if an image with the same ID already exists. /// - `Ok(None)` if the image was added, but didn't exist previously. /// - `Err(e)` if the image couldn't be decoded - pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { (*self.app_state.lock().unwrap()).add_image(id, data, image_type) @@ -232,14 +233,14 @@ impl App { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).remove_image(id) } /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) + pub fn has_image>(&mut self, id: S) -> bool { (*self.app_state.lock().unwrap()).has_image(id) @@ -252,7 +253,7 @@ impl App { /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: R) -> Result, ImageError> { (*self.app_state.lock().unwrap()).add_font(id, data) @@ -261,7 +262,7 @@ impl App { /// Removes a font from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_font>(&mut self, id: S) + pub fn remove_font>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).remove_font(id) diff --git a/src/app_state.rs b/src/app_state.rs index e992565e7..a3788037d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,7 +1,8 @@ use traits::LayoutScreen; use resources::{AppResources}; use std::io::Read; -use image::{ImageType, ImageError}; +use images::ImageType; +use image::ImageError; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application @@ -30,7 +31,7 @@ impl AppState { /// - `Ok(Some(()))` if an image with the same ID already exists. /// - `Ok(None)` if the image was added, but didn't exist previously. /// - `Err(e)` if the image couldn't be decoded - pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { self.resources.add_image(id, data, image_type) @@ -39,14 +40,14 @@ impl AppState { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { self.resources.remove_image(id) } /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) + pub fn has_image>(&mut self, id: S) -> bool { self.resources.has_image(id) @@ -59,7 +60,7 @@ impl AppState { /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: R) -> Result, ImageError> { self.resources.add_font(id, data) @@ -68,7 +69,7 @@ impl AppState { /// Removes a font from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_font>(&mut self, id: S) + pub fn remove_font>(&mut self, id: S) -> Option<()> { self.resources.remove_font(id) diff --git a/src/image.rs b/src/image.rs deleted file mode 100644 index 922ed7ad6..000000000 --- a/src/image.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Module for loading and handling images - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum ImageError { - DecodingFailed, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum ImageType { - Bmp, - Gif, - Hdr, - Ico, - Jpeg, - Png, - Ppm, - Pbm, - Pgm, - Pam, - Tga, - Tiff, - WebP, - /// Try to guess the image format, unknown data - GuessImageFormat, -} \ No newline at end of file diff --git a/src/images.rs b/src/images.rs new file mode 100644 index 000000000..13f7a905a --- /dev/null +++ b/src/images.rs @@ -0,0 +1,40 @@ +//! Module for loading and handling images + +use image::{ImageResult, ImageFormat, guess_format}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ImageType { + Bmp, + Gif, + Hdr, + Ico, + Jpeg, + Png, + Pnm, + Tga, + Tiff, + WebP, + /// Try to guess the image format, unknown data + GuessImageFormat, +} + +impl ImageType { + pub(crate) fn into_image_format(&self, data: &[u8]) -> ImageResult { + use self::ImageType::*; + match *self { + Bmp => Ok(ImageFormat::BMP), + Gif => Ok(ImageFormat::GIF), + Hdr => Ok(ImageFormat::HDR), + Ico => Ok(ImageFormat::ICO), + Jpeg => Ok(ImageFormat::JPEG), + Png => Ok(ImageFormat::PNG), + Pnm => Ok(ImageFormat::PNM), + Tga => Ok(ImageFormat::TGA), + Tiff => Ok(ImageFormat::TIFF), + WebP => Ok(ImageFormat::WEBP), + GuessImageFormat => { + guess_format(data) + } + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3161099b7..085223b6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ extern crate glium; extern crate gleam; extern crate euclid; extern crate simplecss; +extern crate image; /// Global application (Initialization starts here) pub mod app; @@ -64,7 +65,7 @@ mod ui_state; /// Dom / CSS caching mod cache; /// Image handling -mod image; +mod images; /// Font handling mod font; @@ -85,6 +86,9 @@ pub mod prelude { pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, WindowCreateError, WindowDecorations, WindowMonitorTarget}; pub use font::FontError; - pub use image::{ImageType, ImageError}; + pub use images::ImageType; + + // from the extern crate image + pub use image::ImageError; } diff --git a/src/resources.rs b/src/resources.rs index 82efbe699..bcd44fcc2 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -2,7 +2,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey}; use FastHashMap; use std::io::Read; -use image::{ImageType, ImageError}; +use images::ImageType; +use image::{self, ImageError, DynamicImage, GenericImage}; +use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; static LAST_FONT_ID: AtomicUsize = AtomicUsize::new(0); static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0); @@ -19,44 +21,165 @@ static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0); /// (not yet tested, but should work). #[derive(Debug, Default, Clone)] pub(crate) struct AppResources { - pub(crate) images: FastHashMap, - pub(crate) fonts: FastHashMap, + pub(crate) images: FastHashMap, + pub(crate) fonts: FastHashMap>, } +#[derive(Debug, Clone)] +pub(crate) enum ImageState { + // resource is available for the renderer + Uploaded(ImageKey), + // image is loaded & decoded, but not yet available + ReadyForUpload((ImageData, ImageDescriptor)), +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub(crate) struct FontSize(pub(crate) usize); + impl AppResources { /// See `AppState::add_image()` - pub fn add_image, R: Read>(&mut self, id: S, data: R, image_type: ImageType) + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { - Ok(Some(())) + use std::collections::hash_map::Entry::*; + + match self.images.entry(id.into()) { + Occupied(_) => Ok(None), + Vacant(v) => { + let mut image_data = Vec::::new(); + data.read_to_end(&mut image_data).map_err(|e| ImageError::IoError(e))?; + let image_format = image_type.into_image_format(&image_data)?; + let decoded = image::load_from_memory_with_format(&image_data, image_format)?; + v.insert(ImageState::ReadyForUpload(prepare_image(decoded)?)); + Ok(Some(())) + }, + } } /// See `AppState::remove_image()` - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { Some(()) } /// See `AppState::has_image()` - pub fn has_image>(&mut self, id: S) + pub fn has_image>(&mut self, id: S) -> bool { false } /// See `AppState::add_font()` - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: R) -> Result, ImageError> { Ok(Some(())) } /// See `AppState::remove_font()` - pub(crate) fn remove_font>(&mut self, id: S) + pub(crate) fn remove_font>(&mut self, id: S) -> Option<()> { Some(()) } } + +// The next three functions are taken from: +// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs + +use std::path::Path; + +/// Convenience function to get the image type from a path +/// +/// This function looks at the extension of the image. However, this +/// extension could be wrong, i.e. a user labeling a PNG as a JPG and so on. +/// If you don't know the format of the image, simply use Image::GuessImageType +/// - which will guess the type of the image from the magic header in the +/// actual image data. +pub fn get_image_type_from_extension(path: &Path) -> Option { + let ext = path.extension().and_then(|s| s.to_str()) + .map_or(String::new(), |s| s.to_ascii_lowercase()); + + match &ext[..] { + "jpg" | + "jpeg" => Some(ImageType::Jpeg), + "png" => Some(ImageType::Png), + "gif" => Some(ImageType::Gif), + "webp" => Some(ImageType::WebP), + "tif" | + "tiff" => Some(ImageType::Tiff), + "tga" => Some(ImageType::Tga), + "bmp" => Some(ImageType::Bmp), + "ico" => Some(ImageType::Ico), + "hdr" => Some(ImageType::Hdr), + "pbm" | + "pam" | + "ppm" | + "pgm" => Some(ImageType::Pnm), + _ => None, + } +} + +fn prepare_image(mut image_decoded: DynamicImage) + -> Result<(ImageData, ImageDescriptor), ImageError> +{ + let image_dims = image_decoded.dimensions(); + let format = match image_decoded { + image::ImageLuma8(_) => ImageFormat::R8, + image::ImageLumaA8(_) => { + image_decoded = DynamicImage::ImageLuma8(image_decoded.to_luma()); + ImageFormat::R8 + }, + image::ImageRgba8(_) => ImageFormat::BGRA8, + image::ImageRgb8(_) => { + image_decoded = DynamicImage::ImageRgba8(image_decoded.to_rgba()); + ImageFormat::BGRA8 + }, + }; + + let mut bytes = image_decoded.raw_pixels(); + if format == ImageFormat::BGRA8 { + premultiply(bytes.as_mut_slice()); + } + + let opaque = is_image_opaque(format, &bytes[..]); + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); + let data = ImageData::new(bytes); + Ok((data, descriptor)) +} + +fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool { + match format { + ImageFormat::BGRA8 => { + let mut is_opaque = true; + for i in 0..(bytes.len() / 4) { + if bytes[i * 4 + 3] != 255 { + is_opaque = false; + break; + } + } + is_opaque + } + ImageFormat::R8 => true, + _ => unreachable!(), + } +} + +// From webrender/wrench +// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +pub fn premultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = u32::from(pixel[3]); + let r = u32::from(pixel[2]); + let g = u32::from(pixel[1]); + let b = u32::from(pixel[0]); + + pixel[3] = a as u8; + pixel[2] = ((r * a + 128) / 255) as u8; + pixel[1] = ((g * a + 128) / 255) as u8; + pixel[0] = ((b * a + 128) / 255) as u8; + } +} \ No newline at end of file From a8a1a3e7bcb3ef13014123e8359beaa0e06d3064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 17:56:46 +0100 Subject: [PATCH 007/868] Fixed examples to use the new image uploading API --- examples/debug.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 94df3a90c..63307aeb2 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,7 +1,6 @@ extern crate azul; use azul::prelude::*; -use std::collections::HashMap; const TEST_CSS: &str = include_str!("test_content.css"); const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); @@ -11,8 +10,6 @@ const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); pub struct MyAppData { // Your app data goes here pub my_data: u32, - pub fonts: HashMap, - pub images: HashMap, } impl LayoutScreen for MyAppData { @@ -41,13 +38,8 @@ fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen fn main() { let css = Css::new_from_string(TEST_CSS).unwrap(); - let mut fonts = HashMap::new(); - fonts.insert("Roboto".to_string(), azul::resources::new_font_id()); - let my_app_data = MyAppData { my_data: 0, - fonts: fonts, - images: HashMap::new(), }; let mut app = App::new(my_app_data); @@ -55,7 +47,7 @@ fn main() { app.add_font("Webly Sleeky UI", TEST_FONT).unwrap(); app.remove_font("Webly Sleeky UI"); - app.add_image("MyImage", TEST_IMAGE, ImageType::Jpeg).unwrap(); + app.add_image("MyImage", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); app.remove_image("MyImage"); // TODO: Multi-window apps currently crash From fb3fcc05b6b22aa76de33c06e6faf79dcb36c212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 18:03:32 +0100 Subject: [PATCH 008/868] Refactored image loading into images.rs --- examples/debug.rs | 2 +- src/app.rs | 2 +- src/app_state.rs | 2 +- src/images.rs | 101 +++++++++++++++++++++++++++++++++++++++++++++ src/resources.rs | 103 ++-------------------------------------------- 5 files changed, 107 insertions(+), 103 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 63307aeb2..036e7d6ac 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -44,7 +44,7 @@ fn main() { let mut app = App::new(my_app_data); - app.add_font("Webly Sleeky UI", TEST_FONT).unwrap(); + app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); app.remove_font("Webly Sleeky UI"); app.add_image("MyImage", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); diff --git a/src/app.rs b/src/app.rs index 1d7026118..8f69460f5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -253,7 +253,7 @@ impl App { /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, ImageError> { (*self.app_state.lock().unwrap()).add_font(id, data) diff --git a/src/app_state.rs b/src/app_state.rs index a3788037d..70012ec02 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -60,7 +60,7 @@ impl AppState { /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, ImageError> { self.resources.add_font(id, data) diff --git a/src/images.rs b/src/images.rs index 13f7a905a..6d6c3d646 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,6 +1,9 @@ //! Module for loading and handling images +use webrender::api::ImageFormat as WebrenderImageFormat; use image::{ImageResult, ImageFormat, guess_format}; +use image::{self, ImageError, DynamicImage, GenericImage}; +use webrender::api::{ImageData, ImageDescriptor}; #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ImageType { @@ -37,4 +40,102 @@ impl ImageType { } } } +} + +// The next three functions are taken from: +// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs + +use std::path::Path; + +/// Convenience function to get the image type from a path +/// +/// This function looks at the extension of the image. However, this +/// extension could be wrong, i.e. a user labeling a PNG as a JPG and so on. +/// If you don't know the format of the image, simply use Image::GuessImageType +/// - which will guess the type of the image from the magic header in the +/// actual image data. +pub fn get_image_type_from_extension(path: &Path) -> Option { + let ext = path.extension().and_then(|s| s.to_str()) + .map_or(String::new(), |s| s.to_ascii_lowercase()); + + match &ext[..] { + "jpg" | + "jpeg" => Some(ImageType::Jpeg), + "png" => Some(ImageType::Png), + "gif" => Some(ImageType::Gif), + "webp" => Some(ImageType::WebP), + "tif" | + "tiff" => Some(ImageType::Tiff), + "tga" => Some(ImageType::Tga), + "bmp" => Some(ImageType::Bmp), + "ico" => Some(ImageType::Ico), + "hdr" => Some(ImageType::Hdr), + "pbm" | + "pam" | + "ppm" | + "pgm" => Some(ImageType::Pnm), + _ => None, + } +} + +pub(crate) fn prepare_image(mut image_decoded: DynamicImage) + -> Result<(ImageData, ImageDescriptor), ImageError> +{ + let image_dims = image_decoded.dimensions(); + let format = match image_decoded { + image::ImageLuma8(_) => WebrenderImageFormat::R8, + image::ImageLumaA8(_) => { + image_decoded = DynamicImage::ImageLuma8(image_decoded.to_luma()); + WebrenderImageFormat::R8 + }, + image::ImageRgba8(_) => WebrenderImageFormat::BGRA8, + image::ImageRgb8(_) => { + image_decoded = DynamicImage::ImageRgba8(image_decoded.to_rgba()); + WebrenderImageFormat::BGRA8 + }, + }; + + let mut bytes = image_decoded.raw_pixels(); + if format == WebrenderImageFormat::BGRA8 { + premultiply(bytes.as_mut_slice()); + } + + let opaque = is_image_opaque(format, &bytes[..]); + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); + let data = ImageData::new(bytes); + Ok((data, descriptor)) +} + +pub(crate) fn is_image_opaque(format: WebrenderImageFormat, bytes: &[u8]) -> bool { + match format { + WebrenderImageFormat::BGRA8 => { + let mut is_opaque = true; + for i in 0..(bytes.len() / 4) { + if bytes[i * 4 + 3] != 255 { + is_opaque = false; + break; + } + } + is_opaque + } + WebrenderImageFormat::R8 => true, + _ => unreachable!(), + } +} + +// From webrender/wrench +// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +pub(crate) fn premultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = u32::from(pixel[3]); + let r = u32::from(pixel[2]); + let g = u32::from(pixel[1]); + let b = u32::from(pixel[0]); + + pixel[3] = a as u8; + pixel[2] = ((r * a + 128) / 255) as u8; + pixel[1] = ((g * a + 128) / 255) as u8; + pixel[0] = ((b * a + 128) / 255) as u8; + } } \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index bcd44fcc2..8c1f99d53 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -43,6 +43,7 @@ impl AppResources { -> Result, ImageError> { use std::collections::hash_map::Entry::*; + use images; // the module, not the crate! match self.images.entry(id.into()) { Occupied(_) => Ok(None), @@ -51,7 +52,7 @@ impl AppResources { data.read_to_end(&mut image_data).map_err(|e| ImageError::IoError(e))?; let image_format = image_type.into_image_format(&image_data)?; let decoded = image::load_from_memory_with_format(&image_data, image_format)?; - v.insert(ImageState::ReadyForUpload(prepare_image(decoded)?)); + v.insert(ImageState::ReadyForUpload(images::prepare_image(decoded)?)); Ok(Some(())) }, } @@ -72,7 +73,7 @@ impl AppResources { } /// See `AppState::add_font()` - pub fn add_font, R: Read>(&mut self, id: S, data: R) + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, ImageError> { Ok(Some(())) @@ -84,102 +85,4 @@ impl AppResources { { Some(()) } -} - -// The next three functions are taken from: -// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs - -use std::path::Path; - -/// Convenience function to get the image type from a path -/// -/// This function looks at the extension of the image. However, this -/// extension could be wrong, i.e. a user labeling a PNG as a JPG and so on. -/// If you don't know the format of the image, simply use Image::GuessImageType -/// - which will guess the type of the image from the magic header in the -/// actual image data. -pub fn get_image_type_from_extension(path: &Path) -> Option { - let ext = path.extension().and_then(|s| s.to_str()) - .map_or(String::new(), |s| s.to_ascii_lowercase()); - - match &ext[..] { - "jpg" | - "jpeg" => Some(ImageType::Jpeg), - "png" => Some(ImageType::Png), - "gif" => Some(ImageType::Gif), - "webp" => Some(ImageType::WebP), - "tif" | - "tiff" => Some(ImageType::Tiff), - "tga" => Some(ImageType::Tga), - "bmp" => Some(ImageType::Bmp), - "ico" => Some(ImageType::Ico), - "hdr" => Some(ImageType::Hdr), - "pbm" | - "pam" | - "ppm" | - "pgm" => Some(ImageType::Pnm), - _ => None, - } -} - -fn prepare_image(mut image_decoded: DynamicImage) - -> Result<(ImageData, ImageDescriptor), ImageError> -{ - let image_dims = image_decoded.dimensions(); - let format = match image_decoded { - image::ImageLuma8(_) => ImageFormat::R8, - image::ImageLumaA8(_) => { - image_decoded = DynamicImage::ImageLuma8(image_decoded.to_luma()); - ImageFormat::R8 - }, - image::ImageRgba8(_) => ImageFormat::BGRA8, - image::ImageRgb8(_) => { - image_decoded = DynamicImage::ImageRgba8(image_decoded.to_rgba()); - ImageFormat::BGRA8 - }, - }; - - let mut bytes = image_decoded.raw_pixels(); - if format == ImageFormat::BGRA8 { - premultiply(bytes.as_mut_slice()); - } - - let opaque = is_image_opaque(format, &bytes[..]); - let allow_mipmaps = true; - let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); - let data = ImageData::new(bytes); - Ok((data, descriptor)) -} - -fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool { - match format { - ImageFormat::BGRA8 => { - let mut is_opaque = true; - for i in 0..(bytes.len() / 4) { - if bytes[i * 4 + 3] != 255 { - is_opaque = false; - break; - } - } - is_opaque - } - ImageFormat::R8 => true, - _ => unreachable!(), - } -} - -// From webrender/wrench -// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions -pub fn premultiply(data: &mut [u8]) { - for pixel in data.chunks_mut(4) { - let a = u32::from(pixel[3]); - let r = u32::from(pixel[2]); - let g = u32::from(pixel[1]); - let b = u32::from(pixel[0]); - - pixel[3] = a as u8; - pixel[2] = ((r * a + 128) / 255) as u8; - pixel[1] = ((g * a + 128) / 255) as u8; - pixel[0] = ((b * a + 128) / 255) as u8; - } } \ No newline at end of file From d5688ca9316ae06279f504af5e41b5d36265f784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 22 Mar 2018 19:06:25 +0100 Subject: [PATCH 009/868] Implement image rendering --- examples/debug.rs | 8 +++--- examples/test_content.css | 4 +-- src/app.rs | 24 ++++++++++++------ src/display_list.rs | 53 +++++++++++++++++++++++++++++++++++++-- src/images.rs | 17 ++++++++++++- src/resources.rs | 13 +--------- 6 files changed, 91 insertions(+), 28 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 036e7d6ac..742872163 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -43,12 +43,12 @@ fn main() { }; let mut app = App::new(my_app_data); - +/* app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); app.remove_font("Webly Sleeky UI"); - - app.add_image("MyImage", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); - app.remove_image("MyImage"); +*/ + app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); + // app.remove_image("MyImage"); // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that diff --git a/examples/test_content.css b/examples/test_content.css index 354f0e664..dc8261280 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -5,7 +5,8 @@ border-radius: 4px; /*box-shadow: 0px 0px 3px #c5c5c5ad;*/ box-shadow: 0px 0px 3px red; - background: linear-gradient(#fcfcfc, #efefef); + background: image("Cat01"); + /*background: linear-gradient(#fcfcfc, #efefef);*/ width: 200px; height: 200px; min-height: 400px; @@ -20,5 +21,4 @@ * { font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; - background: image("Cat01"); } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 8f69460f5..11ea38680 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use std::io::Read; use images::{ImageType}; use image::ImageError; use font::FontError; +use webrender::api::RenderApi; /// Graphical application that maintains some kind of application state pub struct App { @@ -69,7 +70,7 @@ impl App { // first redraw, initialize cache { - let app_state = self.app_state.lock().unwrap(); + let mut app_state = self.app_state.lock().unwrap(); for (idx, _) in self.windows.iter().enumerate() { ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); } @@ -77,7 +78,10 @@ impl App { // First repaint, otherwise the window would be black on startup for (idx, window) in self.windows.iter_mut().enumerate() { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &app_state.resources, true); + render(window, &WindowId { id: idx, }, + &ui_description_cache[idx], + &mut app_state.resources, + true); window.display.swap_buffers().unwrap(); } } @@ -146,7 +150,6 @@ impl App { if should_update_screen == UpdateScreen::Redraw { frame_event_info.should_redraw_window = true; } - } let mut app_state = self.app_state.lock().unwrap(); @@ -165,7 +168,11 @@ impl App { txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, ¤t_window_id, &ui_description_cache[idx], &app_state.resources, true); + render(window, + ¤t_window_id, + &ui_description_cache[idx], + &mut app_state.resources, + true); let time_end = ::std::time::Instant::now(); debug_has_repainted = Some(time_end - time_start); @@ -190,7 +197,7 @@ impl App { render(window, ¤t_window_id, &ui_description_cache[idx], - &app_state.resources, + &mut app_state.resources, frame_event_info.new_window_size.is_some()); let time_end = ::std::time::Instant::now(); @@ -317,7 +324,7 @@ fn render( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, - app_resources: &AppResources, + app_resources: &mut AppResources, has_window_size_changed: bool) { use webrender::api::*; @@ -329,6 +336,7 @@ fn render( &mut window.solver, &mut window.css, app_resources, + &window.internal.api, has_window_size_changed); if let Some(new_builder) = builder { @@ -344,7 +352,9 @@ fn render( window.internal.epoch, None, window.internal.layout_size, - (window.internal.pipeline_id, window.solver.window_dimensions.layout_size, window.internal.last_display_list_builder.clone()), + (window.internal.pipeline_id, + window.solver.window_dimensions.layout_size, + window.internal.last_display_list_builder.clone()), true, ); diff --git a/src/display_list.rs b/src/display_list.rs index 6f76f5988..80b1b2d24 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -91,12 +91,44 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { } } + /// Looks if any new images need to be uploaded and stores the in the image resources + fn update_resources(api: &RenderApi, app_resources: &mut AppResources) { + use images::{ImageState, ImageInfo}; + + let mut resources = ResourceUpdates::new(); + + let mut updated_images = Vec::<(String, (ImageData, ImageDescriptor))>::new(); + + // possible performance bottleneck (duplicated cloning) !! + for (key, value) in app_resources.images.iter() { + match *value { + ImageState::ReadyForUpload(ref d) => { + updated_images.push((key.clone(), d.clone())); + }, + ImageState::Uploaded(_) => { }, + } + } + + for (resource_key, (data, descriptor)) in updated_images.into_iter() { + let key = api.generate_image_key(); + resources.add_image(key, descriptor, data, None); + *app_resources.images.get_mut(&resource_key).unwrap() = + ImageState::Uploaded(ImageInfo { + key: key, + descriptor: descriptor + }); + } + + api.update_resources(resources); + } + pub fn into_display_list_builder( &self, pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, - app_resources: &AppResources, + app_resources: &mut AppResources, + render_api: &RenderApi, mut has_window_size_changed: bool) -> Option { @@ -147,6 +179,9 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { let mut builder = DisplayListBuilder::with_capacity(pipeline_id, ui_solver.window_dimensions.layout_size, self.rectangles.len()); + // Upload image and font resources + Self::update_resources(render_api, app_resources); + for (rect_idx, rect) in self.rectangles.iter() { // ask the solver what the bounds of the current rectangle is @@ -233,7 +268,21 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, Background::Image(image_id) => { - // TODO: lookup image in resources + if let Some(image_info) = app_resources.images.get(image_id.0) { + use images::ImageState::*; + match *image_info { + Uploaded(ref image_info) => { + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + image_info.key); + }, + ReadyForUpload(_) => { }, + } + } } } } diff --git a/src/images.rs b/src/images.rs index 6d6c3d646..a4795f1ca 100644 --- a/src/images.rs +++ b/src/images.rs @@ -3,7 +3,7 @@ use webrender::api::ImageFormat as WebrenderImageFormat; use image::{ImageResult, ImageFormat, guess_format}; use image::{self, ImageError, DynamicImage, GenericImage}; -use webrender::api::{ImageData, ImageDescriptor}; +use webrender::api::{ImageData, ImageDescriptor, ImageKey}; #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ImageType { @@ -21,6 +21,21 @@ pub enum ImageType { GuessImageFormat, } +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ImageInfo { + pub(crate) key: ImageKey, + pub(crate) descriptor: ImageDescriptor, +} + +#[derive(Debug, Clone)] +pub(crate) enum ImageState { + // resource is available for the renderer + Uploaded(ImageInfo), + // image is loaded & decoded, but not yet available + ReadyForUpload((ImageData, ImageDescriptor)), +} + + impl ImageType { pub(crate) fn into_image_format(&self, data: &[u8]) -> ImageResult { use self::ImageType::*; diff --git a/src/resources.rs b/src/resources.rs index 8c1f99d53..2eb43b005 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -2,13 +2,10 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey}; use FastHashMap; use std::io::Read; -use images::ImageType; +use images::{ImageState, ImageType}; use image::{self, ImageError, DynamicImage, GenericImage}; use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; -static LAST_FONT_ID: AtomicUsize = AtomicUsize::new(0); -static LAST_IMAGE_ID: AtomicUsize = AtomicUsize::new(0); - /// Font and image keys /// /// The idea is that azul doesn't know where the resources come from, @@ -25,14 +22,6 @@ pub(crate) struct AppResources { pub(crate) fonts: FastHashMap>, } -#[derive(Debug, Clone)] -pub(crate) enum ImageState { - // resource is available for the renderer - Uploaded(ImageKey), - // image is loaded & decoded, but not yet available - ReadyForUpload((ImageData, ImageDescriptor)), -} - #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub(crate) struct FontSize(pub(crate) usize); From 310934d365623f93660f3554605fa2cbe4e80212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 23 Mar 2018 21:46:34 +0100 Subject: [PATCH 010/868] Fixed image rendering for RGBA and greyscale-alpha images --- src/images.rs | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/images.rs b/src/images.rs index a4795f1ca..65a2bbf9b 100644 --- a/src/images.rs +++ b/src/images.rs @@ -93,24 +93,49 @@ pub fn get_image_type_from_extension(path: &Path) -> Option { } } -pub(crate) fn prepare_image(mut image_decoded: DynamicImage) +pub(crate) fn prepare_image(image_decoded: DynamicImage) -> Result<(ImageData, ImageDescriptor), ImageError> { let image_dims = image_decoded.dimensions(); - let format = match image_decoded { - image::ImageLuma8(_) => WebrenderImageFormat::R8, + + // see: https://github.com/servo/webrender/blob/80c614ab660bf6cca52594d0e33a0be262a7ac12/wrench/src/yaml_frame_reader.rs#L401-L427 + let (format, mut bytes) = match image_decoded { + image::ImageLuma8(_) => { + (WebrenderImageFormat::R8, image_decoded.raw_pixels()) + }, image::ImageLumaA8(_) => { - image_decoded = DynamicImage::ImageLuma8(image_decoded.to_luma()); - WebrenderImageFormat::R8 + let bytes = image_decoded.raw_pixels(); + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for greyscale_alpha in bytes.chunks(2) { + pixels.extend_from_slice(&[ + greyscale_alpha[0], + greyscale_alpha[0], + greyscale_alpha[0], + greyscale_alpha[1] + ]); + } + (WebrenderImageFormat::BGRA8, pixels) }, - image::ImageRgba8(_) => WebrenderImageFormat::BGRA8, - image::ImageRgb8(_) => { - image_decoded = DynamicImage::ImageRgba8(image_decoded.to_rgba()); - WebrenderImageFormat::BGRA8 + image::ImageRgba8(_) => { + let mut pixels = image_decoded.raw_pixels(); + premultiply(pixels.as_mut_slice()); + (WebrenderImageFormat::BGRA8, pixels) }, + image::ImageRgb8(_) => { + let bytes = image_decoded.raw_pixels(); + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for bgr in bytes.chunks(3) { + pixels.extend_from_slice(&[ + bgr[2], + bgr[1], + bgr[0], + 0xff + ]); + } + (WebrenderImageFormat::BGRA8, pixels) + } }; - let mut bytes = image_decoded.raw_pixels(); if format == WebrenderImageFormat::BGRA8 { premultiply(bytes.as_mut_slice()); } From 1ef2475009eb6dbc8026464205517aceb394830e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 23 Mar 2018 22:25:40 +0100 Subject: [PATCH 011/868] Fixed wrong pre-multiplication of RGBA images + added OSMesa software rendering --- examples/debug.rs | 4 +- src/css_parser.rs | 697 +++++++++++++++++++++++----------------------- src/id_tree.rs | 117 ++++---- src/images.rs | 9 +- src/window.rs | 64 +++-- 5 files changed, 458 insertions(+), 433 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 742872163..71535d768 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -36,6 +36,7 @@ fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen } fn main() { + let css = Css::new_from_string(TEST_CSS).unwrap(); let my_app_data = MyAppData { @@ -43,8 +44,9 @@ fn main() { }; let mut app = App::new(my_app_data); -/* + app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); +/* app.remove_font("Webly Sleeky UI"); */ app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); diff --git a/src/css_parser.rs b/src/css_parser.rs index 420b3aae2..4aa5c4eb6 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1372,385 +1372,390 @@ multi_type_parser!(parse_shape, Shape, ["circle", Circle], ["ellipse", Ellipse]); -#[test] -fn test_parse_box_shadow_1() { - assert_eq!(parse_css_box_shadow("none"), Ok(None)); -} - -#[test] -fn test_parse_box_shadow_2() { - assert_eq!(parse_css_box_shadow("5px 10px"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} - -#[test] -fn test_parse_box_shadow_3() { - assert_eq!(parse_css_box_shadow("5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} +#[cfg(test)] +mod css_tests { + use super::*; + #[test] + fn test_parse_box_shadow_1() { + assert_eq!(parse_css_box_shadow("none"), Ok(None)); + } -#[test] -fn test_parse_box_shadow_4() { - assert_eq!(parse_css_box_shadow("5px 10px inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} + #[test] + fn test_parse_box_shadow_2() { + assert_eq!(parse_css_box_shadow("5px 10px"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + blur_radius: 0.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Outset, + }))); + } -#[test] -fn test_parse_box_shadow_5() { - assert_eq!(parse_css_box_shadow("5px 10px outset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} + #[test] + fn test_parse_box_shadow_3() { + assert_eq!(parse_css_box_shadow("5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 0.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Outset, + }))); + } -#[test] -fn test_parse_box_shadow_6() { - assert_eq!(parse_css_box_shadow("5px 10px 5px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} + #[test] + fn test_parse_box_shadow_4() { + assert_eq!(parse_css_box_shadow("5px 10px inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + blur_radius: 0.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Inset, + }))); + } -#[test] -fn test_parse_box_shadow_7() { - assert_eq!(parse_css_box_shadow("5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 0.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} + #[test] + fn test_parse_box_shadow_5() { + assert_eq!(parse_css_box_shadow("5px 10px outset"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + blur_radius: 0.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Outset, + }))); + } -#[test] -fn test_parse_box_shadow_8() { - assert_eq!(parse_css_box_shadow("5px 10px 5px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 0.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} + #[test] + fn test_parse_box_shadow_6() { + assert_eq!(parse_css_box_shadow("5px 10px 5px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 5.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Outset, + }))); + } -#[test] -fn test_parse_box_shadow_9() { - assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 10.0, - clip_mode: BoxShadowClipMode::Outset, - }))); -} + #[test] + fn test_parse_box_shadow_7() { + assert_eq!(parse_css_box_shadow("5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 0.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Inset, + }))); + } -#[test] -fn test_parse_box_shadow_10() { - assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { - offset: LayoutVector2D::new(5.0, 10.0), - color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, - blur_radius: 5.0, - spread_radius: 10.0, - clip_mode: BoxShadowClipMode::Inset, - }))); -} + #[test] + fn test_parse_box_shadow_8() { + assert_eq!(parse_css_box_shadow("5px 10px 5px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 5.0, + spread_radius: 0.0, + clip_mode: BoxShadowClipMode::Inset, + }))); + } -#[test] -fn test_parse_css_border_1() { - assert_eq!(parse_css_border("5px solid red"), Ok((BorderWidths { - top: 5.0, - bottom: 5.0, - left: 5.0, - right: 5.0, - }, BorderDetails::Normal(NormalBorder { - left: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - right: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - bottom: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - top: BorderSide { - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Solid, - }, - radius: BorderRadius::zero(), - })))); -} + #[test] + fn test_parse_box_shadow_9() { + assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 5.0, + spread_radius: 10.0, + clip_mode: BoxShadowClipMode::Outset, + }))); + } -#[test] -fn test_parse_css_border_2() { - assert_eq!(parse_css_border("double"), Ok((BorderWidths { - top: 1.0, - bottom: 1.0, - left: 1.0, - right: 1.0, - }, BorderDetails::Normal(NormalBorder { - left: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - right: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - bottom: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - top: BorderSide { - color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - style: BorderStyle::Double, - }, - radius: BorderRadius::zero(), - })))); -} + #[test] + fn test_parse_box_shadow_10() { + assert_eq!(parse_css_box_shadow("5px 10px 5px 10px #888888 inset"), Ok(Some(BoxShadowPreDisplayItem { + offset: LayoutVector2D::new(5.0, 10.0), + color: ColorF { r: 0.53333336, g: 0.53333336, b: 0.53333336, a: 1.0 }, + blur_radius: 5.0, + spread_radius: 10.0, + clip_mode: BoxShadowClipMode::Inset, + }))); + } -#[test] -fn test_parse_linear_gradient_1() { - assert_eq!(parse_css_background("linear-gradient(red, yellow)"), - Ok(Background::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), + #[test] + fn test_parse_css_border_1() { + assert_eq!(parse_css_border("5px solid red"), Ok((BorderWidths { + top: 5.0, + bottom: 5.0, + left: 5.0, + right: 5.0, + }, BorderDetails::Normal(NormalBorder { + left: BorderSide { color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Solid, }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_linear_gradient_2() { - assert_eq!(parse_css_background("linear-gradient(red, lime, blue, yellow)"), - Ok(Background::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), + right: BorderSide { color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Solid, }, - GradientStopPre { - offset: Some(0.33333334), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + bottom: BorderSide { + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Solid, }, - GradientStopPre { - offset: Some(0.66666667), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + top: BorderSide { + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Solid, }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} + radius: BorderRadius::zero(), + })))); + } -#[test] -fn test_parse_linear_gradient_3() { - assert_eq!(parse_css_background("repeating-linear-gradient(50deg, blue, yellow, #00FF00)"), - Ok(Background::LinearGradient(LinearGradientPreInfo { - direction: Direction::Angle(50.0), - extend_mode: ExtendMode::Repeat, - stops: vec![ - GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + #[test] + fn test_parse_css_border_2() { + assert_eq!(parse_css_border("double"), Ok((BorderWidths { + top: 1.0, + bottom: 1.0, + left: 1.0, + right: 1.0, + }, BorderDetails::Normal(NormalBorder { + left: BorderSide { + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Double, }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + right: BorderSide { + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Double, }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} - -#[test] -fn test_parse_linear_gradient_4() { - assert_eq!(parse_css_background("linear-gradient(to bottom right, red, yellow)"), - Ok(Background::LinearGradient(LinearGradientPreInfo { - direction: Direction::FromTo(DirectionCorner::TopLeft, DirectionCorner::BottomRight), - extend_mode: ExtendMode::Clamp, - stops: vec![GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + bottom: BorderSide { + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Double, }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], + top: BorderSide { + color: ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + style: BorderStyle::Double, + }, + radius: BorderRadius::zero(), + })))); + } + + #[test] + fn test_parse_linear_gradient_1() { + assert_eq!(parse_css_background("linear-gradient(red, yellow)"), + Ok(Background::LinearGradient(LinearGradientPreInfo { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(0.0), + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } + + #[test] + fn test_parse_linear_gradient_2() { + assert_eq!(parse_css_background("linear-gradient(red, lime, blue, yellow)"), + Ok(Background::LinearGradient(LinearGradientPreInfo { + direction: Direction::FromTo(DirectionCorner::Top, DirectionCorner::Bottom), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(0.0), + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.33333334), + color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.66666667), + color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], }))); -} + } -#[test] -fn test_parse_radial_gradient_1() { - assert_eq!(parse_css_background("radial-gradient(circle, lime, blue, yellow)"), - Ok(Background::RadialGradient(RadialGradientPreInfo { - shape: Shape::Circle, - extend_mode: ExtendMode::Clamp, - stops: vec![ - GradientStopPre { - offset: Some(0.0), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} + #[test] + fn test_parse_linear_gradient_3() { + assert_eq!(parse_css_background("repeating-linear-gradient(50deg, blue, yellow, #00FF00)"), + Ok(Background::LinearGradient(LinearGradientPreInfo { + direction: Direction::Angle(50.0), + extend_mode: ExtendMode::Repeat, + stops: vec![ + GradientStopPre { + offset: Some(0.0), + color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.5), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } -// This test currently fails, but it's not that important to fix right now -/* -#[test] -fn test_parse_radial_gradient_2() { - assert_eq!(parse_css_background("repeating-radial-gradient(circle, red 10%, blue 50%, lime, yellow)"), - Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { - shape: Shape::Circle, - extend_mode: ExtendMode::Repeat, - stops: vec![ - GradientStopPre { - offset: Some(0.1), - color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.5), - color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(0.75), - color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, - }, - GradientStopPre { - offset: Some(1.0), - color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, - }], - }))); -} -*/ + #[test] + fn test_parse_linear_gradient_4() { + assert_eq!(parse_css_background("linear-gradient(to bottom right, red, yellow)"), + Ok(Background::LinearGradient(LinearGradientPreInfo { + direction: Direction::FromTo(DirectionCorner::TopLeft, DirectionCorner::BottomRight), + extend_mode: ExtendMode::Clamp, + stops: vec![GradientStopPre { + offset: Some(0.0), + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } -#[test] -fn test_parse_css_color_1() { - assert_eq!(parse_css_color("#F0F8FF"), Ok(ColorU { r: 240, g: 248, b: 255, a: 255 })); -} + #[test] + fn test_parse_radial_gradient_1() { + assert_eq!(parse_css_background("radial-gradient(circle, lime, blue, yellow)"), + Ok(Background::RadialGradient(RadialGradientPreInfo { + shape: Shape::Circle, + extend_mode: ExtendMode::Clamp, + stops: vec![ + GradientStopPre { + offset: Some(0.0), + color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.5), + color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } -#[test] -fn test_parse_css_color_2() { - assert_eq!(parse_css_color("#F0F8FF00"), Ok(ColorU { r: 240, g: 248, b: 255, a: 0 })); -} + // This test currently fails, but it's not that important to fix right now + /* + #[test] + fn test_parse_radial_gradient_2() { + assert_eq!(parse_css_background("repeating-radial-gradient(circle, red 10%, blue 50%, lime, yellow)"), + Ok(ParsedGradient::RadialGradient(RadialGradientPreInfo { + shape: Shape::Circle, + extend_mode: ExtendMode::Repeat, + stops: vec![ + GradientStopPre { + offset: Some(0.1), + color: ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.5), + color: ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(0.75), + color: ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, + }, + GradientStopPre { + offset: Some(1.0), + color: ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, + }], + }))); + } + */ -#[test] -fn test_parse_css_color_3() { - assert_eq!(parse_css_color("#EEE"), Ok(ColorU { r: 238, g: 238, b: 238, a: 255 })); -} + #[test] + fn test_parse_css_color_1() { + assert_eq!(parse_css_color("#F0F8FF"), Ok(ColorU { r: 240, g: 248, b: 255, a: 255 })); + } -#[test] -fn test_parse_pixel_value_1() { - assert_eq!(parse_pixel_value("15px"), Ok(PixelValue { metric: CssMetric::Px, number: 15.0 })); -} + #[test] + fn test_parse_css_color_2() { + assert_eq!(parse_css_color("#F0F8FF00"), Ok(ColorU { r: 240, g: 248, b: 255, a: 0 })); + } -#[test] -fn test_parse_pixel_value_2() { - assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue { metric: CssMetric::Em, number: 1.2 })); -} + #[test] + fn test_parse_css_color_3() { + assert_eq!(parse_css_color("#EEE"), Ok(ColorU { r: 238, g: 238, b: 238, a: 255 })); + } -#[test] -fn test_parse_pixel_value_3() { - assert_eq!(parse_pixel_value("aslkfdjasdflk"), Err(PixelParseError::InvalidComponent("aslkfdjasdflk"))); -} + #[test] + fn test_parse_pixel_value_1() { + assert_eq!(parse_pixel_value("15px"), Ok(PixelValue { metric: CssMetric::Px, number: 15.0 })); + } -#[test] -fn test_parse_css_border_radius_1() { - assert_eq!(parse_css_border_radius("15px"), Ok(BorderRadius::uniform(15.0))); -} + #[test] + fn test_parse_pixel_value_2() { + assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue { metric: CssMetric::Em, number: 1.2 })); + } -#[test] -fn test_parse_css_border_radius_2() { - assert_eq!(parse_css_border_radius("15px 50px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(15.0, 15.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(50.0, 50.0), - })); -} + #[test] + fn test_parse_pixel_value_3() { + assert_eq!(parse_pixel_value("aslkfdjasdflk"), Err(PixelParseError::InvalidComponent("aslkfdjasdflk"))); + } -#[test] -fn test_parse_css_border_radius_3() { - assert_eq!(parse_css_border_radius("15px 50px 30px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(30.0, 30.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(50.0, 50.0), - })); -} + #[test] + fn test_parse_css_border_radius_1() { + assert_eq!(parse_css_border_radius("15px"), Ok(BorderRadius::uniform(15.0))); + } -#[test] -fn test_parse_css_border_radius_4() { - assert_eq!(parse_css_border_radius("15px 50px 30px 5px"), Ok(BorderRadius { - top_left: LayoutSize::new(15.0, 15.0), - bottom_right: LayoutSize::new(30.0, 30.0), - top_right: LayoutSize::new(50.0, 50.0), - bottom_left: LayoutSize::new(5.0, 5.0), - })); -} + #[test] + fn test_parse_css_border_radius_2() { + assert_eq!(parse_css_border_radius("15px 50px"), Ok(BorderRadius { + top_left: LayoutSize::new(15.0, 15.0), + bottom_right: LayoutSize::new(15.0, 15.0), + top_right: LayoutSize::new(50.0, 50.0), + bottom_left: LayoutSize::new(50.0, 50.0), + })); + } -#[test] -fn test_parse_css_font_family_1() { - assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { - fonts: vec![ - Font::ExternalFont("Webly Sleeky UI"), - Font::BuiltinFont("monospace"), - ] - })); -} + #[test] + fn test_parse_css_border_radius_3() { + assert_eq!(parse_css_border_radius("15px 50px 30px"), Ok(BorderRadius { + top_left: LayoutSize::new(15.0, 15.0), + bottom_right: LayoutSize::new(30.0, 30.0), + top_right: LayoutSize::new(50.0, 50.0), + bottom_left: LayoutSize::new(50.0, 50.0), + })); + } -#[test] -fn test_parse_css_font_family_2() { - assert_eq!(parse_css_font_family("'Webly Sleeky UI'"), Ok(FontFamily { - fonts: vec![ - Font::ExternalFont("Webly Sleeky UI"), - ] - })); + #[test] + fn test_parse_css_border_radius_4() { + assert_eq!(parse_css_border_radius("15px 50px 30px 5px"), Ok(BorderRadius { + top_left: LayoutSize::new(15.0, 15.0), + bottom_right: LayoutSize::new(30.0, 30.0), + top_right: LayoutSize::new(50.0, 50.0), + bottom_left: LayoutSize::new(5.0, 5.0), + })); + } -} -#[test] -fn test_parse_background_image() { - assert_eq!(parse_css_background("image(\"Cat 01\")"), Ok(Background::Image( - CssImageId("Cat 01") - ))); -} + #[test] + fn test_parse_css_font_family_1() { + assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { + fonts: vec![ + Font::ExternalFont("Webly Sleeky UI"), + Font::BuiltinFont("monospace"), + ] + })); + } + + #[test] + fn test_parse_css_font_family_2() { + assert_eq!(parse_css_font_family("'Webly Sleeky UI'"), Ok(FontFamily { + fonts: vec![ + Font::ExternalFont("Webly Sleeky UI"), + ] + })); + + } + + #[test] + fn test_parse_background_image() { + assert_eq!(parse_css_background("image(\"Cat 01\")"), Ok(Background::Image( + CssImageId("Cat 01") + ))); + } +} \ No newline at end of file diff --git a/src/id_tree.rs b/src/id_tree.rs index 09ba672d4..b9903aec0 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -570,69 +570,74 @@ impl<'a, T> Iterator for ReverseTraverse<'a, T> { } } -#[test] -fn drop_allocator() { - use std::cell::Cell; - - struct DropTracker<'a>(&'a Cell); - impl<'a> Drop for DropTracker<'a> { - fn drop(&mut self) { - self.0.set(&self.0.get() + 1); +#[cfg(test)] +mod id_tree_tests { + use super::*; + + #[test] + fn drop_allocator() { + use std::cell::Cell; + + struct DropTracker<'a>(&'a Cell); + impl<'a> Drop for DropTracker<'a> { + fn drop(&mut self) { + self.0.set(&self.0.get() + 1); + } } - } - let drop_counter = Cell::new(0); - { - let mut new_counter = 0; - let arena = &mut Arena::new(); - macro_rules! new { - () => { - { - new_counter += 1; - arena.new_node((new_counter, DropTracker(&drop_counter))) + let drop_counter = Cell::new(0); + { + let mut new_counter = 0; + let arena = &mut Arena::new(); + macro_rules! new { + () => { + { + new_counter += 1; + arena.new_node((new_counter, DropTracker(&drop_counter))) + } } - } - }; + }; + + let a = new!(); // 1 + a.append(new!(), arena); // 2 + a.append(new!(), arena); // 3 + a.prepend(new!(), arena); // 4 + let b = new!(); // 5 + b.append(a, arena); + a.insert_before(new!(), arena); // 6 + a.insert_before(new!(), arena); // 7 + a.insert_after(new!(), arena); // 8 + a.insert_after(new!(), arena); // 9 + let c = new!(); // 10 + b.append(c, arena); + + assert_eq!(drop_counter.get(), 0); + arena[c].previous_sibling().unwrap().detach(arena); + assert_eq!(drop_counter.get(), 0); + + assert_eq!(b.descendants(arena).map(|node| arena[node].data.0).collect::>(), [ + 5, 6, 7, 1, 4, 2, 3, 9, 10 + ]); + } - let a = new!(); // 1 - a.append(new!(), arena); // 2 - a.append(new!(), arena); // 3 - a.prepend(new!(), arena); // 4 - let b = new!(); // 5 - b.append(a, arena); - a.insert_before(new!(), arena); // 6 - a.insert_before(new!(), arena); // 7 - a.insert_after(new!(), arena); // 8 - a.insert_after(new!(), arena); // 9 - let c = new!(); // 10 - b.append(c, arena); - - assert_eq!(drop_counter.get(), 0); - arena[c].previous_sibling().unwrap().detach(arena); - assert_eq!(drop_counter.get(), 0); - - assert_eq!(b.descendants(arena).map(|node| arena[node].data.0).collect::>(), [ - 5, 6, 7, 1, 4, 2, 3, 9, 10 - ]); - } - - assert_eq!(drop_counter.get(), 10); -} + assert_eq!(drop_counter.get(), 10); + } -#[test] -fn children_ordering() { - - let arena = &mut Arena::new(); - let root = arena.new_node("".to_string()); + #[test] + fn children_ordering() { + + let arena = &mut Arena::new(); + let root = arena.new_node("".to_string()); - root.append(arena.new_node("b".to_string()), arena); - root.prepend(arena.new_node("a".to_string()), arena); - root.append(arena.new_node("c".to_string()), arena); + root.append(arena.new_node("b".to_string()), arena); + root.prepend(arena.new_node("a".to_string()), arena); + root.append(arena.new_node("c".to_string()), arena); - let children = root.children(arena).map(|node| &*arena[node].data).collect::>(); - let reverse_children = root.reverse_children(arena).map(|node| &*arena[node].data).collect::>(); + let children = root.children(arena).map(|node| &*arena[node].data).collect::>(); + let reverse_children = root.reverse_children(arena).map(|node| &*arena[node].data).collect::>(); - assert_eq!(children, vec!["a", "b", "c"]); - assert_eq!(reverse_children, vec!["c", "b", "a"]); + assert_eq!(children, vec!["a", "b", "c"]); + assert_eq!(reverse_children, vec!["c", "b", "a"]); + } } \ No newline at end of file diff --git a/src/images.rs b/src/images.rs index 65a2bbf9b..e552917f4 100644 --- a/src/images.rs +++ b/src/images.rs @@ -99,7 +99,7 @@ pub(crate) fn prepare_image(image_decoded: DynamicImage) let image_dims = image_decoded.dimensions(); // see: https://github.com/servo/webrender/blob/80c614ab660bf6cca52594d0e33a0be262a7ac12/wrench/src/yaml_frame_reader.rs#L401-L427 - let (format, mut bytes) = match image_decoded { + let (format, bytes) = match image_decoded { image::ImageLuma8(_) => { (WebrenderImageFormat::R8, image_decoded.raw_pixels()) }, @@ -114,6 +114,8 @@ pub(crate) fn prepare_image(image_decoded: DynamicImage) greyscale_alpha[1] ]); } + // TODO: necessary for greyscale? + premultiply(pixels.as_mut_slice()); (WebrenderImageFormat::BGRA8, pixels) }, image::ImageRgba8(_) => { @@ -136,10 +138,6 @@ pub(crate) fn prepare_image(image_decoded: DynamicImage) } }; - if format == WebrenderImageFormat::BGRA8 { - premultiply(bytes.as_mut_slice()); - } - let opaque = is_image_opaque(format, &bytes[..]); let allow_mipmaps = true; let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); @@ -166,6 +164,7 @@ pub(crate) fn is_image_opaque(format: WebrenderImageFormat, bytes: &[u8]) -> boo // From webrender/wrench // These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +// This function also converts from RGBA8 to BRGA8 pub(crate) fn premultiply(data: &mut [u8]) { for pixel in data.chunks_mut(4) { let a = u32::from(pixel[3]); diff --git a/src/window.rs b/src/window.rs index 1012b8c25..b716d14dc 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,6 @@ use webrender::api::*; -use webrender::{Renderer, RendererOptions}; +use webrender::{Renderer, RendererOptions, RendererKind}; +// use webrender::renderer::RendererError; use glium::{IncompatibleOpenGl, Display}; use glium::debug::DebugCallbackBehavior; use glium::glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, @@ -227,6 +228,8 @@ pub enum WindowCreateError { SwapBuffers(::glium::SwapBuffersError), /// IO error Io(::std::io::Error), + /// Webrender creation error (probably OpenGL missing?) + Renderer/*(RendererError)*/, } impl From<::glium::SwapBuffersError> for WindowCreateError { @@ -473,6 +476,39 @@ impl Window { frame.clear_color_srgb(options.background.r, options.background.g, options.background.b, options.background.a); frame.finish()?; + let device_pixel_ratio = display.gl_window().hidpi_factor(); + + // this exists because RendererOptions isn't Clone-able + fn get_renderer_opts(native: bool, device_pixel_ratio: f32, clear_color: Option) -> RendererOptions { + RendererOptions { + resource_override_path: None, + // pre-caching shaders means to compile all shaders on startup + // this can take significant time and should be only used for testing the shaders + precache_shaders: false, + device_pixel_ratio: device_pixel_ratio, + enable_subpixel_aa: true, + enable_aa: true, + clear_color: clear_color, + enable_render_on_scroll: false, + renderer_kind: if native { + RendererKind::Native + } else { + RendererKind::OSMesa + }, + .. RendererOptions::default() + } + } + + let opts_native = get_renderer_opts(true, device_pixel_ratio, Some(options.background)); + let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); + + let framebuffer_size = { + #[allow(deprecated)] + let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); + DeviceUintSize::new(width, height) + }; + let notifier = Box::new(Notifier::new(events_loop.create_proxy())); + let gl = match display.gl_window().get_api() { glutin::Api::OpenGl => unsafe { gl::GlFns::load_with(|symbol| @@ -485,30 +521,8 @@ impl Window { glutin::Api::WebGl => return Err(WindowCreateError::WebGlNotSupported), }; - let device_pixel_ratio = display.gl_window().hidpi_factor(); - - let opts = RendererOptions { - resource_override_path: None, - // pre-caching shaders means to compile all shaders on startup - // this can take significant time and should be only used for testing the shaders - precache_shaders: false, - device_pixel_ratio, - enable_subpixel_aa: true, - enable_aa: true, - clear_color: Some(options.background), - enable_render_on_scroll: false, - // TODO: Fallback to OSMesa if needed! - // renderer_kind: RendererKind::Native, - .. RendererOptions::default() - }; - - let framebuffer_size = { - #[allow(deprecated)] - let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); - DeviceUintSize::new(width, height) - }; - let notifier = Box::new(Notifier::new(events_loop.create_proxy())); - let (renderer, sender) = Renderer::new(gl.clone(), notifier, opts).unwrap(); + let (renderer, sender) = Renderer::new(gl.clone(), notifier.clone(), opts_native) + .or_else(|_| Renderer::new(gl, notifier, opts_osmesa)).unwrap(); let api = sender.create_api(); let document_id = api.add_document(framebuffer_size, 0); From 6ecb90c9f58b313ddbdd8c56f1170f5770687c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 23 Mar 2018 23:55:38 +0100 Subject: [PATCH 012/868] Added font loading using rusttype --- Cargo.toml | 3 ++- src/app.rs | 10 ++++----- src/app_state.rs | 11 ++++----- src/display_list.rs | 17 +++++++++++++- src/font.rs | 38 +++++++++++++++++++++++++++++-- src/images.rs | 2 ++ src/lib.rs | 1 + src/resources.rs | 54 ++++++++++++++++++++++++++++++++++++--------- 8 files changed, 112 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 891af8f4f..ad0a696a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ twox-hash = "1.1.0" glium = "0.20.0" gleam = "0.4.20" euclid = "0.17" -image = "0.18.0" \ No newline at end of file +image = "0.18.0" +rusttype = "0.5.2" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 11ea38680..5d6c070f7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,11 +17,11 @@ use font::FontError; use webrender::api::RenderApi; /// Graphical application that maintains some kind of application state -pub struct App { +pub struct App<'a, T: LayoutScreen> { /// The graphical windows, indexed by ID windows: Vec>, /// The global application state - pub app_state: Arc>>, + pub app_state: Arc>>, } pub(crate) struct FrameEventInfo { @@ -46,7 +46,7 @@ impl Default for FrameEventInfo { } } -impl App { +impl<'a, T: LayoutScreen> App<'a, T> { /// Create a new, empty application (note: doesn't create a window!) pub fn new(initial_data: T) -> Self { @@ -240,7 +240,7 @@ impl App { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).remove_image(id) @@ -261,7 +261,7 @@ impl App { /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, ImageError> + -> Result, FontError> { (*self.app_state.lock().unwrap()).add_font(id, data) } diff --git a/src/app_state.rs b/src/app_state.rs index 70012ec02..15247d6e6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -3,18 +3,19 @@ use resources::{AppResources}; use std::io::Read; use images::ImageType; use image::ImageError; +use font::FontError; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application /// should be laid out) -pub struct AppState { +pub struct AppState<'a, T: LayoutScreen> { /// Your data (the global struct which all callbacks will have access to) pub data: T, /// Fonts and images that are currently loaded into the app - pub(crate) resources: AppResources, + pub(crate) resources: AppResources<'a>, } -impl AppState { +impl<'a, T: LayoutScreen> AppState<'a, T> { /// Creates a new `AppState` pub fn new(initial_data: T) -> Self { @@ -40,7 +41,7 @@ impl AppState { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { self.resources.remove_image(id) @@ -61,7 +62,7 @@ impl AppState { /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, ImageError> + -> Result, FontError> { self.resources.add_font(id, data) } diff --git a/src/display_list.rs b/src/display_list.rs index 80b1b2d24..00769b8cb 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -98,6 +98,7 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { let mut resources = ResourceUpdates::new(); let mut updated_images = Vec::<(String, (ImageData, ImageDescriptor))>::new(); + let mut to_delete_images = Vec::<(String, Option)>::new(); // possible performance bottleneck (duplicated cloning) !! for (key, value) in app_resources.images.iter() { @@ -106,9 +107,22 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { updated_images.push((key.clone(), d.clone())); }, ImageState::Uploaded(_) => { }, + ImageState::AboutToBeDeleted(ref k) => { + to_delete_images.push((key.clone(), k.clone())); + } } } + // Remove any images that should be deleted + for (resource_key, image_key) in to_delete_images.into_iter() { + if let Some(image_key) = image_key { + resources.delete_image(image_key); + } + app_resources.images.remove(&resource_key); + } + + // Upload all remaining images to the GPU only if the haven't been + // uploaded yet for (resource_key, (data, descriptor)) in updated_images.into_iter() { let key = api.generate_image_key(); resources.add_image(key, descriptor, data, None); @@ -119,6 +133,7 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { }); } + api.update_resources(resources); } @@ -280,7 +295,7 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { AlphaType::Alpha, image_info.key); }, - ReadyForUpload(_) => { }, + _ => { }, } } } diff --git a/src/font.rs b/src/font.rs index 783bdc3c2..4644a8427 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,9 +1,43 @@ //! Module for loading and handling fonts +use webrender::api::FontKey; +use rusttype::{Font, FontCollection}; +use rusttype::Error as RusttypeError; -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] +pub(crate) enum FontState { + // Font is available for the renderer + Uploaded(FontKey), + // Raw bytes for the font, to be uploaded in the next + // draw call (for webrenders add_raw_font function) + ReadyForUpload(Vec), +} + +#[derive(Debug)] pub enum FontError { /// Font failed to upload to the GPU UploadError, + /// + InvalidFormat, /// Rusttype failed to parse the font - RusttypeError, + ParseError(RusttypeError), + /// IO error + IoError(::std::io::Error), +} + +impl From for FontError { + fn from(e: RusttypeError) -> Self { + FontError::ParseError(e) + } } + +/// Read font data to get font information, v_metrics, glyph info etc. +pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontError> { + let collection = FontCollection::from_bytes(data)?; + let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); + Ok(font) +} + +// font key, font_instance_key, size in app_units::Au +// +// let instance_key = render_api.generate_font_instance_key(); +// resources.add_font_instance(instance_key, font_key, size, None, None, Vec::new()); \ No newline at end of file diff --git a/src/images.rs b/src/images.rs index e552917f4..8e56997a7 100644 --- a/src/images.rs +++ b/src/images.rs @@ -33,6 +33,8 @@ pub(crate) enum ImageState { Uploaded(ImageInfo), // image is loaded & decoded, but not yet available ReadyForUpload((ImageData, ImageDescriptor)), + // Image is about to get deleted in the next frame + AboutToBeDeleted(Option), } diff --git a/src/lib.rs b/src/lib.rs index 085223b6a..f88c1521c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ extern crate gleam; extern crate euclid; extern crate simplecss; extern crate image; +extern crate rusttype; /// Global application (Initialization starts here) pub mod app; diff --git a/src/resources.rs b/src/resources.rs index 2eb43b005..aca388b8d 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,10 +1,13 @@ use std::sync::atomic::{AtomicUsize, Ordering}; -use webrender::api::{ImageKey, FontKey}; +use webrender::api::{ImageKey, FontKey, FontInstanceKey}; use FastHashMap; use std::io::Read; use images::{ImageState, ImageType}; +use font::{FontState, FontError}; use image::{self, ImageError, DynamicImage, GenericImage}; use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; +use std::collections::hash_map::Entry::*; +use rusttype::Font; /// Font and image keys /// @@ -16,22 +19,30 @@ use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; /// /// Images and fonts can be references across window contexts /// (not yet tested, but should work). -#[derive(Debug, Default, Clone)] -pub(crate) struct AppResources { +#[derive(Default, Clone)] +pub(crate) struct AppResources<'a> { pub(crate) images: FastHashMap, - pub(crate) fonts: FastHashMap>, + // Fonts are trickier to handle than images. + // First, we duplicate the font - webrender wants the raw font data, + // but we also need access to the font metrics. So we first parse the font + // to make sure that nothing is going wrong. In the next draw call, we + // upload the font and replace the FontState with the newly created font key + pub(crate) font_data: FastHashMap, FontState)>, + // After we've looked up the FontKey in the font_data map, we can then access + // the font instance key (if there is any). If there is no font instance key, + // we first need to create one. + pub(crate) fonts: FastHashMap>, } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub(crate) struct FontSize(pub(crate) usize); -impl AppResources { +impl<'a> AppResources<'a> { /// See `AppState::add_image()` pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { - use std::collections::hash_map::Entry::*; use images; // the module, not the crate! match self.images.entry(id.into()) { @@ -48,10 +59,22 @@ impl AppResources { } /// See `AppState::remove_image()` - pub fn remove_image>(&mut self, id: S) + pub fn remove_image>(&mut self, id: S) -> Option<()> { - Some(()) + match self.images.get_mut(id.as_ref()) { + None => None, + Some(v) => { + let to_delete_image_key = match *v { + ImageState::Uploaded(ref image_info) => { + Some(image_info.key.clone()) + }, + _ => None, + }; + *v = ImageState::AboutToBeDeleted(to_delete_image_key); + Some(()) + } + } } /// See `AppState::has_image()` @@ -63,9 +86,20 @@ impl AppResources { /// See `AppState::add_font()` pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, ImageError> + -> Result, FontError> { - Ok(Some(())) + use font; + + match self.font_data.entry(id.into()) { + Occupied(_) => Ok(None), + Vacant(v) => { + let mut font_data = Vec::::new(); + data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; + let parsed_font = font::rusttype_load_font(font_data.clone())?; + v.insert((parsed_font, FontState::ReadyForUpload(font_data))); + Ok(Some(())) + }, + } } /// See `AppState::remove_font()` From de00bd301b392286f37cc7ae1ffce62d56ad38cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 24 Mar 2018 00:15:25 +0100 Subject: [PATCH 013/868] Prepare font loading --- src/display_list.rs | 58 ++++++++++++++++++++++++++++++++++++++++----- src/font.rs | 1 + 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 00769b8cb..55d1fe197 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -66,7 +66,7 @@ impl<'a> DisplayRectangle<'a> { } } -impl<'a, T: LayoutScreen> DisplayList<'a, T> { +impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { /// NOTE: This function assumes that the UiDescription has an initialized arena /// @@ -93,9 +93,14 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { /// Looks if any new images need to be uploaded and stores the in the image resources fn update_resources(api: &RenderApi, app_resources: &mut AppResources) { - use images::{ImageState, ImageInfo}; - let mut resources = ResourceUpdates::new(); + Self::update_image_resources(api, app_resources, &mut resources); + Self::update_font_resources(api, app_resources, &mut resources); + api.update_resources(resources); + } + + fn update_image_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { + use images::{ImageState, ImageInfo}; let mut updated_images = Vec::<(String, (ImageData, ImageDescriptor))>::new(); let mut to_delete_images = Vec::<(String, Option)>::new(); @@ -116,7 +121,7 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { // Remove any images that should be deleted for (resource_key, image_key) in to_delete_images.into_iter() { if let Some(image_key) = image_key { - resources.delete_image(image_key); + resource_updates.delete_image(image_key); } app_resources.images.remove(&resource_key); } @@ -125,16 +130,57 @@ impl<'a, T: LayoutScreen> DisplayList<'a, T> { // uploaded yet for (resource_key, (data, descriptor)) in updated_images.into_iter() { let key = api.generate_image_key(); - resources.add_image(key, descriptor, data, None); + resource_updates.add_image(key, descriptor, data, None); *app_resources.images.get_mut(&resource_key).unwrap() = ImageState::Uploaded(ImageInfo { key: key, descriptor: descriptor }); } + } + fn update_font_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { - api.update_resources(resources); + use font::FontState; + + let mut updated_fonts = Vec::<(String, Vec)>::new(); + let mut to_delete_fonts = Vec::<(String, (FontKey, Vec))>::new(); +/* + for (key, value) in app_resources.images.iter() { + match *value { + FontState::ReadyForUpload(ref d) => { + updated_fonts.push((key.clone(), d.1.clone())); + }, + FontState::Uploaded(_) => { }, + FontState::AboutToBeDeleted(ref k) => { + to_delete_fonts.push(( (key.clone(), k.values().cloned().collect()))); + } + } + } +*/ + // Delete the complete font. Maybe a more granular option to + // keep the font data in memory should be added later + for (resource_key, (font_key, font_instance_keys)) in to_delete_fonts.into_iter() { + for instance in font_instance_keys { + resource_updates.delete_font_instance(instance); + } + resource_updates.delete_font(font_key); + // app_resources.fonts.remove(&resource_key); + app_resources.font_data.remove(&resource_key); + } + + /* + // Fonts are trickier to handle than images. + // First, we duplicate the font - webrender wants the raw font data, + // but we also need access to the font metrics. So we first parse the font + // to make sure that nothing is going wrong. In the next draw call, we + // upload the font and replace the FontState with the newly created font key + pub(crate) font_data: FastHashMap, FontState)>, + // After we've looked up the FontKey in the font_data map, we can then access + // the font instance key (if there is any). If there is no font instance key, + // we first need to create one. + pub(crate) fonts: FastHashMap>, + */ } pub fn into_display_list_builder( diff --git a/src/font.rs b/src/font.rs index 4644a8427..30e6cedde 100644 --- a/src/font.rs +++ b/src/font.rs @@ -10,6 +10,7 @@ pub(crate) enum FontState { // Raw bytes for the font, to be uploaded in the next // draw call (for webrenders add_raw_font function) ReadyForUpload(Vec), + AboutToBeDeleted() } #[derive(Debug)] From 342d0cebb9aa6d87d8ad42bf077ff8cad5adab96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 24 Mar 2018 21:28:06 +0100 Subject: [PATCH 014/868] Added font rendering --- Cargo.toml | 5 +- examples/debug.rs | 2 +- examples/test_content.css | 3 +- src/app.rs | 6 +- src/app_state.rs | 8 +- src/css_parser.rs | 25 +++++- src/display_list.rs | 158 ++++++++++++++++++++++++++++---------- src/font.rs | 12 ++- src/lib.rs | 1 + src/resources.rs | 30 +++++--- 10 files changed, 180 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ad0a696a4..4a0989fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "30cfecc343e407ce277d07cf09f27ad9dd1917a1" } +webrender = { git = "https://github.com/servo/webrender", rev = "00b85801f8c09431e5963a9e1dcd1a9087b744b9" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" @@ -12,4 +12,5 @@ glium = "0.20.0" gleam = "0.4.20" euclid = "0.17" image = "0.18.0" -rusttype = "0.5.2" \ No newline at end of file +rusttype = "0.5.2" +app_units = "0.6" diff --git a/examples/debug.rs b/examples/debug.rs index 71535d768..c6f769790 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -50,7 +50,7 @@ fn main() { app.remove_font("Webly Sleeky UI"); */ app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); - // app.remove_image("MyImage"); + // app.remove_image("Cat01"); // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that diff --git a/examples/test_content.css b/examples/test_content.css index dc8261280..e9d1ab804 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -3,8 +3,7 @@ color: #000000; border: 1px solid #b7b7b7; border-radius: 4px; - /*box-shadow: 0px 0px 3px #c5c5c5ad;*/ - box-shadow: 0px 0px 3px red; + box-shadow: 10px 10px red; background: image("Cat01"); /*background: linear-gradient(#fcfcfc, #efefef);*/ width: 200px; diff --git a/src/app.rs b/src/app.rs index 5d6c070f7..1ae6a4ea4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -243,7 +243,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn remove_image>(&mut self, id: S) -> Option<()> { - (*self.app_state.lock().unwrap()).remove_image(id) + (*self.app_state.lock().unwrap()).delete_image(id) } /// Checks if an image is currently registered and ready-to-use @@ -269,10 +269,10 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Removes a font from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_font>(&mut self, id: S) + pub fn remove_font>(&mut self, id: S) -> Option<()> { - (*self.app_state.lock().unwrap()).remove_font(id) + (*self.app_state.lock().unwrap()).delete_font(id) } } diff --git a/src/app_state.rs b/src/app_state.rs index 15247d6e6..7c34f1a3f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -41,10 +41,10 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn delete_image>(&mut self, id: S) -> Option<()> { - self.resources.remove_image(id) + self.resources.delete_image(id) } /// Checks if an image is currently registered and ready-to-use @@ -70,9 +70,9 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// Removes a font from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_font>(&mut self, id: S) + pub fn delete_font>(&mut self, id: S) -> Option<()> { - self.resources.remove_font(id) + self.resources.delete_font(id) } } diff --git a/src/css_parser.rs b/src/css_parser.rs index 4aa5c4eb6..af0b71eea 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -7,7 +7,14 @@ use webrender::api::{ColorU, BorderRadius, LayoutVector2D, LayoutPoint, use std::num::{ParseIntError, ParseFloatError}; use euclid::{TypedRotation2D, Angle, TypedPoint2D}; -pub const EM_HEIGHT: f32 = 16.0; +pub(crate) const EM_HEIGHT: f32 = 16.0; + +// In case no font size is specified for a node, this will be subsituted as the +// default font size +pub(crate) const DEFAULT_FONT_SIZE: FontSize = FontSize(PixelValue { + metric: CssMetric::Px, + number: 10.0, +}); macro_rules! impl_from { ($a:ident, $b:ident, $enum_type:ident) => ( @@ -1288,15 +1295,27 @@ typed_pixel_value_parser!(parse_css_font_size, FontSize); #[derive(Debug, PartialEq, Clone)] pub struct FontFamily<'a> { // parsed fonts, in order, i.e. "Webly Sleeky UI", "monospace", etc. - fonts: Vec> + pub(crate) fonts: Vec> } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Hash)] pub enum Font<'a> { BuiltinFont(&'a str), ExternalFont(&'a str), } +impl<'a> Font<'a> { + pub fn get_font_id(&self) -> &'a str { + use self::Font::*; + // TODO: Currently BuiltinFont("sans-serif") and + // ExternalFont("sans-serif") are the same because of this function + match *self { + BuiltinFont(f) => f, + ExternalFont(f) => f, + } + } +} + #[derive(Debug, PartialEq, Copy, Clone)] pub enum FontFamilyParseError<'a> { InvalidFontFamily(&'a str), diff --git a/src/display_list.rs b/src/display_list.rs index 55d1fe197..117903c3d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -17,6 +17,8 @@ use FastHashMap; use cache::DomChangeSet; use std::sync::atomic::{Ordering, AtomicUsize}; +const DEBUG_COLOR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 }; + pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) ui_descr: &'a UiDescription, pub(crate) rectangles: BTreeMap> @@ -92,11 +94,9 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } /// Looks if any new images need to be uploaded and stores the in the image resources - fn update_resources(api: &RenderApi, app_resources: &mut AppResources) { - let mut resources = ResourceUpdates::new(); - Self::update_image_resources(api, app_resources, &mut resources); - Self::update_font_resources(api, app_resources, &mut resources); - api.update_resources(resources); + fn update_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { + Self::update_image_resources(api, app_resources, resource_updates); + Self::update_font_resources(api, app_resources, resource_updates); } fn update_image_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { @@ -139,48 +139,51 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } } + // almost the same as update_image_resources, but fonts + // have two HashMaps that need to be updated fn update_font_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { use font::FontState; let mut updated_fonts = Vec::<(String, Vec)>::new(); - let mut to_delete_fonts = Vec::<(String, (FontKey, Vec))>::new(); -/* - for (key, value) in app_resources.images.iter() { - match *value { - FontState::ReadyForUpload(ref d) => { - updated_fonts.push((key.clone(), d.1.clone())); + let mut to_delete_fonts = Vec::<(String, Option<(FontKey, Vec)>)>::new(); + + for (key, value) in app_resources.font_data.iter() { + match value.1 { + FontState::ReadyForUpload(ref bytes) => { + updated_fonts.push((key.clone(), bytes.clone())); }, FontState::Uploaded(_) => { }, - FontState::AboutToBeDeleted(ref k) => { - to_delete_fonts.push(( (key.clone(), k.values().cloned().collect()))); + FontState::AboutToBeDeleted(ref font_key) => { + let to_delete_font_instances = font_key.and_then(|f_key| { + let to_delete_font_instances = app_resources.fonts[&f_key].values().cloned().collect(); + Some((f_key.clone(), to_delete_font_instances)) + }); + to_delete_fonts.push((key.clone(), to_delete_font_instances)); } } } -*/ + // Delete the complete font. Maybe a more granular option to // keep the font data in memory should be added later - for (resource_key, (font_key, font_instance_keys)) in to_delete_fonts.into_iter() { - for instance in font_instance_keys { - resource_updates.delete_font_instance(instance); + for (resource_key, to_delete_instances) in to_delete_fonts.into_iter() { + if let Some((font_key, font_instance_keys)) = to_delete_instances { + for instance in font_instance_keys { + resource_updates.delete_font_instance(instance); + } + resource_updates.delete_font(font_key); + app_resources.fonts.remove(&font_key); } - resource_updates.delete_font(font_key); - // app_resources.fonts.remove(&resource_key); app_resources.font_data.remove(&resource_key); } - /* - // Fonts are trickier to handle than images. - // First, we duplicate the font - webrender wants the raw font data, - // but we also need access to the font metrics. So we first parse the font - // to make sure that nothing is going wrong. In the next draw call, we - // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: FastHashMap, FontState)>, - // After we've looked up the FontKey in the font_data map, we can then access - // the font instance key (if there is any). If there is no font instance key, - // we first need to create one. - pub(crate) fonts: FastHashMap>, - */ + // Upload all remaining fonts to the GPU only if the haven't been uploaded yet + for (resource_key, data) in updated_fonts.into_iter() { + let key = api.generate_font_key(); + println!("adding new font key"); + resource_updates.add_raw_font(key, data, 0); // TODO: use the index better? + app_resources.font_data.get_mut(&resource_key).unwrap().1 = FontState::Uploaded(key); + } } pub fn into_display_list_builder( @@ -239,9 +242,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { css.needs_relayout = false; let mut builder = DisplayListBuilder::with_capacity(pipeline_id, ui_solver.window_dimensions.layout_size, self.rectangles.len()); + let mut resource_updates = ResourceUpdates::new(); // Upload image and font resources - Self::update_resources(render_api, app_resources); + Self::update_resources(render_api, app_resources, &mut resource_updates); for (rect_idx, rect) in self.rectangles.iter() { @@ -293,17 +297,12 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { Vec::new(), ); + // Push clip if let Some(id) = clip_region_id { builder.push_clip_id(id); } - builder.push_rect(&info, rect.style.background_color.unwrap_or(ColorU { r: 255, g: 0, b: 0, a: 255 }).into()); - - if clip_region_id.is_some() { - builder.pop_clip_id(); - } - - // red rectangle if we don't have a background color + // Push box shadow if let Some(ref pre_shadow) = rect.style.box_shadow { // The pre_shadow is missing the BorderRadius & LayoutRect // TODO: do we need to pop the shadows? @@ -313,6 +312,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { border_radius, pre_shadow.clip_mode); } + // Push basic rect + background + builder.push_rect(&info, rect.style.background_color.unwrap_or(DEBUG_COLOR).into()); + + // Push background gradient / image if let Some(ref background) = rect.style.background { match *background { Background::RadialGradient(ref _gradient) => { @@ -348,6 +351,29 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } } + use font::FontState; + use euclid::TypedPoint2D; + const FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 255, a: 150 }; + + // Push font + if let Some(ref font_family) = rect.style.font_family { + let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); + let font_size = rect.style.font_size.unwrap_or(DEFAULT_FONT_SIZE); + let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX); + let font_result = push_font(font_id, font_size_app_units, &mut resource_updates, app_resources, render_api); + + if let Some(font_instance_key) = font_result { + let font = &app_resources.font_data[font_id].0; + let glyph = font.glyph('a'); // TODO: get label + let glyphs = [GlyphInstance { + index: glyph.id().0, + point: TypedPoint2D::new(50.0, 50.0), + }]; + builder.push_text(&info, &glyphs, font_instance_key, FONT_COLOR.into(), None); + } + } + + // Push border if let Some((border_widths, mut border_details)) = rect.style.border { if let Some(border_radius) = rect.style.border_radius { if let BorderDetails::Normal(ref mut n) = border_details { @@ -357,13 +383,67 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { builder.push_border(&info, border_widths, border_details); } + // Pop clip + if clip_region_id.is_some() { + builder.pop_clip_id(); + } + builder.pop_stacking_context(); } + render_api.update_resources(resource_updates); + Some(builder) } } +use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; + +fn push_font( + font_id: &str, + font_size_app_units: Au, + resource_updates: &mut ResourceUpdates, + app_resources: &mut AppResources, + render_api: &RenderApi) -> Option { + + use font::FontState; + + if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { + println!("warning: too big or too small font size"); + return None; + } + + if let Some(&(ref font, ref font_state)) = app_resources.font_data.get(font_id) { + match *font_state { + FontState::Uploaded(font_key) => { + let font_sizes_hashmap = app_resources.fonts.entry(font_key) + .or_insert(FastHashMap::default()); + let font_instance_key = font_sizes_hashmap.entry(font_size_app_units) + .or_insert_with(|| { + let f_instance_key = render_api.generate_font_instance_key(); + resource_updates.add_font_instance( + f_instance_key, + font_key, + font_size_app_units, + None, + None, + Vec::new(), + ); + f_instance_key + } + ); + + return Some(*font_instance_key); + }, + _ => { + println!("warning: trying to use font {:?} that isn't available", font_id); + }, + } + } + + return None; +} + macro_rules! parse { ($constraint_list:ident, $key:expr, $func:tt) => ( $constraint_list.get($key).and_then(|w| $func(w).map_err(|e| { diff --git a/src/font.rs b/src/font.rs index 30e6cedde..6386444c6 100644 --- a/src/font.rs +++ b/src/font.rs @@ -10,7 +10,10 @@ pub(crate) enum FontState { // Raw bytes for the font, to be uploaded in the next // draw call (for webrenders add_raw_font function) ReadyForUpload(Vec), - AboutToBeDeleted() + /// Font that is about to be deleted + /// We need both the ID (to delete the bytes of the font) + /// as well as the FontKey to delete all the font instances + AboutToBeDeleted(Option), } #[derive(Debug)] @@ -36,9 +39,4 @@ pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontErro let collection = FontCollection::from_bytes(data)?; let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); Ok(font) -} - -// font key, font_instance_key, size in app_units::Au -// -// let instance_key = render_api.generate_font_instance_key(); -// resources.add_font_instance(instance_key, font_key, size, None, None, Vec::new()); \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f88c1521c..3de64dc25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,6 +33,7 @@ extern crate euclid; extern crate simplecss; extern crate image; extern crate rusttype; +extern crate app_units; /// Global application (Initialization starts here) pub mod app; diff --git a/src/resources.rs b/src/resources.rs index aca388b8d..647193032 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -8,6 +8,7 @@ use image::{self, ImageError, DynamicImage, GenericImage}; use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; use std::collections::hash_map::Entry::*; use rusttype::Font; +use app_units::Au; /// Font and image keys /// @@ -21,6 +22,7 @@ use rusttype::Font; /// (not yet tested, but should work). #[derive(Default, Clone)] pub(crate) struct AppResources<'a> { + /// Image cache pub(crate) images: FastHashMap, // Fonts are trickier to handle than images. // First, we duplicate the font - webrender wants the raw font data, @@ -31,12 +33,9 @@ pub(crate) struct AppResources<'a> { // After we've looked up the FontKey in the font_data map, we can then access // the font instance key (if there is any). If there is no font instance key, // we first need to create one. - pub(crate) fonts: FastHashMap>, + pub(crate) fonts: FastHashMap>, } -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub(crate) struct FontSize(pub(crate) usize); - impl<'a> AppResources<'a> { /// See `AppState::add_image()` @@ -58,8 +57,8 @@ impl<'a> AppResources<'a> { } } - /// See `AppState::remove_image()` - pub fn remove_image>(&mut self, id: S) + /// See `AppState::delete_image()` + pub fn delete_image>(&mut self, id: S) -> Option<()> { match self.images.get_mut(id.as_ref()) { @@ -102,10 +101,23 @@ impl<'a> AppResources<'a> { } } - /// See `AppState::remove_font()` - pub(crate) fn remove_font>(&mut self, id: S) + /// See `AppState::delete_font()` + pub(crate) fn delete_font>(&mut self, id: S) -> Option<()> { - Some(()) + // TODO: can fonts that haven't been uploaded yet be deleted? + match self.font_data.get_mut(id.as_ref()) { + None => None, + Some(v) => { + let to_delete_font_key = match v.1 { + FontState::Uploaded(ref font_key) => { + Some(font_key.clone()) + }, + _ => None, + }; + v.1 = FontState::AboutToBeDeleted(to_delete_font_key); + Some(()) + } + } } } \ No newline at end of file From 1bc0a9ff5170fdce56726322f4bcdc23c664f562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 04:33:20 +0200 Subject: [PATCH 015/868] Fixed box-shadow, experimented with blur and skew filters --- examples/test_content.css | 2 +- src/css_parser.rs | 4 +- src/display_list.rs | 133 +++++++++++++++++++++++--------------- 3 files changed, 85 insertions(+), 54 deletions(-) diff --git a/examples/test_content.css b/examples/test_content.css index e9d1ab804..97d9ac383 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -3,7 +3,7 @@ color: #000000; border: 1px solid #b7b7b7; border-radius: 4px; - box-shadow: 10px 10px red; + box-shadow: 0px 0px 3px #c5c5c5ad; background: image("Cat01"); /*background: linear-gradient(#fcfcfc, #efefef);*/ width: 200px; diff --git a/src/css_parser.rs b/src/css_parser.rs index af0b71eea..97f3132be 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1265,7 +1265,9 @@ pub(crate) struct RectStyle<'a> { /// Font size pub(crate) font_size: Option, /// Font name / family - pub(crate) font_family: Option> + pub(crate) font_family: Option>, + /// Text color + pub(crate) font_color: Option, } // Layout constraints for a given rectangle, such as "" diff --git a/src/display_list.rs b/src/display_list.rs index 117903c3d..f82bb32ee 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -18,6 +18,7 @@ use cache::DomChangeSet; use std::sync::atomic::{Ordering, AtomicUsize}; const DEBUG_COLOR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 }; +const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 255, a: 255 }; pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) ui_descr: &'a UiDescription, @@ -180,7 +181,6 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // Upload all remaining fonts to the GPU only if the haven't been uploaded yet for (resource_key, data) in updated_fonts.into_iter() { let key = api.generate_font_key(); - println!("adding new font key"); resource_updates.add_raw_font(key, data, 0); // TODO: use the index better? app_resources.font_data.get_mut(&resource_key).unwrap().1 = FontState::Uploaded(key); } @@ -243,12 +243,16 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { let mut builder = DisplayListBuilder::with_capacity(pipeline_id, ui_solver.window_dimensions.layout_size, self.rectangles.len()); let mut resource_updates = ResourceUpdates::new(); - + let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; + // Upload image and font resources Self::update_resources(render_api, app_resources, &mut resource_updates); for (rect_idx, rect) in self.rectangles.iter() { + use font::FontState; + use euclid::{TypedSize2D, TypedPoint2D, TypedTransform3D, Angle}; + // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); @@ -259,7 +263,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // have two touching rectangles let mut bounds = if rect.style.background_color.is_some() { LayoutRect::new( - LayoutPoint::new(0.0, 0.0), + LayoutPoint::new(50.0, 50.0), LayoutSize::new(200.0, 200.0), ) } else { @@ -269,57 +273,80 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { ) }; - // bug - for some reason, the origin gets scaled by 2.0, - // even if the HiDpi factor is set to 1.0 - // this is a workaround, this seems to be a bug in webrender - bounds.origin.x /= 2.0; - bounds.origin.y /= 2.0; - - let clip_region_id = rect.style.border_radius.and_then(|border_radius| { - let region = ComplexClipRegion { - rect: bounds, - radii: border_radius, - mode: ClipMode::Clip, - }; - Some(builder.define_clip(bounds, vec![region], None)) - }); + let info = LayoutPrimitiveInfo { + rect: bounds, + clip_rect: bounds, + is_backface_visible: true, + tag: rect.tag.and_then(|tag| Some((tag, 0))), + }; - let mut info = LayoutPrimitiveInfo::new(bounds); - info.tag = rect.tag.and_then(|tag| Some((tag, 0))); - +/* + builder.push_stacking_context( + &info, + ScrollPolicy::Scrollable, + None, + TransformStyle::Preserve3D, + Some(TypedTransform3D::create_skew(Angle{ radians: 20.0_f32.to_radians() }, Angle{ radians: 20.0_f32.to_radians() })), // TODO: expose 3D-transform in CSS + MixBlendMode::HardLight, // TODO: expose blend-modes in CSS + vec![FilterOp::Blur(3.0)], // TODO: expose filters (blur, hue, etc.) in CSS + ); +*/ builder.push_stacking_context( &info, ScrollPolicy::Scrollable, None, - TransformStyle::Flat, + TransformStyle::Preserve3D, None, MixBlendMode::Normal, - Vec::new(), + Vec::new() ); - // Push clip - if let Some(id) = clip_region_id { - builder.push_clip_id(id); - } - - // Push box shadow + // Push box shadow, before the clip is active if let Some(ref pre_shadow) = rect.style.box_shadow { // The pre_shadow is missing the BorderRadius & LayoutRect - // TODO: do we need to pop the shadows? let border_radius = rect.style.border_radius.unwrap_or(BorderRadius::zero()); + // Currently the box shadow is blurred across the whole window + // This can be possibly optimized further + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), full_screen_rect); builder.push_box_shadow(&info, bounds, pre_shadow.offset, pre_shadow.color, pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); + } - // Push basic rect + background - builder.push_rect(&info, rect.style.background_color.unwrap_or(DEBUG_COLOR).into()); + let clip_region_id = rect.style.border_radius.and_then(|border_radius| { + let region = ComplexClipRegion { + rect: bounds, + radii: border_radius, + mode: ClipMode::Clip, + }; + Some(builder.define_clip(bounds, vec![region], None)) + }); + + // Push clip + if let Some(id) = clip_region_id { + builder.push_clip_id(id); + } + + // Push basic rect + optional background + match rect.style.background_color { + Some(background_color) => builder.push_rect(&info, background_color.into()), + None => builder.push_clear_rect(&info), + } // Push background gradient / image if let Some(ref background) = rect.style.background { match *background { - Background::RadialGradient(ref _gradient) => { - + Background::RadialGradient(ref gradient) => { + let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| + GradientStop { + offset: gradient_pre.offset.unwrap(), + color: gradient_pre.color, + }).collect(); + let center = bounds.bottom_left(); // TODO - expose in CSS + let radius = TypedSize2D::new(40.0, 40.0); // TODO - expose in CSS + let gradient = builder.create_radial_gradient(center, radius, stops, gradient.extend_mode); + builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, Background::LinearGradient(ref gradient) => { let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| @@ -351,9 +378,20 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } } - use font::FontState; - use euclid::TypedPoint2D; - const FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 255, a: 150 }; + // Push border + if let Some((border_widths, mut border_details)) = rect.style.border { + if let Some(border_radius) = rect.style.border_radius { + if let BorderDetails::Normal(ref mut n) = border_details { + n.radius = border_radius; + } + } + builder.push_border(&info, border_widths, border_details); + } + + // Pop clip + if clip_region_id.is_some() { + builder.pop_clip_id(); + } // Push font if let Some(ref font_family) = rect.style.font_family { @@ -369,25 +407,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { index: glyph.id().0, point: TypedPoint2D::new(50.0, 50.0), }]; - builder.push_text(&info, &glyphs, font_instance_key, FONT_COLOR.into(), None); + builder.push_text(&info, &glyphs, font_instance_key, DEFAULT_FONT_COLOR.into(), None); } } - // Push border - if let Some((border_widths, mut border_details)) = rect.style.border { - if let Some(border_radius) = rect.style.border_radius { - if let BorderDetails::Normal(ref mut n) = border_details { - n.radius = border_radius; - } - } - builder.push_border(&info, border_widths, border_details); - } - - // Pop clip - if clip_region_id.is_some() { - builder.pop_clip_id(); - } - builder.pop_stacking_context(); } @@ -461,6 +484,7 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) rect.style.border_radius = parse!(constraint_list, "border-radius", parse_css_border_radius); rect.style.background_color = parse!(constraint_list, "background-color", parse_css_color); + rect.style.font_color = parse!(constraint_list, "color", parse_css_color); rect.style.border = parse!(constraint_list, "border", parse_css_border); rect.style.background = parse!(constraint_list, "background", parse_css_background); rect.style.font_size = parse!(constraint_list, "font-size", parse_css_font_size); @@ -470,6 +494,11 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) if let Some(box_shadow_opt) = box_shadow_opt{ rect.style.box_shadow = box_shadow_opt; } + if rect.style.font_color.is_none() { + // Be lenient - the correct CSS is to use "color", but it has tripped me + // up so often not to be able to use "font-color". + rect.style.font_color = parse!(constraint_list, "font-color", parse_css_color); + } } /// Populate and parse the CSS layout properties From b45748f36194865d0c45e44b7c965f3f36ba2ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 15:36:57 +0200 Subject: [PATCH 016/868] Added font color parsing and additional notes for displaying characters --- src/display_list.rs | 18 ++++++++++-------- src/window.rs | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index f82bb32ee..3882b172e 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -18,7 +18,7 @@ use cache::DomChangeSet; use std::sync::atomic::{Ordering, AtomicUsize}; const DEBUG_COLOR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 }; -const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 255, a: 255 }; +const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) ui_descr: &'a UiDescription, @@ -263,7 +263,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // have two touching rectangles let mut bounds = if rect.style.background_color.is_some() { LayoutRect::new( - LayoutPoint::new(50.0, 50.0), + LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 200.0), ) } else { @@ -388,12 +388,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { builder.push_border(&info, border_widths, border_details); } - // Pop clip - if clip_region_id.is_some() { - builder.pop_clip_id(); - } - // Push font + // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it if let Some(ref font_family) = rect.style.font_family { let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); let font_size = rect.style.font_size.unwrap_or(DEFAULT_FONT_SIZE); @@ -402,15 +398,21 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { if let Some(font_instance_key) = font_result { let font = &app_resources.font_data[font_id].0; + let font_color = rect.style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); let glyph = font.glyph('a'); // TODO: get label let glyphs = [GlyphInstance { index: glyph.id().0, point: TypedPoint2D::new(50.0, 50.0), }]; - builder.push_text(&info, &glyphs, font_instance_key, DEFAULT_FONT_COLOR.into(), None); + builder.push_text(&info, &glyphs, font_instance_key, font_color, None); } } + // Pop clip + if clip_region_id.is_some() { + builder.pop_clip_id(); + } + builder.pop_stacking_context(); } diff --git a/src/window.rs b/src/window.rs index b716d14dc..c897641cd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -453,6 +453,7 @@ impl Window { // Only create a context with VSync and SRGB if the context creation works let gl_window = GlWindow::new(window.clone(), create_context_builder(true, true), &events_loop) .or_else(|_| GlWindow::new(window.clone(), create_context_builder(true, false), &events_loop)) + .or_else(|_| GlWindow::new(window.clone(), create_context_builder(false, true), &events_loop)) .or_else(|_| GlWindow::new(window, create_context_builder(false, false), &events_loop))?; let hidpi_factor = gl_window.hidpi_factor(); From 3970162fe628050eff143f2da76e6124e72f954d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 19:39:04 +0200 Subject: [PATCH 017/868] Improved documentation and added doc-tests --- .travis.yml | 61 ++++++++++++++------------ Cargo.toml | 5 +++ examples/debug.rs | 9 ++-- examples/test_content.css | 3 +- src/app.rs | 90 +++++++++++++++++++++++++++++++++++---- src/app_state.rs | 77 ++++++++++++++++++++++++++++----- src/resources.rs | 17 +++++--- src/window.rs | 1 + 8 files changed, 207 insertions(+), 56 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13cce64d7..dc1007f40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,13 @@ language: rust +script: + - cargo build --all + - cargo build --examples + - cargo test --features "doc-test" sudo: false +cache: cargo + rust: - stable - beta @@ -10,33 +16,34 @@ rust: matrix: allow_failures: - rust: nightly + fast_finish: true -env: - global: - - RUSTFLAGS="-C link-dead-code" +# env: +# global: +# - RUSTFLAGS="-C link-dead-code" -addons: - apt: - packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - cmake - - gcc - - binutils-dev - - libiberty-dev +# addons: +# apt: +# packages: +# - libcurl4-openssl-dev +# - libelf-dev +# - libdw-dev +# - cmake +# - gcc +# - binutils-dev +# - libiberty-dev -after_success: | - wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && - tar xzf master.tar.gz && - cd kcov-master && - mkdir build && - cd build && - cmake .. && - make && - make install DESTDIR=../../kcov-build && - cd ../.. && - rm -rf kcov-master && - for file in target/debug/azul-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && - bash <(curl -s https://codecov.io/bash) && - echo "Uploaded code coverage" \ No newline at end of file +# after_success: | +# wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && +# tar xzf master.tar.gz && +# cd kcov-master && +# mkdir build && +# cd build && +# cmake .. && +# make && +# make install DESTDIR=../../kcov-build && +# cd ../.. && +# rm -rf kcov-master && +# for file in target/debug/azul-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && +# bash <(curl -s https://codecov.io/bash) && +# echo "Uploaded code coverage" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 4a0989fca..4a8996f2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,8 @@ euclid = "0.17" image = "0.18.0" rusttype = "0.5.2" app_units = "0.6" + +[features] +# The reason we do this is because doctests don't get cfg(test) +# See: https://github.com/rust-lang/cargo/issues/4669 +doc-test = [] \ No newline at end of file diff --git a/examples/debug.rs b/examples/debug.rs index c6f769790..3887b9f04 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -46,14 +46,13 @@ fn main() { let mut app = App::new(my_app_data); app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); -/* - app.remove_font("Webly Sleeky UI"); -*/ + // app.delete_font("Webly Sleeky UI"); + app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); - // app.remove_image("Cat01"); + // app.delete_image("Cat01"); // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that app.create_window(WindowCreateOptions::default(), css).unwrap(); - app.start_render_loop(); + app.run(); } diff --git a/examples/test_content.css b/examples/test_content.css index 97d9ac383..80ca9ee48 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -1,6 +1,6 @@ .__azul-native-button { background-color: #fcfcfc; - color: #000000; + font-color: tomato; border: 1px solid #b7b7b7; border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; @@ -20,4 +20,5 @@ * { font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; + font-color: green; } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 1ae6a4ea4..8cab42922 100644 --- a/src/app.rs +++ b/src/app.rs @@ -63,7 +63,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { } /// Start the rendering loop for the currently open windows - pub fn start_render_loop(&mut self) + pub fn run(&mut self) { let mut ui_state_cache = Vec::with_capacity(self.windows.len()); let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; @@ -240,20 +240,20 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_image>(&mut self, id: S) + pub fn delete_image>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).delete_image(id) } /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) + pub fn has_image>(&mut self, id: S) -> bool { (*self.app_state.lock().unwrap()).has_image(id) } - /// Add a font (TTF or OTF) to the internal resources + /// Add a font (TTF or OTF) as a resource, identified by ID /// /// ## Returns /// @@ -266,14 +266,88 @@ impl<'a, T: LayoutScreen> App<'a, T> { (*self.app_state.lock().unwrap()).add_font(id, data) } - /// Removes a font from the internal app resources. - /// Returns `Some` if the image existed and was removed. - /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn remove_font>(&mut self, id: S) + /// Checks if a font is currently registered and ready-to-use + pub fn has_font>(&mut self, id: S) + -> bool + { + (*self.app_state.lock().unwrap()).has_font(id) + } + + /// Deletes a font from the internal app resources. + /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. + /// + /// ## Returns + /// + /// - `Some(())` if if the image existed and was successfully removed + /// - `None` if the given ID doesn't exist. In that case, the function does + /// nothing. + /// + /// Wrapper function for [`AppState::add_font`]. After this function has been + /// called, you can be sure that the renderer doesn't know about your font anymore. + /// This also means that the font needs to be re-parsed if you want to add it again. + /// Use with care. + /// + /// ## Example + /// + /// ``` + /// # use azul::prelude::*; + /// # const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); + /// # + /// # struct MyAppData { } + /// # + /// # impl LayoutScreen for MyAppData { + /// # fn get_dom(&self, _window_id: WindowId) -> Dom { + /// # Dom::new(NodeType::Div) + /// # } + /// # } + /// # + /// # fn main() { + /// let mut app = App::new(MyAppData { }); + /// app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + /// app.delete_font("Webly Sleeky UI"); + /// // NOTE: The font isn't immediately removed, only in the next draw call + /// app.mock_render_frame(); + /// assert!(!app.has_font("Webly Sleeky UI")); + /// # } + /// ``` + /// + /// [`AppState::delete_font`]: ../app_state/struct.AppState.html#method.delete_font + pub fn delete_font>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).delete_font(id) } + + /// Mock rendering function, for creating a hidden window and rendering one frame + /// Used in unit tests + #[cfg(any(feature = "doc-test"))] + pub fn mock_render_frame(&mut self) { + use window::WindowClass; + let hidden_create_options = WindowCreateOptions { + class: WindowClass::Hidden, + .. Default::default() + }; + self.create_window(hidden_create_options, Css::native()).unwrap(); + let mut ui_state_cache = Vec::with_capacity(self.windows.len()); + let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; + let mut app_state = self.app_state.lock().unwrap(); + + for (idx, _) in self.windows.iter().enumerate() { + ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); + } + + for (idx, window) in self.windows.iter_mut().enumerate() { + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + render(window, &WindowId { id: idx, }, + &ui_description_cache[idx], + &mut app_state.resources, + true); + window.display.swap_buffers().unwrap(); + } + } } fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { diff --git a/src/app_state.rs b/src/app_state.rs index 7c34f1a3f..267508be8 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -37,6 +37,12 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { { self.resources.add_image(id, data, image_type) } + /// Checks if an image is currently registered and ready-to-use + pub fn has_image>(&mut self, id: S) + -> bool + { + self.resources.has_image(id) + } /// Removes an image from the internal app resources. /// Returns `Some` if the image existed and was removed. @@ -47,29 +53,80 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { self.resources.delete_image(id) } - /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) - -> bool - { - self.resources.has_image(id) - } - /// Add a font (TTF or OTF) to the internal resources /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to add, e.g. `"Helvetica-Bold"`. + /// - `data`: The bytes of the .ttf or .otf font file. Can be anything + /// that is read-able, i.e. a File, a network stream, etc. + /// /// ## Returns /// /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded + /// + /// ## Example + /// + /// This function exists so you can add functions to the app-internal state + /// at runtime in a [`Callback`](../dom/enum.Callback.html) function. + /// + /// Here is an example of how to add a font at runtime (when the app is already running): + /// + /// ``` + /// # use azul::prelude::*; + /// const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); + /// + /// struct MyAppData { } + /// + /// impl LayoutScreen for MyAppData { + /// fn get_dom(&self, _window_id: WindowId) -> Dom { + /// let mut dom = Dom::new(NodeType::Div); + /// dom.event(On::MouseEnter, Callback::Sync(my_callback)); + /// dom + /// } + /// } + /// + /// fn my_callback(app_state: &mut AppState) -> UpdateScreen { + /// /// Here you can add your font at runtime to the app_state + /// app_state.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + /// UpdateScreen::DontRedraw + /// } + /// ``` pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, FontError> { self.resources.add_font(id, data) } - /// Removes a font from the internal app resources. - /// Returns `Some` if the image existed and was removed. - /// If the given ID doesn't exist, this function does nothing and returns `None`. + /// Checks if a font is currently registered and ready-to-use + pub fn has_font>(&mut self, id: S) + -> bool + { + self.resources.has_font(id) + } + + /// Deletes a font from the internal app resources. + /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. + /// + /// ## Returns + /// + /// - `Some(())` if if the image existed and was successfully removed + /// - `None` if the given ID doesn't exist. In that case, the function does + /// nothing. + /// + /// After this function has been + /// called, you can be sure that the renderer doesn't know about your font anymore. + /// This also means that the font needs to be re-parsed if you want to add it again. + /// Use with care. + /// + /// You can also call this function on an `App` struct, see [`App::add_font`]. + /// + /// [`App::add_font`]: ../app/struct.App.html#method.add_font pub fn delete_font>(&mut self, id: S) -> Option<()> { diff --git a/src/resources.rs b/src/resources.rs index 647193032..fd51b5ca8 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -39,7 +39,7 @@ pub(crate) struct AppResources<'a> { impl<'a> AppResources<'a> { /// See `AppState::add_image()` - pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) + pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { use images; // the module, not the crate! @@ -58,7 +58,7 @@ impl<'a> AppResources<'a> { } /// See `AppState::delete_image()` - pub fn delete_image>(&mut self, id: S) + pub(crate) fn delete_image>(&mut self, id: S) -> Option<()> { match self.images.get_mut(id.as_ref()) { @@ -77,14 +77,14 @@ impl<'a> AppResources<'a> { } /// See `AppState::has_image()` - pub fn has_image>(&mut self, id: S) + pub(crate) fn has_image>(&mut self, id: S) -> bool { - false + self.images.get(id.as_ref()).is_some() } /// See `AppState::add_font()` - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + pub(crate) fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, FontError> { use font; @@ -101,6 +101,13 @@ impl<'a> AppResources<'a> { } } + /// Checks if a font is currently registered and ready-to-use + pub(crate) fn has_font>(&mut self, id: S) + -> bool + { + self.font_data.get(id.as_ref()).is_some() + } + /// See `AppState::delete_font()` pub(crate) fn delete_font>(&mut self, id: S) -> Option<()> diff --git a/src/window.rs b/src/window.rs index c897641cd..63c1de2e3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -419,6 +419,7 @@ impl Window { .with_dimensions(options.size.width, options.size.height) .with_title(options.title.clone()) .with_decorations(options.decorations != WindowDecorations::NoDecorations) + .with_visibility(options.class != WindowClass::Hidden) .with_maximized(options.class == WindowClass::Maximized); if options.class == WindowClass::FullScreen { From b274797ac4451d2d9cdd5beb779407e840856ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 20:05:34 +0200 Subject: [PATCH 018/868] More documentation, added X11 to Travis --- .travis.yml | 18 +++++++++++++++++- src/app.rs | 8 ++++++-- src/app_state.rs | 17 ++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc1007f40..f0aa4c0de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,16 @@ language: rust + +# Emulate an X11 virtual framebuffer, since Travis +# runs on a headless system +# before_script: +# - "export DISPLAY=:99.0" +# - "sh -e /etc/init.d/xvfb start" +# - sleep 3 # give xvfb some time to start + script: - cargo build --all - cargo build --examples - - cargo test --features "doc-test" + - xvfb-run --server-args "-screen 0 1920x1080x24" cargo test --features "doc-test" sudo: false @@ -22,6 +30,14 @@ matrix: # global: # - RUSTFLAGS="-C link-dead-code" +addons: + apt: + packages: + - xvfb + install: + - export DISPLAY=':99.0' + - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + # addons: # apt: # packages: diff --git a/src/app.rs b/src/app.rs index 8cab42922..f1132ca01 100644 --- a/src/app.rs +++ b/src/app.rs @@ -48,7 +48,7 @@ impl Default for FrameEventInfo { impl<'a, T: LayoutScreen> App<'a, T> { - /// Create a new, empty application (note: doesn't create a window!) + /// Create a new, empty application. This does not open any windows. pub fn new(initial_data: T) -> Self { Self { windows: Vec::new(), @@ -56,13 +56,17 @@ impl<'a, T: LayoutScreen> App<'a, T> { } } - /// Spawn a new window on the screen + /// Spawn a new window on the screen. If an application has no windows, + /// the [`run`](#method.run) function will exit immediately. pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { self.windows.push(Window::new(options, css)?); Ok(()) } /// Start the rendering loop for the currently open windows + /// This is the "main app loop", "main game loop" or whatever you want to call it. + /// Usually this is the last function you call in your `main()` function, since exiting + /// it means that the user has closed all windows and wants to close the app. pub fn run(&mut self) { let mut ui_state_cache = Vec::with_capacity(self.windows.len()); diff --git a/src/app_state.rs b/src/app_state.rs index 267508be8..f70874390 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -25,13 +25,28 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { } } - /// Add an image to the internal resources + /// Add an image to the internal resources. /// + /// ## Arguments + /// + /// - `id`: A stringified ID (hash) for the image. It's recommended to use the + /// file path as the hash, maybe combined with a timestamp or a hash + /// of the file contents if the image will change. + /// - `data`: The data of the image - can be a File, a network stream, etc. + /// - `image_type`: If you know the type of image that you are adding, it is + /// recommended to specify it. In case you don't know, use + /// [`ImageType::GuessImageFormat`] + /// /// ## Returns /// /// - `Ok(Some(()))` if an image with the same ID already exists. /// - `Ok(None)` if the image was added, but didn't exist previously. /// - `Err(e)` if the image couldn't be decoded + /// + /// **NOTE:** This function blocks the current thread. + /// + /// [`ImageType::GuessImageFormat`]: ../prelude/enum.ImageType.html#variant.GuessImageFormat + /// pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { From 4f7331011c4b7d1ece71f9bcb14bf229fde153c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 20:39:22 +0200 Subject: [PATCH 019/868] Added force-override to force a hardware or software renderer --- src/app.rs | 16 ++++++++++++++-- src/lib.rs | 2 +- src/window.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index f1132ca01..bb80df144 100644 --- a/src/app.rs +++ b/src/app.rs @@ -326,12 +326,24 @@ impl<'a, T: LayoutScreen> App<'a, T> { } /// Mock rendering function, for creating a hidden window and rendering one frame - /// Used in unit tests + /// Used in unit tests. You **have** to enable software rendering, otherwise, + /// this function won't work in a headless environment. + /// + /// **NOTE**: In a headless environment, such as Travis, you have to use XVFB to + /// create a fake X11 server. XVFB also has a bug where it loads with the default of + /// 8-bit greyscale color (see [here]). In order to fix that, you have to run: + /// + /// `xvfb-run --server-args "-screen 0 1920x1080x24" cargo test --features "doc-test"` + /// + /// [here]: https://unix.stackexchange.com/questions/104914/ + /// #[cfg(any(feature = "doc-test"))] pub fn mock_render_frame(&mut self) { - use window::WindowClass; + use prelude::*; let hidden_create_options = WindowCreateOptions { class: WindowClass::Hidden, + /// force sofware renderer (OSMesa) + renderer_type: RendererType::Software, .. Default::default() }; self.create_window(hidden_create_options, Css::native()).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 3de64dc25..6cec0854a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,7 +86,7 @@ pub mod prelude { pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, WindowPlacement}; pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, - WindowCreateError, WindowDecorations, WindowMonitorTarget}; + WindowCreateError, WindowDecorations, WindowMonitorTarget, RendererType}; pub use font::FontError; pub use images::ImageType; diff --git a/src/window.rs b/src/window.rs index 63c1de2e3..f2a887ef9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -61,6 +61,8 @@ pub struct WindowCreateOptions { pub size: WindowPlacement, /// What type of window (full screen, popup, normal) pub class: WindowClass, + /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? + pub renderer_type: RendererType, } impl Default for WindowCreateOptions { @@ -77,10 +79,36 @@ impl Default for WindowCreateOptions { decorations: WindowDecorations::default(), size: WindowPlacement::default(), class: WindowClass::default(), + renderer_type: RendererType::default(), } } } +/// Force a specific renderer. +/// By default, azul will try to use the hardware renderer and fall +/// back to the software renderer if it can't create an OpenGL 3.2 context. +/// However, in some cases a hardware renderer might create problems +/// or you want to force either a software or hardware renderer. +/// +/// If the field `renderer_type` on the `WindowCreateOptions` is not +/// `RendererType::Default`, the `create_window` method will try to create +/// a window with the specific renderer type and **crash** if the renderer is +/// not available for whatever reason. +/// +/// If you don't know what any of this means, leave it at `Default`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RendererType { + Default, + Hardware, + Software, +} + +impl Default for RendererType { + fn default() -> Self { + RendererType::Default + } +} + /// How should the window be decorated #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum WindowDecorations { @@ -501,9 +529,6 @@ impl Window { } } - let opts_native = get_renderer_opts(true, device_pixel_ratio, Some(options.background)); - let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); - let framebuffer_size = { #[allow(deprecated)] let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); @@ -523,8 +548,25 @@ impl Window { glutin::Api::WebGl => return Err(WindowCreateError::WebGlNotSupported), }; - let (renderer, sender) = Renderer::new(gl.clone(), notifier.clone(), opts_native) - .or_else(|_| Renderer::new(gl, notifier, opts_osmesa)).unwrap(); + let opts_native = get_renderer_opts(true, device_pixel_ratio, Some(options.background)); + let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); + + use self::RendererType::*; + let (renderer, sender) = match options.renderer_type { + Hardware => { + // force hardware renderer + Renderer::new(gl, notifier, opts_native).unwrap() + }, + Software => { + // force software renderer + Renderer::new(gl, notifier, opts_osmesa).unwrap() + }, + Default => { + // try hardware first, fall back to software + Renderer::new(gl.clone(), notifier.clone(), opts_native).or_else(|_| + Renderer::new(gl, notifier, opts_osmesa)).unwrap() + } + }; let api = sender.create_api(); let document_id = api.add_document(framebuffer_size, 0); From 389b8726aa1ecb4c203097f4a782574149e597b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 25 Mar 2018 21:03:11 +0200 Subject: [PATCH 020/868] Disabled OpenGL-based unit tests on Travis --- .travis.yml | 19 +++---------------- Cargo.toml | 8 +++++++- src/app.rs | 5 +++-- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index f0aa4c0de..2f1dc5120 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,11 @@ language: rust -# Emulate an X11 virtual framebuffer, since Travis -# runs on a headless system -# before_script: -# - "export DISPLAY=:99.0" -# - "sh -e /etc/init.d/xvfb start" -# - sleep 3 # give xvfb some time to start - +# We can't test OpenGL 3.2 on Travis, the shader compilation fails +# because glium does a check first if it has a OGL 3.2 context script: - cargo build --all - cargo build --examples - - xvfb-run --server-args "-screen 0 1920x1080x24" cargo test --features "doc-test" + - RUST_BACKTRACE=1 cargo test --features "doc-test no-opengl-tests" sudo: false @@ -30,14 +25,6 @@ matrix: # global: # - RUSTFLAGS="-C link-dead-code" -addons: - apt: - packages: - - xvfb - install: - - export DISPLAY=':99.0' - - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - # addons: # apt: # packages: diff --git a/Cargo.toml b/Cargo.toml index 4a8996f2f..4c10c375c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,10 @@ app_units = "0.6" [features] # The reason we do this is because doctests don't get cfg(test) # See: https://github.com/rust-lang/cargo/issues/4669 -doc-test = [] \ No newline at end of file +doc-test = [] + +# Some test have to be disabled for Travis, since Travis does not +# use OpenGL 3.2, so the tests will fail +# +# To actually test the library, run cargo --test --features=doc-test +no-opengl-tests = [] \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index bb80df144..7ac65288c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -289,14 +289,15 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// - `None` if the given ID doesn't exist. In that case, the function does /// nothing. /// - /// Wrapper function for [`AppState::add_font`]. After this function has been + /// Wrapper function for [`AppState::delete_font`]. After this function has been /// called, you can be sure that the renderer doesn't know about your font anymore. /// This also means that the font needs to be re-parsed if you want to add it again. /// Use with care. /// /// ## Example /// - /// ``` + #[cfg_attr(feature = "no-opengl-tests", doc = " ```no_run")] + #[cfg_attr(not(feature = "no-opengl-tests"), doc = " ```")] /// # use azul::prelude::*; /// # const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); /// # From 203e2c227f27a38e9a06ba8dbfa2b3f32db3ce2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 26 Mar 2018 01:06:18 +0200 Subject: [PATCH 021/868] Added font positioning, font is currently not showing --- examples/test_content.css | 1 + src/display_list.rs | 373 ++++++++++++++++++++++++-------------- src/lib.rs | 14 +- 3 files changed, 248 insertions(+), 140 deletions(-) diff --git a/examples/test_content.css b/examples/test_content.css index 80ca9ee48..f45fad982 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -21,4 +21,5 @@ font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; font-color: green; + background-color: red; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 3882b172e..c5993b3e6 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -250,9 +250,6 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { for (rect_idx, rect) in self.rectangles.iter() { - use font::FontState; - use euclid::{TypedSize2D, TypedPoint2D, TypedTransform3D, Angle}; - // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); @@ -280,17 +277,9 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { tag: rect.tag.and_then(|tag| Some((tag, 0))), }; -/* - builder.push_stacking_context( - &info, - ScrollPolicy::Scrollable, - None, - TransformStyle::Preserve3D, - Some(TypedTransform3D::create_skew(Angle{ radians: 20.0_f32.to_radians() }, Angle{ radians: 20.0_f32.to_radians() })), // TODO: expose 3D-transform in CSS - MixBlendMode::HardLight, // TODO: expose blend-modes in CSS - vec![FilterOp::Blur(3.0)], // TODO: expose filters (blur, hue, etc.) in CSS - ); -*/ + // TODO: expose 3D-transform in CSS + // TODO: expose blend-modes in CSS + // TODO: expose filters (blur, hue, etc.) in CSS builder.push_stacking_context( &info, ScrollPolicy::Scrollable, @@ -302,17 +291,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { ); // Push box shadow, before the clip is active - if let Some(ref pre_shadow) = rect.style.box_shadow { - // The pre_shadow is missing the BorderRadius & LayoutRect - let border_radius = rect.style.border_radius.unwrap_or(BorderRadius::zero()); - // Currently the box shadow is blurred across the whole window - // This can be possibly optimized further - let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), full_screen_rect); - builder.push_box_shadow(&info, bounds, pre_shadow.offset, pre_shadow.color, - pre_shadow.blur_radius, pre_shadow.spread_radius, - border_radius, pre_shadow.clip_mode); - - } + push_box_shadow(&mut builder, &rect.style, &bounds, &full_screen_rect); let clip_region_id = rect.style.border_radius.and_then(|border_radius| { let region = ComplexClipRegion { @@ -328,85 +307,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { builder.push_clip_id(id); } - // Push basic rect + optional background - match rect.style.background_color { - Some(background_color) => builder.push_rect(&info, background_color.into()), - None => builder.push_clear_rect(&info), - } - - // Push background gradient / image - if let Some(ref background) = rect.style.background { - match *background { - Background::RadialGradient(ref gradient) => { - let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| - GradientStop { - offset: gradient_pre.offset.unwrap(), - color: gradient_pre.color, - }).collect(); - let center = bounds.bottom_left(); // TODO - expose in CSS - let radius = TypedSize2D::new(40.0, 40.0); // TODO - expose in CSS - let gradient = builder.create_radial_gradient(center, radius, stops, gradient.extend_mode); - builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); - }, - Background::LinearGradient(ref gradient) => { - let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| - GradientStop { - offset: gradient_pre.offset.unwrap(), - color: gradient_pre.color, - }).collect(); - let (begin_pt, end_pt) = gradient.direction.to_points(&bounds); - let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); - builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); - }, - Background::Image(image_id) => { - if let Some(image_info) = app_resources.images.get(image_id.0) { - use images::ImageState::*; - match *image_info { - Uploaded(ref image_info) => { - builder.push_image( - &info, - bounds.size, - LayoutSize::zero(), - ImageRendering::Auto, - AlphaType::Alpha, - image_info.key); - }, - _ => { }, - } - } - } - } - } - - // Push border - if let Some((border_widths, mut border_details)) = rect.style.border { - if let Some(border_radius) = rect.style.border_radius { - if let BorderDetails::Normal(ref mut n) = border_details { - n.radius = border_radius; - } - } - builder.push_border(&info, border_widths, border_details); - } - - // Push font - // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it - if let Some(ref font_family) = rect.style.font_family { - let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); - let font_size = rect.style.font_size.unwrap_or(DEFAULT_FONT_SIZE); - let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX); - let font_result = push_font(font_id, font_size_app_units, &mut resource_updates, app_resources, render_api); - - if let Some(font_instance_key) = font_result { - let font = &app_resources.font_data[font_id].0; - let font_color = rect.style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); - let glyph = font.glyph('a'); // TODO: get label - let glyphs = [GlyphInstance { - index: glyph.id().0, - point: TypedPoint2D::new(50.0, 50.0), - }]; - builder.push_text(&info, &glyphs, font_instance_key, font_color, None); - } - } + push_rect(&info, &mut builder, &rect.style); + push_background(&info, &bounds, &mut builder, &rect.style, &app_resources); + push_border(&info, &mut builder, &rect.style); + push_text(&info, &self, *rect_idx, &mut builder, &rect.style, app_resources, &render_api, &bounds, &mut resource_updates); // Pop clip if clip_region_id.is_some() { @@ -423,14 +327,202 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; +use euclid::{TypedRect, TypedSize2D}; + +#[inline] +fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { + match style.background_color { + Some(background_color) => builder.push_rect(&info, background_color.into()), + None => builder.push_clear_rect(&info), + } +} + +#[inline] +fn push_text( + info: &PrimitiveInfo, + display_list: &DisplayList, + rect_idx: NodeId, + builder: &mut DisplayListBuilder, + style: &RectStyle, + app_resources: &mut AppResources, + render_api: &RenderApi, + bounds: &TypedRect, + resource_updates: &mut ResourceUpdates) +{ + use dom::NodeType::*; + use euclid::{TypedPoint2D}; + + // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it + let arena = display_list.ui_descr.ui_descr_arena.borrow(); + + let text = match arena[rect_idx].data.node_type { + Div => return, + Label { ref text } => { + text + }, + _ => { + /// The display list should only ever handle divs and labels. + /// Everything more complex should be handled b + println!("got a NodeType in a DisplayList that wasn't a div or a label, this is a bug"); + // unreachable!(); + return; + } + }; + + if text.is_empty() { + return; + } + let font_family = match style.font_family { + Some(ref ff) => ff, + None => return, + }; + + let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); + let font_size_pixels = font_size.0.to_pixels(); + let font_size_app_units = Au((font_size_pixels as i32) * AU_PER_PX); + let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); + let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); + let font_instance_key = match font_result { + Some(f) => f, + None => return, + }; + + let font = &app_resources.font_data[font_id].0; + let positioned_glyphs = put_text_in_bounds(text, font, font_size_pixels, bounds); + + let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); + builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, None); +} + +#[inline] +fn put_text_in_bounds<'a>(text: &str, font: &::rusttype::Font<'a>, font_size_pixels: f32, bounds: &TypedRect) -> Vec { + use euclid::TypedPoint2D; + use rusttype::Scale; + + let mut line_y = bounds.origin.y; + let mut line_x = bounds.origin.x; + + text.chars().map(|ch| { + let glyph = font.glyph(ch); + let idx = glyph.id().0; + let scaled_glyph = glyph.scaled(Scale::uniform(font_size_pixels)); + let h_metrics = scaled_glyph.h_metrics(); + + if line_x > (bounds.origin.x + bounds.size.width) { + line_y += font_size_pixels; + line_x = bounds.origin.x; + } + println!("pushing glyph {} at: {:?} x {:?} y", ch, line_x, line_y); + let glyph_instance = GlyphInstance { + index: idx, + point: TypedPoint2D::new(line_x, line_y), + }; + line_x += h_metrics.advance_width; + glyph_instance + }).collect() +} + +#[inline] +fn push_box_shadow( + builder: &mut DisplayListBuilder, + style: &RectStyle, + bounds: &TypedRect, + full_screen_rect: &TypedRect) +{ + let pre_shadow = match style.box_shadow { + Some(ref ps) => ps, + None => return, + }; + + // The pre_shadow is missing the BorderRadius & LayoutRect + let border_radius = style.border_radius.unwrap_or(BorderRadius::zero()); + + // Currently the box shadow is blurred across the whole window + // This can be possibly optimized further + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), *full_screen_rect); + builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, + pre_shadow.blur_radius, pre_shadow.spread_radius, + border_radius, pre_shadow.clip_mode); + +} + +#[inline] +fn push_background( + info: &PrimitiveInfo, + bounds: &TypedRect, + builder: &mut DisplayListBuilder, + style: &RectStyle, + app_resources: &AppResources) +{ + let background = match style.background { + Some(ref bg) => bg, + None => return, + }; + + match *background { + Background::RadialGradient(ref gradient) => { + let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| + GradientStop { + offset: gradient_pre.offset.unwrap(), + color: gradient_pre.color, + }).collect(); + let center = bounds.bottom_left(); // TODO - expose in CSS + let radius = TypedSize2D::new(40.0, 40.0); // TODO - expose in CSS + let gradient = builder.create_radial_gradient(center, radius, stops, gradient.extend_mode); + builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); + }, + Background::LinearGradient(ref gradient) => { + let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| + GradientStop { + offset: gradient_pre.offset.unwrap(), + color: gradient_pre.color, + }).collect(); + let (begin_pt, end_pt) = gradient.direction.to_points(&bounds); + let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); + builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); + }, + Background::Image(image_id) => { + if let Some(image_info) = app_resources.images.get(image_id.0) { + use images::ImageState::*; + match *image_info { + Uploaded(ref image_info) => { + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + image_info.key); + }, + _ => { }, + } + } + } + } +} + +#[inline] +fn push_border(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { + if let Some((border_widths, mut border_details)) = style.border { + if let Some(border_radius) = style.border_radius { + if let BorderDetails::Normal(ref mut n) = border_details { + n.radius = border_radius; + } + } + builder.push_border(info, border_widths, border_details); + } +} + +#[inline] fn push_font( font_id: &str, font_size_app_units: Au, resource_updates: &mut ResourceUpdates, app_resources: &mut AppResources, - render_api: &RenderApi) -> Option { - + render_api: &RenderApi) +-> Option +{ use font::FontState; if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { @@ -469,55 +561,68 @@ fn push_font( return None; } -macro_rules! parse { - ($constraint_list:ident, $key:expr, $func:tt) => ( - $constraint_list.get($key).and_then(|w| $func(w).map_err(|e| { - #[cfg(debug_assertions)] - println!("ERROR - invalid {:?}: {:?}", e, $key); - e - }).ok()) - ) +use ui_description::CssConstraintList; +use std::fmt::Debug; + +/// Internal helper function - gets a key from the constraint list and passes it through +/// the parse_func - if an error occurs, then the error gets printed +fn parse<'a, T, E: Debug>( + constraint_list: &'a CssConstraintList, + key: &'static str, + parse_func: fn(&'a str) -> Result) +-> Option +{ + #[inline(always)] + fn print_error_debug(err: &E, key: &'static str) { + eprintln!("ERROR - invalid {:?}: {:?}", err, key); + } + + constraint_list.list.get(key).and_then(|w| parse_func(w).map_err(|e| { + #[cfg(debug_assertions)] + print_error_debug(&e, key); + e + }).ok()) } /// Populate and parse the CSS style properties fn parse_css_style_properties(rect: &mut DisplayRectangle) { - let constraint_list = &rect.styled_node.css_constraints.list; + let constraint_list = &rect.styled_node.css_constraints; - rect.style.border_radius = parse!(constraint_list, "border-radius", parse_css_border_radius); - rect.style.background_color = parse!(constraint_list, "background-color", parse_css_color); - rect.style.font_color = parse!(constraint_list, "color", parse_css_color); - rect.style.border = parse!(constraint_list, "border", parse_css_border); - rect.style.background = parse!(constraint_list, "background", parse_css_background); - rect.style.font_size = parse!(constraint_list, "font-size", parse_css_font_size); - rect.style.font_family = parse!(constraint_list, "font-family", parse_css_font_family); + rect.style.border_radius = parse(constraint_list, "border-radius", parse_css_border_radius); + rect.style.background_color = parse(constraint_list, "background-color", parse_css_color); + rect.style.font_color = parse(constraint_list, "color", parse_css_color); + rect.style.border = parse(constraint_list, "border", parse_css_border); + rect.style.background = parse(constraint_list, "background", parse_css_background); + rect.style.font_size = parse(constraint_list, "font-size", parse_css_font_size); + rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); - let box_shadow_opt = parse!(constraint_list, "box-shadow", parse_css_box_shadow); + let box_shadow_opt = parse(constraint_list, "box-shadow", parse_css_box_shadow); if let Some(box_shadow_opt) = box_shadow_opt{ rect.style.box_shadow = box_shadow_opt; } if rect.style.font_color.is_none() { // Be lenient - the correct CSS is to use "color", but it has tripped me // up so often not to be able to use "font-color". - rect.style.font_color = parse!(constraint_list, "font-color", parse_css_color); + rect.style.font_color = parse(constraint_list, "font-color", parse_css_color); } } /// Populate and parse the CSS layout properties fn parse_css_layout_properties(rect: &mut DisplayRectangle) { - let constraint_list = &rect.styled_node.css_constraints.list; + let constraint_list = &rect.styled_node.css_constraints; - rect.layout.width = parse!(constraint_list, "width", parse_layout_width); - rect.layout.height = parse!(constraint_list, "height", parse_layout_height); - rect.layout.min_width = parse!(constraint_list, "min-width", parse_layout_min_width); - rect.layout.min_height = parse!(constraint_list, "min-height", parse_layout_min_height); + rect.layout.width = parse(constraint_list, "width", parse_layout_width); + rect.layout.height = parse(constraint_list, "height", parse_layout_height); + rect.layout.min_width = parse(constraint_list, "min-width", parse_layout_min_width); + rect.layout.min_height = parse(constraint_list, "min-height", parse_layout_min_height); - rect.layout.wrap = parse!(constraint_list, "flex-wrap", parse_layout_wrap); - rect.layout.direction = parse!(constraint_list, "flex-direction", parse_layout_direction); - rect.layout.justify_content = parse!(constraint_list, "justify-content", parse_layout_justify_content); - rect.layout.align_items = parse!(constraint_list, "align-items", parse_layout_align_items); - rect.layout.align_content = parse!(constraint_list, "align-content", parse_layout_align_content); + rect.layout.wrap = parse(constraint_list, "flex-wrap", parse_layout_wrap); + rect.layout.direction = parse(constraint_list, "flex-direction", parse_layout_direction); + rect.layout.justify_content = parse(constraint_list, "justify-content", parse_layout_justify_content); + rect.layout.align_items = parse(constraint_list, "align-items", parse_layout_align_items); + rect.layout.align_content = parse(constraint_list, "align-content", parse_layout_align_content); } // Adds and removes layout constraints if necessary diff --git a/src/lib.rs b/src/lib.rs index 6cec0854a..1d816befc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,11 +36,11 @@ extern crate rusttype; extern crate app_units; /// Global application (Initialization starts here) -pub mod app; +mod app; /// Wrapper for the application data & application state -pub mod app_state; +mod app_state; /// Styling & CSS parsing -pub mod css; +mod css; /// DOM / HTML node handling pub mod dom; /// The layout traits for creating a layout-able application @@ -75,11 +75,13 @@ mod font; type FastHashMap = ::std::collections::HashMap>; type FastHashSet = ::std::collections::HashSet>; +pub use app::App; +pub use app_state::AppState; +pub use css::{CssRule, Css}; + /// Quick exports of common types pub mod prelude { - pub use app::App; - pub use app_state::AppState; - pub use css::{CssRule, Css}; + pub use {App, AppState, CssRule, Css}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; pub use traits::LayoutScreen; pub use webrender::api::{ColorF, ColorU}; From 68b90016aaaf619233bbabc03e43f19adec209c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 26 Mar 2018 11:12:06 +0200 Subject: [PATCH 022/868] Got font rendering to work, working on text layout --- src/display_list.rs | 108 +++++++++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index c5993b3e6..0c9f78ad8 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -95,12 +95,20 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } /// Looks if any new images need to be uploaded and stores the in the image resources - fn update_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { + fn update_resources( + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) + { Self::update_image_resources(api, app_resources, resource_updates); Self::update_font_resources(api, app_resources, resource_updates); } - fn update_image_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { + fn update_image_resources( + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) + { use images::{ImageState, ImageInfo}; let mut updated_images = Vec::<(String, (ImageData, ImageDescriptor))>::new(); @@ -142,8 +150,11 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // almost the same as update_image_resources, but fonts // have two HashMaps that need to be updated - fn update_font_resources(api: &RenderApi, app_resources: &mut AppResources, resource_updates: &mut ResourceUpdates) { - + fn update_font_resources( + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) + { use font::FontState; let mut updated_fonts = Vec::<(String, Vec)>::new(); @@ -241,7 +252,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { css.needs_relayout = false; - let mut builder = DisplayListBuilder::with_capacity(pipeline_id, ui_solver.window_dimensions.layout_size, self.rectangles.len()); + let layout_size = ui_solver.window_dimensions.layout_size; + let mut builder = DisplayListBuilder::with_capacity(pipeline_id, layout_size, self.rectangles.len()); let mut resource_updates = ResourceUpdates::new(); let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; @@ -291,7 +303,11 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { ); // Push box shadow, before the clip is active - push_box_shadow(&mut builder, &rect.style, &bounds, &full_screen_rect); + push_box_shadow( + &mut builder, + &rect.style, + &bounds, + &full_screen_rect); let clip_region_id = rect.style.border_radius.and_then(|border_radius| { let region = ComplexClipRegion { @@ -307,10 +323,33 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { builder.push_clip_id(id); } - push_rect(&info, &mut builder, &rect.style); - push_background(&info, &bounds, &mut builder, &rect.style, &app_resources); - push_border(&info, &mut builder, &rect.style); - push_text(&info, &self, *rect_idx, &mut builder, &rect.style, app_resources, &render_api, &bounds, &mut resource_updates); + push_rect( + &info, + &mut builder, + &rect.style); + + push_background( + &info, + &bounds, + &mut builder, + &rect.style, + &app_resources); + + push_border( + &info, + &mut builder, + &rect.style); + + push_text( + &info, + &self, + *rect_idx, + &mut builder, + &rect.style, + app_resources, + &render_api, + &bounds, + &mut resource_updates); // Pop clip if clip_region_id.is_some() { @@ -362,7 +401,7 @@ fn push_text( }, _ => { /// The display list should only ever handle divs and labels. - /// Everything more complex should be handled b + /// Everything more complex should be handled by a pre-processing step println!("got a NodeType in a DisplayList that wasn't a div or a label, this is a bug"); // unreachable!(); return; @@ -383,6 +422,7 @@ fn push_text( let font_size_app_units = Au((font_size_pixels as i32) * AU_PER_PX); let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); + let font_instance_key = match font_result { Some(f) => f, None => return, @@ -396,13 +436,25 @@ fn push_text( } #[inline] -fn put_text_in_bounds<'a>(text: &str, font: &::rusttype::Font<'a>, font_size_pixels: f32, bounds: &TypedRect) -> Vec { +fn put_text_in_bounds<'a>( + text: &str, + font: &::rusttype::Font<'a>, + font_size_pixels: f32, + bounds: &TypedRect) +-> Vec +{ use euclid::TypedPoint2D; use rusttype::Scale; - let mut line_y = bounds.origin.y; let mut line_x = bounds.origin.x; - + let mut line_y = bounds.origin.y + font_size_pixels; + let v_metrics = font.v_metrics(Scale::uniform(font_size_pixels)); + let units_per_em = font.units_per_em(); +/* + println!("unscaled: {:?}", font.v_metrics_unscaled()); + println!("got font size of: {:?}", font_size_pixels); + println!("units_per_em: {:?}", units_per_em); +*/ text.chars().map(|ch| { let glyph = font.glyph(ch); let idx = glyph.id().0; @@ -412,13 +464,14 @@ fn put_text_in_bounds<'a>(text: &str, font: &::rusttype::Font<'a>, font_size_pix if line_x > (bounds.origin.x + bounds.size.width) { line_y += font_size_pixels; line_x = bounds.origin.x; + } else { + line_x += h_metrics.advance_width; } println!("pushing glyph {} at: {:?} x {:?} y", ch, line_x, line_y); let glyph_instance = GlyphInstance { index: idx, point: TypedPoint2D::new(line_x, line_y), }; - line_x += h_metrics.advance_width; glyph_instance }).collect() } @@ -503,7 +556,11 @@ fn push_background( } #[inline] -fn push_border(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { +fn push_border( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + style: &RectStyle) +{ if let Some((border_widths, mut border_details)) = style.border { if let Some(border_radius) = style.border_radius { if let BorderDetails::Normal(ref mut n) = border_details { @@ -596,21 +653,19 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) rect.style.background = parse(constraint_list, "background", parse_css_background); rect.style.font_size = parse(constraint_list, "font-size", parse_css_font_size); rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); - - let box_shadow_opt = parse(constraint_list, "box-shadow", parse_css_box_shadow); - if let Some(box_shadow_opt) = box_shadow_opt{ + if let Some(box_shadow_opt) = parse(constraint_list, "box-shadow", parse_css_box_shadow) { rect.style.box_shadow = box_shadow_opt; } + if rect.style.font_color.is_none() { - // Be lenient - the correct CSS is to use "color", but it has tripped me - // up so often not to be able to use "font-color". + // Use "color" and "font-color" interchangeably, even though this isn't in the CSS spec rect.style.font_color = parse(constraint_list, "font-color", parse_css_color); } } /// Populate and parse the CSS layout properties -fn parse_css_layout_properties(rect: &mut DisplayRectangle) { - +fn parse_css_layout_properties(rect: &mut DisplayRectangle) +{ let constraint_list = &rect.styled_node.css_constraints; rect.layout.width = parse(constraint_list, "width", parse_layout_width); @@ -626,9 +681,10 @@ fn parse_css_layout_properties(rect: &mut DisplayRectangle) { } // Adds and removes layout constraints if necessary -fn create_layout_constraints(rect: &DisplayRectangle, - arena: &Arena>, - ui_solver: &mut UiSolver) +fn create_layout_constraints( + rect: &DisplayRectangle, + arena: &Arena>, + ui_solver: &mut UiSolver) where T: LayoutScreen { use css_parser; From bd9b03fc0a0e681d53ff767eeba730a7f1a9d673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 26 Mar 2018 13:19:39 +0200 Subject: [PATCH 023/868] Refactored glyph positioning --- examples/debug.rs | 8 +- examples/test_content.css | 5 +- src/css_parser.rs | 46 ++++++++++++ src/display_list.rs | 150 +++++++++++++------------------------- src/lib.rs | 2 + src/text_layout.rs | 117 +++++++++++++++++++++++++++++ 6 files changed, 219 insertions(+), 109 deletions(-) create mode 100644 src/text_layout.rs diff --git a/examples/debug.rs b/examples/debug.rs index 3887b9f04..12b942a6a 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -20,11 +20,9 @@ impl LayoutScreen for MyAppData { dom.class("__azul-native-button"); dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - for i in 0..self.my_data { - dom.add_sibling(Dom::new(NodeType::Label { - text: format!("{}", i), - })); - } + dom.add_sibling(Dom::new(NodeType::Label { + text: String::from("This is a very long text that should break on to multiple lines aldkf jasdölfkjas öldfkjasdölkf jasöl dfkj"), + })); dom } diff --git a/examples/test_content.css b/examples/test_content.css index f45fad982..cbadf1498 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -1,6 +1,6 @@ .__azul-native-button { background-color: #fcfcfc; - font-color: tomato; + color: tomato; border: 1px solid #b7b7b7; border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; @@ -20,6 +20,5 @@ * { font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; - font-color: green; - background-color: red; + color: red; } \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index 97f3132be..905978544 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1250,6 +1250,37 @@ pub enum LayoutAlignContent { SpaceAround, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TextOverflowBehaviour { + /// Always shows a scroll bar, overflows on scroll + Scroll, + /// Does not show a scroll bar by default, only when text is overflowing + Auto, + /// Never shows a scroll bar, simply clips text + Hidden, + /// Doesn't show a scroll bar, simply overflows the text + Visible, +} + +impl Default for TextOverflowBehaviour { + fn default() -> Self { + TextOverflowBehaviour::Auto + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TextAlignment { + Center, + Left, + Right, +} + +impl Default for TextAlignment { + fn default() -> Self { + TextAlignment::Left + } +} + #[derive(Default, Debug, Clone, PartialEq)] pub(crate) struct RectStyle<'a> { /// Background color of this rectangle @@ -1268,6 +1299,10 @@ pub(crate) struct RectStyle<'a> { pub(crate) font_family: Option>, /// Text color pub(crate) font_color: Option, + /// Text alignment + pub(crate) text_align: Option, + /// Text overflow behaviour + pub(crate) text_overflow: Option, } // Layout constraints for a given rectangle, such as "" @@ -1393,6 +1428,17 @@ multi_type_parser!(parse_shape, Shape, ["circle", Circle], ["ellipse", Ellipse]); +multi_type_parser!(parse_text_overflow, TextOverflowBehaviour, + ["auto", Auto], + ["scroll", Scroll], + ["visible", Visible], + ["hidden", Hidden]); + +multi_type_parser!(parse_text_align, TextAlignment, + ["center", Center], + ["left", Left], + ["right", Right]); + #[cfg(test)] mod css_tests { use super::*; diff --git a/src/display_list.rs b/src/display_list.rs index 0c9f78ad8..7826520b5 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -16,8 +16,9 @@ use std::collections::BTreeMap; use FastHashMap; use cache::DomChangeSet; use std::sync::atomic::{Ordering, AtomicUsize}; +use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; +use euclid::{TypedRect, TypedSize2D}; -const DEBUG_COLOR: ColorU = ColorU { r: 255, g: 0, b: 0, a: 255 }; const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { @@ -265,22 +266,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); - // debugging - there are currently two rectangles on the screen - // if the rectangle doesn't have a background color, choose the first bound - // - // this means, since the DOM in the debug example has two rectangles, we should - // have two touching rectangles - let mut bounds = if rect.style.background_color.is_some() { - LayoutRect::new( - LayoutPoint::new(0.0, 0.0), - LayoutSize::new(200.0, 200.0), - ) - } else { - LayoutRect::new( - LayoutPoint::new(0.0, 0.0), - LayoutSize::new((*rect_idx).index as f32 * 3.0, 3.0), - ) - }; + // debug rectangle + let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 200.0)); let info = LayoutPrimitiveInfo { rect: bounds, @@ -365,13 +352,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } } -use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; -use euclid::{TypedRect, TypedSize2D}; - #[inline] fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { match style.background_color { - Some(background_color) => builder.push_rect(&info, background_color.into()), + Some(bg) => builder.push_rect(&info, bg.into()), None => builder.push_clear_rect(&info), } } @@ -389,7 +373,9 @@ fn push_text( resource_updates: &mut ResourceUpdates) { use dom::NodeType::*; - use euclid::{TypedPoint2D}; + use euclid::{TypedPoint2D, Length}; + use text_layout; + use css_parser::{TextAlignment, TextOverflowBehaviour}; // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it let arena = display_list.ui_descr.ui_descr_arena.borrow(); @@ -398,11 +384,11 @@ fn push_text( Div => return, Label { ref text } => { text - }, + }, _ => { /// The display list should only ever handle divs and labels. /// Everything more complex should be handled by a pre-processing step - println!("got a NodeType in a DisplayList that wasn't a div or a label, this is a bug"); + eprintln!("got a NodeType in a DisplayList that wasn't a div or a label, this is a bug"); // unreachable!(); return; } @@ -418,8 +404,8 @@ fn push_text( }; let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); - let font_size_pixels = font_size.0.to_pixels(); - let font_size_app_units = Au((font_size_pixels as i32) * AU_PER_PX); + let font_size = Length::new(font_size.0.to_pixels()); + let font_size_app_units = Au((font_size.0 as i32) * AU_PER_PX); let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); @@ -429,53 +415,15 @@ fn push_text( }; let font = &app_resources.font_data[font_id].0; - let positioned_glyphs = put_text_in_bounds(text, font, font_size_pixels, bounds); + let alignment = style.text_align.unwrap_or(TextAlignment::default()); + let overflow_behaviour = style.text_overflow.unwrap_or(TextOverflowBehaviour::default()); + let positioned_glyphs = text_layout::put_text_in_bounds( + text, font, font_size, alignment, overflow_behaviour, bounds); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, None); } -#[inline] -fn put_text_in_bounds<'a>( - text: &str, - font: &::rusttype::Font<'a>, - font_size_pixels: f32, - bounds: &TypedRect) --> Vec -{ - use euclid::TypedPoint2D; - use rusttype::Scale; - - let mut line_x = bounds.origin.x; - let mut line_y = bounds.origin.y + font_size_pixels; - let v_metrics = font.v_metrics(Scale::uniform(font_size_pixels)); - let units_per_em = font.units_per_em(); -/* - println!("unscaled: {:?}", font.v_metrics_unscaled()); - println!("got font size of: {:?}", font_size_pixels); - println!("units_per_em: {:?}", units_per_em); -*/ - text.chars().map(|ch| { - let glyph = font.glyph(ch); - let idx = glyph.id().0; - let scaled_glyph = glyph.scaled(Scale::uniform(font_size_pixels)); - let h_metrics = scaled_glyph.h_metrics(); - - if line_x > (bounds.origin.x + bounds.size.width) { - line_y += font_size_pixels; - line_x = bounds.origin.x; - } else { - line_x += h_metrics.advance_width; - } - println!("pushing glyph {} at: {:?} x {:?} y", ch, line_x, line_y); - let glyph_instance = GlyphInstance { - index: idx, - point: TypedPoint2D::new(line_x, line_y), - }; - glyph_instance - }).collect() -} - #[inline] fn push_box_shadow( builder: &mut DisplayListBuilder, @@ -587,35 +535,37 @@ fn push_font( return None; } - if let Some(&(ref font, ref font_state)) = app_resources.font_data.get(font_id) { - match *font_state { - FontState::Uploaded(font_key) => { - let font_sizes_hashmap = app_resources.fonts.entry(font_key) - .or_insert(FastHashMap::default()); - let font_instance_key = font_sizes_hashmap.entry(font_size_app_units) - .or_insert_with(|| { - let f_instance_key = render_api.generate_font_instance_key(); - resource_updates.add_font_instance( - f_instance_key, - font_key, - font_size_app_units, - None, - None, - Vec::new(), - ); - f_instance_key - } - ); - - return Some(*font_instance_key); - }, - _ => { - println!("warning: trying to use font {:?} that isn't available", font_id); - }, - } - } + let &(ref font, ref font_state) = match app_resources.font_data.get(font_id) { + Some(f) => f, + None => return None, + }; + + match *font_state { + FontState::Uploaded(font_key) => { + let font_sizes_hashmap = app_resources.fonts.entry(font_key) + .or_insert(FastHashMap::default()); + let font_instance_key = font_sizes_hashmap.entry(font_size_app_units) + .or_insert_with(|| { + let f_instance_key = render_api.generate_font_instance_key(); + resource_updates.add_font_instance( + f_instance_key, + font_key, + font_size_app_units, + None, + None, + Vec::new(), + ); + f_instance_key + } + ); - return None; + Some(*font_instance_key) + }, + _ => { + println!("warning: trying to use font {:?} that isn't available", font_id); + None + }, + } } use ui_description::CssConstraintList; @@ -653,14 +603,12 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) rect.style.background = parse(constraint_list, "background", parse_css_background); rect.style.font_size = parse(constraint_list, "font-size", parse_css_font_size); rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); + rect.style.text_overflow = parse(constraint_list, "overflow", parse_text_overflow); + rect.style.text_align = parse(constraint_list, "text-align", parse_text_align); + if let Some(box_shadow_opt) = parse(constraint_list, "box-shadow", parse_css_box_shadow) { rect.style.box_shadow = box_shadow_opt; } - - if rect.style.font_color.is_none() { - // Use "color" and "font-color" interchangeably, even though this isn't in the CSS spec - rect.style.font_color = parse(constraint_list, "font-color", parse_css_color); - } } /// Populate and parse the CSS layout properties diff --git a/src/lib.rs b/src/lib.rs index 1d816befc..9e38539ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,8 @@ mod app; mod app_state; /// Styling & CSS parsing mod css; +/// Text layout +mod text_layout; /// DOM / HTML node handling pub mod dom; /// The layout traits for creating a layout-able application diff --git a/src/text_layout.rs b/src/text_layout.rs new file mode 100644 index 000000000..e046cb256 --- /dev/null +++ b/src/text_layout.rs @@ -0,0 +1,117 @@ +use webrender::api::*; +use euclid::{Length, TypedRect, TypedPoint2D}; +use rusttype::{Font, Scale}; +use css_parser::{TextAlignment, TextOverflowBehaviour}; + +/// Lines is responsible for layouting the lines of the rectangle to +struct Lines<'a> { + align: TextAlignment, + max_lines_before_overflow: usize, + line_height: Length, + max_horizontal_width: Length, + font: &'a Font<'a>, + font_size: Scale, + origin: TypedPoint2D, + current_line: usize, + line_writer_x: f32, + line_writer_y: f32, +} + +pub(crate) enum TextOverflow { + /// Text is overflowing in the vertical direction + IsOverflowing, + /// Text is in bounds + InBounds, +} + +impl<'a> Lines<'a> { + pub(crate) fn from_bounds( + bounds: &TypedRect, + alignment: TextAlignment, + font: &'a Font<'a>, + font_size: Length) + -> Self + { + let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; + let max_horizontal_width = Length::new(bounds.size.width); + + Self { + align: alignment, + max_lines_before_overflow: max_lines_before_overflow, + line_height: font_size, + font: font, + origin: bounds.origin, + max_horizontal_width: max_horizontal_width, + font_size: Scale::uniform(font_size.0), + current_line: 0, + line_writer_x: 0.0, + line_writer_y: 0.0, + } + } + + /// NOTE: The glyphs are in the space of the bounds, not of the layer! + /// You'd need to offset them by `bounds.origin` to get the correct position + /// + /// This function will only process the glyphs until the overflow. + /// + /// TODO: Only process the glyphs until the screen height is filled + pub(crate) fn get_glyphs(&mut self, text: &str, overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { + // fill the rect from top to bottom with glyphs + let mut char_iterator_peek = text.chars().peekable(); + let mut positioned_glyphs = Vec::new(); + + for current_char in text.chars() { + + let kerning = char_iterator_peek.peek().and_then(|next_char| { + Some(self.font.pair_kerning(self.font_size, current_char, *next_char)) + }); + + let kerning = match kerning { + Some(k) => {char_iterator_peek.next(); k}, + None => 0.0, + }; + + let glyph = self.font.glyph(current_char); + let idx = glyph.id().0; + let scaled_glyph = glyph.scaled(self.font_size); + let h_metrics = scaled_glyph.h_metrics(); + + if self.line_writer_x > self.max_horizontal_width.0 { + self.line_writer_y += self.font_size.y; + self.current_line += 1; + self.line_writer_x = self.origin.x; + } else { + self.line_writer_x += h_metrics.advance_width + kerning; + } + + let final_x = self.line_writer_x + self.origin.x + kerning; + let final_y = self.line_writer_y + self.origin.y + self.font_size.y; + + if self.current_line > self.max_lines_before_overflow { + + } + positioned_glyphs.push(GlyphInstance { + index: idx, + point: TypedPoint2D::new(final_x, final_y), + }); + + } + + (positioned_glyphs, TextOverflow::InBounds) + } +} + +#[inline] +pub(crate) fn put_text_in_bounds<'a>( + text: &str, + font: &Font<'a>, + font_size: Length, + alignment: TextAlignment, + overflow_behaviour: TextOverflowBehaviour, + bounds: &TypedRect) +-> Vec +{ + let mut lines = Lines::from_bounds(bounds, alignment, font, font_size); + let (glyphs, overflow) = lines.get_glyphs(text, overflow_behaviour); + glyphs +} \ No newline at end of file From 2286c5ad59b8a9f4a582efb53f45a5a47b0b0a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 28 Mar 2018 18:17:03 +0200 Subject: [PATCH 024/868] Added Cargo.lock file + added ProgramCache --- Cargo.toml | 2 +- src/window.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c10c375c..ac69101ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "00b85801f8c09431e5963a9e1dcd1a9087b744b9" } +webrender = { git = "https://github.com/fschutt/webrender", branch = "make_debug_renderer_optional" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" diff --git a/src/window.rs b/src/window.rs index f2a887ef9..a738df8f3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -510,6 +510,7 @@ impl Window { // this exists because RendererOptions isn't Clone-able fn get_renderer_opts(native: bool, device_pixel_ratio: f32, clear_color: Option) -> RendererOptions { + use webrender::ProgramCache; RendererOptions { resource_override_path: None, // pre-caching shaders means to compile all shaders on startup @@ -520,6 +521,7 @@ impl Window { enable_aa: true, clear_color: clear_color, enable_render_on_scroll: false, + cached_programs: Some(ProgramCache::new()), renderer_kind: if native { RendererKind::Native } else { From 834aa5999363c48bab8230bcf3d917f31a04bfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 29 Mar 2018 07:46:01 +0200 Subject: [PATCH 025/868] Added unicode normalization and fixed character spacing --- Cargo.toml | 3 +- examples/debug.rs | 10 ++++- src/css_parser.rs | 8 ++-- src/lib.rs | 1 + src/text_layout.rs | 107 ++++++++++++++++++++++++++++++++++++--------- 5 files changed, 102 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac69101ba..a72368a07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/fschutt/webrender", branch = "make_debug_renderer_optional" } +webrender = { git = "https://github.com/servo/webrender", rev = "190ed69b68b659ccd7db8c924ec564b4ca1a3e57" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" @@ -14,6 +14,7 @@ euclid = "0.17" image = "0.18.0" rusttype = "0.5.2" app_units = "0.6" +unicode-normalization = "0.1.5" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/examples/debug.rs b/examples/debug.rs index 12b942a6a..b6107127b 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,4 +1,11 @@ +#![feature(alloc_system, global_allocator, allocator_api)] extern crate azul; +extern crate alloc_system; + +use alloc_system::System; + +#[global_allocator] +static A: System = System; use azul::prelude::*; @@ -21,7 +28,8 @@ impl LayoutScreen for MyAppData { dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("This is a very long text that should break on to multiple lines aldkf jasdölfkjas öldfkjasdölkf jasöl dfkj"), + text: String::from("this is a very long string that should be \ + broken onto multiple lines"), })); dom diff --git a/src/css_parser.rs b/src/css_parser.rs index 905978544..6fccbb204 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -790,10 +790,10 @@ impl DirectionCorner { { use self::DirectionCorner::*; match *self { - Right => TypedPoint2D::new(rect.size.width, (rect.size.height / 2.0)), - Left => TypedPoint2D::new(0.0, (rect.size.height / 2.0)), - Top => TypedPoint2D::new((rect.size.width / 2.0), 0.0), - Bottom => TypedPoint2D::new((rect.size.width / 2.0), rect.size.height), + Right => TypedPoint2D::new(rect.size.width, rect.size.height / 2.0), + Left => TypedPoint2D::new(0.0, rect.size.height / 2.0), + Top => TypedPoint2D::new(rect.size.width / 2.0, 0.0), + Bottom => TypedPoint2D::new(rect.size.width / 2.0, rect.size.height), TopRight => TypedPoint2D::new(rect.size.width, 0.0), TopLeft => TypedPoint2D::new(0.0, 0.0), BottomRight => TypedPoint2D::new(rect.size.width, rect.size.height), diff --git a/src/lib.rs b/src/lib.rs index 9e38539ac..523485638 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ extern crate simplecss; extern crate image; extern crate rusttype; extern crate app_units; +extern crate unicode_normalization; /// Global application (Initialization starts here) mod app; diff --git a/src/text_layout.rs b/src/text_layout.rs index e046cb256..14261548b 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -15,6 +15,7 @@ struct Lines<'a> { current_line: usize, line_writer_x: f32, line_writer_y: f32, + v_scale_factor: f32, } pub(crate) enum TextOverflow { @@ -34,7 +35,9 @@ impl<'a> Lines<'a> { { let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; let max_horizontal_width = Length::new(bounds.size.width); - + let v_metrics = font.v_metrics_unscaled(); + let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / font.units_per_em() as f32; + Self { align: alignment, max_lines_before_overflow: max_lines_before_overflow, @@ -42,8 +45,9 @@ impl<'a> Lines<'a> { font: font, origin: bounds.origin, max_horizontal_width: max_horizontal_width, - font_size: Scale::uniform(font_size.0), + font_size: Scale { y: font_size.0 * v_scale_factor, x: font_size.0 }, current_line: 0, + v_scale_factor: v_scale_factor, line_writer_x: 0.0, line_writer_y: 0.0, } @@ -55,21 +59,44 @@ impl<'a> Lines<'a> { /// This function will only process the glyphs until the overflow. /// /// TODO: Only process the glyphs until the screen height is filled - pub(crate) fn get_glyphs(&mut self, text: &str, overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { + pub(crate) fn get_glyphs(&mut self, text: &str, _overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { + // fill the rect from top to bottom with glyphs - let mut char_iterator_peek = text.chars().peekable(); - let mut positioned_glyphs = Vec::new(); + // let mut positioned_glyphs = Vec::new(); + + // step 0: estimate how many lines / words are probably needed + + // step 1: collect the words + + // let words: Vec<&str> = text.split_whitespace().collect(); + +/* + struct Word<'a> { + // the original text + text: &'a str, + // character offsets, from the start of the word + character_offset: Vec, + // the sum of the width of all the characters + total_width: f32, + } + + let words_layouted = words.into_iter().map(|word| { + }).collect::(); +*/ + +/* + println!("self.font_size: {:?}", self.font_size); + + let mut last_char = None; for current_char in text.chars() { - let kerning = char_iterator_peek.peek().and_then(|next_char| { - Some(self.font.pair_kerning(self.font_size, current_char, *next_char)) - }); + let kerning = last_char.and_then(|last_char| { + Some(self.font.pair_kerning(self.font_size, last_char, current_char)) + }).unwrap_or(0.0); - let kerning = match kerning { - Some(k) => {char_iterator_peek.next(); k}, - None => 0.0, - }; + // println!("kerning: ({:?} - {:?}) - {:?}", last_char, current_char, kerning); + last_char = Some(current_char); let glyph = self.font.glyph(current_char); let idx = glyph.id().0; @@ -84,20 +111,58 @@ impl<'a> Lines<'a> { self.line_writer_x += h_metrics.advance_width + kerning; } - let final_x = self.line_writer_x + self.origin.x + kerning; - let final_y = self.line_writer_y + self.origin.y + self.font_size.y; + // println!("h_metrics.advance_width: {:?}, kerning: {}", h_metrics.advance_width, kerning); - if self.current_line > self.max_lines_before_overflow { + let final_x = self.origin.x + self.line_writer_x /* + kerning */; + let final_y = self.origin.y + self.line_writer_y + self.font_size.y; + if self.current_line <= (self.max_lines_before_overflow + 1) { + positioned_glyphs.push(GlyphInstance { + index: idx, + point: TypedPoint2D::new(final_x, final_y), + }); + } else { + // do not layout text that is off-screen anyways + break; } - positioned_glyphs.push(GlyphInstance { - index: idx, - point: TypedPoint2D::new(final_x, final_y), - }); - } +*/ + use rusttype::{Point, Vector}; + + let mut last_glyph = None; + let mut caret = 0.0; + + // normalize characters, i.e. A + ^ = Â + use unicode_normalization::UnicodeNormalization; + // TODO: do this before hading the string to webrender? + let text_normalized = text.nfc().collect::(); + + let positioned_glyphs2 = text_normalized.chars().map(|c| { + let g = self.font.glyph(c).scaled(self.font_size); + if let Some(last) = last_glyph { + caret += self.font.pair_kerning(self.font_size, last, g.id()); + } + let g = g.positioned(Point { x: self.origin.x + caret, y: self.origin.y }); + last_glyph = Some(g.id()); + caret += g.clone().into_unpositioned().h_metrics().advance_width; + GlyphInstance { + index: g.id().0, + point: TypedPoint2D::new(g.position().x, g.position().y + self.font_size.y), + } + }).collect(); + +/* + use rusttype::Point; - (positioned_glyphs, TextOverflow::InBounds) + let positioned_glyphs3 = self.font.layout(text, self.font_size, Point { x: self.origin.x, y: self.origin.y}) + .map(|g| { + GlyphInstance { + index: g.id().0, + point: TypedPoint2D::new(g.position().x, g.position().y + self.font_size.y), + } + }).collect(); +*/ + (positioned_glyphs2, TextOverflow::InBounds) } } From bcaee4ff38a41dad499d0bcd679ba42ff78d1f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 29 Mar 2018 08:22:04 +0200 Subject: [PATCH 026/868] Fixed the Font cache for distinguishing between internal / external fonts --- Cargo.toml | 1 + examples/debug.rs | 10 +--------- examples/test_content.css | 2 +- src/app.rs | 4 ++-- src/app_state.rs | 4 ++-- src/css_parser.rs | 38 ++++++++++++++++---------------------- src/display_list.rs | 13 +++++++++---- src/lib.rs | 1 + src/resources.rs | 15 ++++++++------- src/text_layout.rs | 38 ++++++++++++++++++++++++++++++++++++-- 10 files changed, 77 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a72368a07..527e3fd87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ image = "0.18.0" rusttype = "0.5.2" app_units = "0.6" unicode-normalization = "0.1.5" +harfbuzz_rs = "0.1.0" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/examples/debug.rs b/examples/debug.rs index b6107127b..81004d8be 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,11 +1,4 @@ -#![feature(alloc_system, global_allocator, allocator_api)] extern crate azul; -extern crate alloc_system; - -use alloc_system::System; - -#[global_allocator] -static A: System = System; use azul::prelude::*; @@ -28,8 +21,7 @@ impl LayoutScreen for MyAppData { dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("this is a very long string that should be \ - broken onto multiple lines"), + text: String::from("Ööaeiou0ß"), })); dom diff --git a/examples/test_content.css b/examples/test_content.css index cbadf1498..40f47cdff 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -18,7 +18,7 @@ } * { - font-size: 15px; + font-size: 10px; font-family: "Webly Sleeky UI", sans-serif; color: red; } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 7ac65288c..904eed401 100644 --- a/src/app.rs +++ b/src/app.rs @@ -271,7 +271,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { } /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) + pub fn has_font>(&mut self, id: S) -> bool { (*self.app_state.lock().unwrap()).has_font(id) @@ -320,7 +320,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// ``` /// /// [`AppState::delete_font`]: ../app_state/struct.AppState.html#method.delete_font - pub fn delete_font>(&mut self, id: S) + pub fn delete_font>(&mut self, id: S) -> Option<()> { (*self.app_state.lock().unwrap()).delete_font(id) diff --git a/src/app_state.rs b/src/app_state.rs index f70874390..f07e8794b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -116,7 +116,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { } /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) + pub fn has_font>(&mut self, id: S) -> bool { self.resources.has_font(id) @@ -142,7 +142,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// You can also call this function on an `App` struct, see [`App::add_font`]. /// /// [`App::add_font`]: ../app/struct.App.html#method.add_font - pub fn delete_font>(&mut self, id: S) + pub fn delete_font>(&mut self, id: S) -> Option<()> { self.resources.delete_font(id) diff --git a/src/css_parser.rs b/src/css_parser.rs index 6fccbb204..f0a789499 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1296,7 +1296,7 @@ pub(crate) struct RectStyle<'a> { /// Font size pub(crate) font_size: Option, /// Font name / family - pub(crate) font_family: Option>, + pub(crate) font_family: Option, /// Text color pub(crate) font_color: Option, /// Text alignment @@ -1330,32 +1330,21 @@ pub struct FontSize(pub PixelValue); typed_pixel_value_parser!(parse_css_font_size, FontSize); #[derive(Debug, PartialEq, Clone)] -pub struct FontFamily<'a> { +pub struct FontFamily { // parsed fonts, in order, i.e. "Webly Sleeky UI", "monospace", etc. - pub(crate) fonts: Vec> + pub(crate) fonts: Vec } -#[derive(Debug, PartialEq, Clone, Hash)] -pub enum Font<'a> { - BuiltinFont(&'a str), - ExternalFont(&'a str), -} - -impl<'a> Font<'a> { - pub fn get_font_id(&self) -> &'a str { - use self::Font::*; - // TODO: Currently BuiltinFont("sans-serif") and - // ExternalFont("sans-serif") are the same because of this function - match *self { - BuiltinFont(f) => f, - ExternalFont(f) => f, - } - } +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub enum Font { + BuiltinFont(&'static str), + ExternalFont(String), } #[derive(Debug, PartialEq, Copy, Clone)] pub enum FontFamilyParseError<'a> { InvalidFontFamily(&'a str), + UnrecognizedBuiltinFont(&'a str), UnclosedQuotes(&'a str), } @@ -1370,7 +1359,7 @@ impl<'a> From> for FontFamilyParseError<'a> { // "Webly Sleeky UI", monospace // 'Webly Sleeky Ui', monospace // sans-serif -pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result, FontFamilyParseError<'a>> { +pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result> { let multiple_fonts = input.split(','); let mut fonts = Vec::with_capacity(1); @@ -1384,9 +1373,14 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result if double_quote_iter.next().is_some() || single_quote_iter.next().is_some() { let stripped_font = strip_quotes(font)?; - fonts.push(Font::ExternalFont(stripped_font.0)); + fonts.push(Font::ExternalFont(stripped_font.0.into())); } else { - fonts.push(Font::BuiltinFont(font)); + match font { + "serif" => fonts.push(Font::BuiltinFont("serif")), + "sans-serif" => fonts.push(Font::BuiltinFont("sans-serif")), + "monospace" => fonts.push(Font::BuiltinFont("sans-serif")), + _ => return Err(FontFamilyParseError::UnrecognizedBuiltinFont(font)), + } } } diff --git a/src/display_list.rs b/src/display_list.rs index 7826520b5..f1e409dd1 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -20,6 +20,7 @@ use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; +const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) ui_descr: &'a UiDescription, @@ -158,8 +159,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { { use font::FontState; - let mut updated_fonts = Vec::<(String, Vec)>::new(); - let mut to_delete_fonts = Vec::<(String, Option<(FontKey, Vec)>)>::new(); + let mut updated_fonts = Vec::<(::css_parser::Font, Vec)>::new(); + let mut to_delete_fonts = Vec::<(::css_parser::Font, Option<(FontKey, Vec)>)>::new(); for (key, value) in app_resources.font_data.iter() { match value.1 { @@ -405,8 +406,10 @@ fn push_text( let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size = Length::new(font_size.0.to_pixels()); + // HEAVY TODO: webrender bug -for some reason the font is rendered at the half of the expected size + let font_size = font_size * 2.0; let font_size_app_units = Au((font_size.0 as i32) * AU_PER_PX); - let font_id = font_family.fonts.get(0).unwrap_or(&Font::BuiltinFont("sans-serif")).get_font_id(); + let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { @@ -519,9 +522,11 @@ fn push_border( } } +use css_parser; + #[inline] fn push_font( - font_id: &str, + font_id: &css_parser::Font, font_size_app_units: Au, resource_updates: &mut ResourceUpdates, app_resources: &mut AppResources, diff --git a/src/lib.rs b/src/lib.rs index 523485638..c98d9b4d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ extern crate image; extern crate rusttype; extern crate app_units; extern crate unicode_normalization; +extern crate harfbuzz_rs; /// Global application (Initialization starts here) mod app; diff --git a/src/resources.rs b/src/resources.rs index fd51b5ca8..6566020a9 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -7,8 +7,9 @@ use font::{FontState, FontError}; use image::{self, ImageError, DynamicImage, GenericImage}; use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; use std::collections::hash_map::Entry::*; -use rusttype::Font; use app_units::Au; +use css_parser; +use css_parser::Font::ExternalFont; /// Font and image keys /// @@ -29,7 +30,7 @@ pub(crate) struct AppResources<'a> { // but we also need access to the font metrics. So we first parse the font // to make sure that nothing is going wrong. In the next draw call, we // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: FastHashMap, FontState)>, + pub(crate) font_data: FastHashMap, FontState)>, // After we've looked up the FontKey in the font_data map, we can then access // the font instance key (if there is any). If there is no font instance key, // we first need to create one. @@ -89,7 +90,7 @@ impl<'a> AppResources<'a> { { use font; - match self.font_data.entry(id.into()) { + match self.font_data.entry(ExternalFont(id.into())) { Occupied(_) => Ok(None), Vacant(v) => { let mut font_data = Vec::::new(); @@ -102,18 +103,18 @@ impl<'a> AppResources<'a> { } /// Checks if a font is currently registered and ready-to-use - pub(crate) fn has_font>(&mut self, id: S) + pub(crate) fn has_font>(&mut self, id: S) -> bool { - self.font_data.get(id.as_ref()).is_some() + self.font_data.get(&ExternalFont(id.into())).is_some() } /// See `AppState::delete_font()` - pub(crate) fn delete_font>(&mut self, id: S) + pub(crate) fn delete_font>(&mut self, id: S) -> Option<()> { // TODO: can fonts that haven't been uploaded yet be deleted? - match self.font_data.get_mut(id.as_ref()) { + match self.font_data.get_mut(&ExternalFont(id.into())) { None => None, Some(v) => { let to_delete_font_key = match v.1 { diff --git a/src/text_layout.rs b/src/text_layout.rs index 14261548b..5ccf8f021 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -45,7 +45,7 @@ impl<'a> Lines<'a> { font: font, origin: bounds.origin, max_horizontal_width: max_horizontal_width, - font_size: Scale { y: font_size.0 * v_scale_factor, x: font_size.0 }, + font_size: /* Scale { y: font_size.0 * v_scale_factor, x: font_size.0 },*/ Scale::uniform(font_size.0), current_line: 0, v_scale_factor: v_scale_factor, line_writer_x: 0.0, @@ -127,16 +127,50 @@ impl<'a> Lines<'a> { } } */ - use rusttype::{Point, Vector}; + use rusttype::Point; let mut last_glyph = None; let mut caret = 0.0; // normalize characters, i.e. A + ^ = Â use unicode_normalization::UnicodeNormalization; + // TODO: do this before hading the string to webrender? let text_normalized = text.nfc().collect::(); + // harfbuzz pass + /* + use harfbuzz_rs::*; + use harfbuzz_rs::rusttype::SetRustTypeFuncs; + + let path = "path/to/some/font_file.otf"; + let index = 0; //< face index in the font file + let face = Face::from_file(path, index).unwrap(); + let mut font = Font::new(face); + // Use RustType as provider for font information that harfbuzz needs. + // You can also use a custom font implementation. For more information look + // at the documentation for `FontFuncs`. + font.set_rusttype_funcs(); + + let output = UnicodeBuffer::new().add_str("Hello World!").shape(&font, &[]); + */ + + /* + let positions = output.get_glyph_positions(); + let infos = output.get_glyph_infos(); + + // iterate over the shaped glyphs + for (position, info) in positions.iter().zip(infos) { + let gid = info.codepoint; + let cluster = info.cluster; + let x_advance = position.x_advance; + let x_offset = position.x_offset; + let y_offset = position.y_offset; + + // Here you would usually draw the glyphs. + println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); + } + */ let positioned_glyphs2 = text_normalized.chars().map(|c| { let g = self.font.glyph(c).scaled(self.font_size); if let Some(last) = last_glyph { From bfa2fe364d2df91c30cb34254f9f99ad1b03f6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 29 Mar 2018 09:07:11 +0200 Subject: [PATCH 027/868] Seem to have fixed the font-size bug in webrender --- examples/debug.rs | 2 +- examples/test_content.css | 2 +- src/display_list.rs | 19 ++++++++++++++++--- src/text_layout.rs | 7 +++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 81004d8be..7f06df23d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -21,7 +21,7 @@ impl LayoutScreen for MyAppData { dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("Ööaeiou0ß"), + text: String::from("Azul App"), })); dom diff --git a/examples/test_content.css b/examples/test_content.css index 40f47cdff..cbadf1498 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -18,7 +18,7 @@ } * { - font-size: 10px; + font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; color: red; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index f1e409dd1..5c229e136 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -404,12 +404,25 @@ fn push_text( None => return, }; + // HEAVY TODO: webrender bug -for some reason the font is rendered at the half of the expected size + // let font_size = font_size * 2.0 * v_scale_factor; + // TODO: This is a horrible hack, but it seems to work! + + // TODO: border let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size = Length::new(font_size.0.to_pixels()); - // HEAVY TODO: webrender bug -for some reason the font is rendered at the half of the expected size - let font_size = font_size * 2.0; - let font_size_app_units = Au((font_size.0 as i32) * AU_PER_PX); + let font_size_app_units = (font_size.0 as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); + let v_scale_factor; + { + let font = &app_resources.font_data[font_id].0; + let v_metrics = font.v_metrics_unscaled(); + v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / font.units_per_em() as f32; + } + + let font_size = (font_size * 2.0) / v_scale_factor; + let font_size_app_units = Au((font_size_app_units as f32 * v_scale_factor) as i32); + let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { diff --git a/src/text_layout.rs b/src/text_layout.rs index 5ccf8f021..87a9bcb6c 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -45,7 +45,7 @@ impl<'a> Lines<'a> { font: font, origin: bounds.origin, max_horizontal_width: max_horizontal_width, - font_size: /* Scale { y: font_size.0 * v_scale_factor, x: font_size.0 },*/ Scale::uniform(font_size.0), + font_size: Scale::uniform(font_size.0), current_line: 0, v_scale_factor: v_scale_factor, line_writer_x: 0.0, @@ -171,6 +171,9 @@ impl<'a> Lines<'a> { println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); } */ + // HORRIBLE WEBRENDER HACK! + let offset_top = self.font_size.y * 3.0 / 4.0; + let positioned_glyphs2 = text_normalized.chars().map(|c| { let g = self.font.glyph(c).scaled(self.font_size); if let Some(last) = last_glyph { @@ -181,7 +184,7 @@ impl<'a> Lines<'a> { caret += g.clone().into_unpositioned().h_metrics().advance_width; GlyphInstance { index: g.id().0, - point: TypedPoint2D::new(g.position().x, g.position().y + self.font_size.y), + point: TypedPoint2D::new(g.position().x, g.position().y + offset_top), } }).collect(); From 9a825618e4906915ab49ca0e2376726c674a1ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 29 Mar 2018 09:19:49 +0200 Subject: [PATCH 028/868] Fixed unit tests, corrected css font-family parsing bug --- src/css_parser.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index f0a789499..01d6a53bc 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1378,7 +1378,7 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result fonts.push(Font::BuiltinFont("serif")), "sans-serif" => fonts.push(Font::BuiltinFont("sans-serif")), - "monospace" => fonts.push(Font::BuiltinFont("sans-serif")), + "monospace" => fonts.push(Font::BuiltinFont("monospace")), _ => return Err(FontFamilyParseError::UnrecognizedBuiltinFont(font)), } } @@ -1797,7 +1797,7 @@ mod css_tests { fn test_parse_css_font_family_1() { assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { fonts: vec![ - Font::ExternalFont("Webly Sleeky UI"), + Font::ExternalFont("Webly Sleeky UI".into()), Font::BuiltinFont("monospace"), ] })); @@ -1807,7 +1807,7 @@ mod css_tests { fn test_parse_css_font_family_2() { assert_eq!(parse_css_font_family("'Webly Sleeky UI'"), Ok(FontFamily { fonts: vec![ - Font::ExternalFont("Webly Sleeky UI"), + Font::ExternalFont("Webly Sleeky UI".into()), ] })); From 312e5b5e12624ded64dba8f399e3b949ffc55056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 30 Mar 2018 01:37:29 +0200 Subject: [PATCH 029/868] Break text into lines on word boundaries --- Cargo.toml | 2 +- examples/debug.rs | 2 +- examples/test_content.css | 2 +- src/text_layout.rs | 179 ++++++++++++++++++-------------------- 4 files changed, 86 insertions(+), 99 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 527e3fd87..9e15e8bf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "190ed69b68b659ccd7db8c924ec564b4ca1a3e57" } +webrender = { git = "https://github.com/servo/webrender", rev = "02c2afc3eb9353432b6dccf926ea4ca1b8000887" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" diff --git a/examples/debug.rs b/examples/debug.rs index 7f06df23d..1311a37a1 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -21,7 +21,7 @@ impl LayoutScreen for MyAppData { dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("Azul App"), + text: String::from("This is a very long string that should break into multiple lines"), })); dom diff --git a/examples/test_content.css b/examples/test_content.css index cbadf1498..9d0d0ae36 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -20,5 +20,5 @@ * { font-size: 15px; font-family: "Webly Sleeky UI", sans-serif; - color: red; + color: blue; } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index 87a9bcb6c..79eca560a 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -13,8 +13,7 @@ struct Lines<'a> { font_size: Scale, origin: TypedPoint2D, current_line: usize, - line_writer_x: f32, - line_writer_y: f32, + v_advance: f32, v_scale_factor: f32, } @@ -36,7 +35,8 @@ impl<'a> Lines<'a> { let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; let max_horizontal_width = Length::new(bounds.size.width); let v_metrics = font.v_metrics_unscaled(); - let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / font.units_per_em() as f32; + let v_advance = v_metrics.ascent - v_metrics.descent + v_metrics.line_gap; + let v_scale_factor = v_advance / font.units_per_em() as f32; Self { align: alignment, @@ -48,8 +48,7 @@ impl<'a> Lines<'a> { font_size: Scale::uniform(font_size.0), current_line: 0, v_scale_factor: v_scale_factor, - line_writer_x: 0.0, - line_writer_y: 0.0, + v_advance: v_advance, } } @@ -61,82 +60,23 @@ impl<'a> Lines<'a> { /// TODO: Only process the glyphs until the screen height is filled pub(crate) fn get_glyphs(&mut self, text: &str, _overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { - // fill the rect from top to bottom with glyphs - // let mut positioned_glyphs = Vec::new(); - - // step 0: estimate how many lines / words are probably needed - - // step 1: collect the words + use unicode_normalization::UnicodeNormalization; + use rusttype::Point; - // let words: Vec<&str> = text.split_whitespace().collect(); + let text = text.nfc().collect::(); -/* + #[derive(Debug)] struct Word<'a> { // the original text - text: &'a str, - // character offsets, from the start of the word - character_offset: Vec, + pub text: &'a str, + // glyphs, positions are relative to the first character of the word + pub glyphs: Vec, // the sum of the width of all the characters - total_width: f32, + pub total_width: f32, } - let words_layouted = words.into_iter().map(|word| { - - }).collect::(); -*/ - -/* - println!("self.font_size: {:?}", self.font_size); - - let mut last_char = None; - for current_char in text.chars() { - - let kerning = last_char.and_then(|last_char| { - Some(self.font.pair_kerning(self.font_size, last_char, current_char)) - }).unwrap_or(0.0); - - // println!("kerning: ({:?} - {:?}) - {:?}", last_char, current_char, kerning); - last_char = Some(current_char); - - let glyph = self.font.glyph(current_char); - let idx = glyph.id().0; - let scaled_glyph = glyph.scaled(self.font_size); - let h_metrics = scaled_glyph.h_metrics(); - - if self.line_writer_x > self.max_horizontal_width.0 { - self.line_writer_y += self.font_size.y; - self.current_line += 1; - self.line_writer_x = self.origin.x; - } else { - self.line_writer_x += h_metrics.advance_width + kerning; - } - - // println!("h_metrics.advance_width: {:?}, kerning: {}", h_metrics.advance_width, kerning); - - let final_x = self.origin.x + self.line_writer_x /* + kerning */; - let final_y = self.origin.y + self.line_writer_y + self.font_size.y; - - if self.current_line <= (self.max_lines_before_overflow + 1) { - positioned_glyphs.push(GlyphInstance { - index: idx, - point: TypedPoint2D::new(final_x, final_y), - }); - } else { - // do not layout text that is off-screen anyways - break; - } - } -*/ - use rusttype::Point; - - let mut last_glyph = None; - let mut caret = 0.0; - // normalize characters, i.e. A + ^ = Â - use unicode_normalization::UnicodeNormalization; - - // TODO: do this before hading the string to webrender? - let text_normalized = text.nfc().collect::(); + // TODO: this is currently done on the whole string // harfbuzz pass /* @@ -171,35 +111,82 @@ impl<'a> Lines<'a> { println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); } */ + + // HORRIBLE WEBRENDER HACK! let offset_top = self.font_size.y * 3.0 / 4.0; - - let positioned_glyphs2 = text_normalized.chars().map(|c| { - let g = self.font.glyph(c).scaled(self.font_size); - if let Some(last) = last_glyph { - caret += self.font.pair_kerning(self.font_size, last, g.id()); - } - let g = g.positioned(Point { x: self.origin.x + caret, y: self.origin.y }); - last_glyph = Some(g.id()); - caret += g.clone().into_unpositioned().h_metrics().advance_width; - GlyphInstance { - index: g.id().0, - point: TypedPoint2D::new(g.position().x, g.position().y + offset_top), + + let mut words = Vec::new(); + + // TODO: estimate how much of the text is going to fit into the rectangle + + { + for line in text.lines() { + for word in line.split_whitespace() { + + let mut caret = 0.0; + let mut cur_word_length = 0.0; + let mut glyphs_in_this_word = Vec::new(); + let mut last_glyph = None; + + for c in word.chars() { + let g = self.font.glyph(c).scaled(self.font_size); + let id = g.id(); + if let Some(last) = last_glyph { + caret += self.font.pair_kerning(self.font_size, last, g.id()); + } + let g = g.positioned(Point { x: caret, y: 0.0 }); + last_glyph = Some(id); + let horiz_advance = g.unpositioned().h_metrics().advance_width; + caret += horiz_advance; + cur_word_length += horiz_advance; + + glyphs_in_this_word.push(GlyphInstance { + index: id.0, + point: TypedPoint2D::new(g.position().x, g.position().y), + }) + } + + words.push(Word { + text: word, + glyphs: glyphs_in_this_word, + total_width: cur_word_length, + }) + } } - }).collect(); + } -/* - use rusttype::Point; + let mut positioned_glyphs = Vec::new(); + + // do knuth-plass text layout here, determine spacing and alignment + + // position words into glyphs + { + let v_metrics_scaled = self.font.v_metrics(self.font_size); + let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; + + let mut word_caret = 0.0; + let mut cur_line = 0; - let positioned_glyphs3 = self.font.layout(text, self.font_size, Point { x: self.origin.x, y: self.origin.y}) - .map(|g| { - GlyphInstance { - index: g.id().0, - point: TypedPoint2D::new(g.position().x, g.position().y + self.font_size.y), + for word in words { + let text_overflows_rect = word_caret + word.total_width > self.max_horizontal_width.0; + if text_overflows_rect { + word_caret = 0.0; + cur_line += 1; } - }).collect(); -*/ - (positioned_glyphs2, TextOverflow::InBounds) + for mut glyph in word.glyphs { + let push_x = self.origin.x + word_caret; + let push_y = self.origin.y + (cur_line as f32 * v_advance_scaled) + offset_top; + glyph.point.x += push_x; + glyph.point.y += push_y; + positioned_glyphs.push(glyph); + } + + word_caret += word.total_width + 5.0; // space between words + } + } + + (positioned_glyphs, TextOverflow::InBounds) } } From e678046255bcc426f1222d0a7b070f9d5063b38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 30 Mar 2018 02:01:50 +0200 Subject: [PATCH 030/868] Optimized text rendering (enabled subpixel) + early return for overflowing text --- examples/debug.rs | 2 +- src/display_list.rs | 7 +++- src/text_layout.rs | 81 ++++++++++++++++++++++----------------------- 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 1311a37a1..94c27abaa 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -21,7 +21,7 @@ impl LayoutScreen for MyAppData { dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("This is a very long string that should break into multiple lines"), + text: String::from("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."), })); dom diff --git a/src/display_list.rs b/src/display_list.rs index 5c229e136..460def435 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -437,7 +437,12 @@ fn push_text( text, font, font_size, alignment, overflow_behaviour, bounds); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); - builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, None); + let flags = FontInstanceFlags::SUBPIXEL_BGR; + let options = GlyphOptions { + render_mode: FontRenderMode::Subpixel, + flags: flags, + }; + builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); } #[inline] diff --git a/src/text_layout.rs b/src/text_layout.rs index 79eca560a..3b6795136 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -63,6 +63,8 @@ impl<'a> Lines<'a> { use unicode_normalization::UnicodeNormalization; use rusttype::Point; + // normalize characters, i.e. A + ^ = Â + // TODO: this is currently done on the whole string let text = text.nfc().collect::(); #[derive(Debug)] @@ -75,50 +77,43 @@ impl<'a> Lines<'a> { pub total_width: f32, } - // normalize characters, i.e. A + ^ = Â - // TODO: this is currently done on the whole string - // harfbuzz pass - /* + { use harfbuzz_rs::*; use harfbuzz_rs::rusttype::SetRustTypeFuncs; - - let path = "path/to/some/font_file.otf"; - let index = 0; //< face index in the font file - let face = Face::from_file(path, index).unwrap(); - let mut font = Font::new(face); - // Use RustType as provider for font information that harfbuzz needs. - // You can also use a custom font implementation. For more information look - // at the documentation for `FontFuncs`. - font.set_rusttype_funcs(); - - let output = UnicodeBuffer::new().add_str("Hello World!").shape(&font, &[]); - */ - - /* - let positions = output.get_glyph_positions(); - let infos = output.get_glyph_infos(); - - // iterate over the shaped glyphs - for (position, info) in positions.iter().zip(infos) { - let gid = info.codepoint; - let cluster = info.cluster; - let x_advance = position.x_advance; - let x_offset = position.x_offset; - let y_offset = position.y_offset; - - // Here you would usually draw the glyphs. - println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); - } - */ - + /* + let path = "path/to/some/font_file.otf"; + let index = 0; //< face index in the font file + let face = Face::from_file(path, index).unwrap(); + let mut font = Font::new(face); + + // Use RustType as provider for font information that harfbuzz needs. + // You can also use a custom font implementation. For more information look + // at the documentation for `FontFuncs`. + font.set_rusttype_funcs(); + let output = UnicodeBuffer::new().add_str(text).shape(&font, &[]); + let positions = output.get_glyph_positions(); + let infos = output.get_glyph_infos(); + + // iterate over the shaped glyphs + for (position, info) in positions.iter().zip(infos) { + let gid = info.codepoint; + let cluster = info.cluster; + let x_advance = position.x_advance; + let x_offset = position.x_offset; + let y_offset = position.y_offset; + + // Here you would usually draw the glyphs. + println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); + } + */ + } // HORRIBLE WEBRENDER HACK! let offset_top = self.font_size.y * 3.0 / 4.0; - let mut words = Vec::new(); - // TODO: estimate how much of the text is going to fit into the rectangle + let mut words = Vec::new(); { for line in text.lines() { @@ -156,15 +151,15 @@ impl<'a> Lines<'a> { } } + // Alignment + Knuth-Plass + + // Final positioning let mut positioned_glyphs = Vec::new(); - - // do knuth-plass text layout here, determine spacing and alignment - - // position words into glyphs { let v_metrics_scaled = self.font.v_metrics(self.font_size); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; + let space_width = self.font.glyph(' ').scaled(self.font_size).h_metrics().advance_width; let mut word_caret = 0.0; let mut cur_line = 0; @@ -181,8 +176,10 @@ impl<'a> Lines<'a> { glyph.point.y += push_y; positioned_glyphs.push(glyph); } - - word_caret += word.total_width + 5.0; // space between words + if cur_line > self.max_lines_before_overflow { + break; + } + word_caret += word.total_width + space_width; // space between words } } From 3604d1c3d4e96fd2f04ef2f1e9b3573c8944cf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 30 Mar 2018 21:25:31 +0200 Subject: [PATCH 031/868] Optimized shadow clipping --- src/display_list.rs | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 460def435..d647c6cbe 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -290,7 +290,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { Vec::new() ); - // Push box shadow, before the clip is active + // Push the "outset" box shadow, before the clip is active push_box_shadow( &mut builder, &rect.style, @@ -323,6 +323,12 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { &rect.style, &app_resources); + // push the inset shadow (if any) + push_box_shadow(&mut builder, + &rect.style, + &bounds, + &full_screen_rect); + push_border( &info, &mut builder, @@ -445,6 +451,8 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); } +/// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the +/// shadow will not show up. #[inline] fn push_box_shadow( builder: &mut DisplayListBuilder, @@ -460,9 +468,32 @@ fn push_box_shadow( // The pre_shadow is missing the BorderRadius & LayoutRect let border_radius = style.border_radius.unwrap_or(BorderRadius::zero()); - // Currently the box shadow is blurred across the whole window - // This can be possibly optimized further - let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), *full_screen_rect); + if pre_shadow.clip_mode == BoxShadowClipMode::Inset { + // inset shadows do not work like outset shadows + // for inset shadows, you have to push a clip ID first, so that they are + // clipped to the bounds -we trust that the calling function knows to do this + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), *bounds); + builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, + pre_shadow.blur_radius, pre_shadow.spread_radius, + border_radius, pre_shadow.clip_mode); + return; + } + + // calculate the maximum extent of the outset shadow + let mut clip_rect = *bounds; + + let origin_displace = pre_shadow.spread_radius - pre_shadow.blur_radius; + clip_rect.origin.x = clip_rect.origin.x + pre_shadow.offset.x - origin_displace; + clip_rect.origin.y = clip_rect.origin.y + pre_shadow.offset.y - origin_displace; + + let spread = (pre_shadow.spread_radius * 2.0) + (pre_shadow.blur_radius * 2.0); + clip_rect.size.height = clip_rect.size.height + spread; + clip_rect.size.width = clip_rect.size.width + spread; + + // prevent shadows that are larger than the full screen + let clip_rect = clip_rect.intersection(full_screen_rect).unwrap_or(clip_rect); + + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), clip_rect); builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); From 2aef1802d0662ebe25e74e0c4cca52987f89b055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 31 Mar 2018 18:49:08 +0200 Subject: [PATCH 032/868] Fixed DPI scaling + prevented duplicated box shadow --- Cargo.toml | 2 +- examples/test_content.css | 8 ++-- src/display_list.rs | 82 +++++++++++++++++++++------------------ 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e15e8bf6..2e6a7da17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "02c2afc3eb9353432b6dccf926ea4ca1b8000887" } +webrender = { git = "https://github.com/fschutt/webrender", branch = "fix_dpi" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" diff --git a/examples/test_content.css b/examples/test_content.css index 9d0d0ae36..32fe3417c 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -4,8 +4,8 @@ border: 1px solid #b7b7b7; border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; - background: image("Cat01"); - /*background: linear-gradient(#fcfcfc, #efefef);*/ + /*background: image("Cat01");*/ + background: linear-gradient(#fcfcfc, #efefef); width: 200px; height: 200px; min-height: 400px; @@ -18,7 +18,7 @@ } * { - font-size: 15px; + font-size: 10px; font-family: "Webly Sleeky UI", sans-serif; - color: blue; + color: black; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index d647c6cbe..a5b19ae39 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -295,7 +295,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { &mut builder, &rect.style, &bounds, - &full_screen_rect); + &full_screen_rect, + BoxShadowClipMode::Outset); let clip_region_id = rect.style.border_radius.and_then(|border_radius| { let region = ComplexClipRegion { @@ -327,7 +328,8 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { push_box_shadow(&mut builder, &rect.style, &bounds, - &full_screen_rect); + &full_screen_rect, + BoxShadowClipMode::Inset); push_border( &info, @@ -410,25 +412,12 @@ fn push_text( None => return, }; - // HEAVY TODO: webrender bug -for some reason the font is rendered at the half of the expected size - // let font_size = font_size * 2.0 * v_scale_factor; - // TODO: This is a horrible hack, but it seems to work! - // TODO: border let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size = Length::new(font_size.0.to_pixels()); let font_size_app_units = (font_size.0 as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); - let v_scale_factor; - { - let font = &app_resources.font_data[font_id].0; - let v_metrics = font.v_metrics_unscaled(); - v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / font.units_per_em() as f32; - } - - let font_size = (font_size * 2.0) / v_scale_factor; - let font_size_app_units = Au((font_size_app_units as f32 * v_scale_factor) as i32); - + let font_size_app_units = Au(font_size_app_units as i32); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { @@ -436,29 +425,49 @@ fn push_text( None => return, }; + // The font_size_adjustment_hack is a hack to make horizontal spacing work correctly + // For some reason, rusttype doesn't return the correct horizontal spacing for characters + let font_size_adjustment_hack = { + let font = &app_resources.font_data[font_id].0; + let v_metrics = font.v_metrics_unscaled(); + let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / + font.units_per_em() as f32; + 1.0 + ((v_scale_factor - 1.0) * 2.0) + }; + let font = &app_resources.font_data[font_id].0; let alignment = style.text_align.unwrap_or(TextAlignment::default()); let overflow_behaviour = style.text_overflow.unwrap_or(TextOverflowBehaviour::default()); let positioned_glyphs = text_layout::put_text_in_bounds( - text, font, font_size, alignment, overflow_behaviour, bounds); + text, font, font_size * font_size_adjustment_hack, alignment, overflow_behaviour, bounds); + // TODO: webrender doesn't respect the DPI of the monitor its on: + // + // See: https://github.com/servo/webrender/pull/2597 + // and: https://github.com/servo/webrender/issues/2596 + let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); let flags = FontInstanceFlags::SUBPIXEL_BGR; let options = GlyphOptions { render_mode: FontRenderMode::Subpixel, flags: flags, + dpi: Some(96), }; builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); } /// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the /// shadow will not show up. +/// +/// To prevent a shadow from being pushed twice, you have to annotate the clip +/// mode for this - outset or inset. #[inline] fn push_box_shadow( builder: &mut DisplayListBuilder, style: &RectStyle, bounds: &TypedRect, - full_screen_rect: &TypedRect) + full_screen_rect: &TypedRect, + shadow_type: BoxShadowClipMode) { let pre_shadow = match style.box_shadow { Some(ref ps) => ps, @@ -468,36 +477,35 @@ fn push_box_shadow( // The pre_shadow is missing the BorderRadius & LayoutRect let border_radius = style.border_radius.unwrap_or(BorderRadius::zero()); - if pre_shadow.clip_mode == BoxShadowClipMode::Inset { - // inset shadows do not work like outset shadows - // for inset shadows, you have to push a clip ID first, so that they are - // clipped to the bounds -we trust that the calling function knows to do this - let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), *bounds); - builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, - pre_shadow.blur_radius, pre_shadow.spread_radius, - border_radius, pre_shadow.clip_mode); + if pre_shadow.clip_mode != shadow_type { return; } - // calculate the maximum extent of the outset shadow - let mut clip_rect = *bounds; + let clip_rect = if pre_shadow.clip_mode == BoxShadowClipMode::Inset { + // inset shadows do not work like outset shadows + // for inset shadows, you have to push a clip ID first, so that they are + // clipped to the bounds -we trust that the calling function knows to do this + *bounds + } else { + // calculate the maximum extent of the outset shadow + let mut clip_rect = *bounds; - let origin_displace = pre_shadow.spread_radius - pre_shadow.blur_radius; - clip_rect.origin.x = clip_rect.origin.x + pre_shadow.offset.x - origin_displace; - clip_rect.origin.y = clip_rect.origin.y + pre_shadow.offset.y - origin_displace; + let origin_displace = pre_shadow.spread_radius - pre_shadow.blur_radius; + clip_rect.origin.x = clip_rect.origin.x + pre_shadow.offset.x - origin_displace; + clip_rect.origin.y = clip_rect.origin.y + pre_shadow.offset.y - origin_displace; - let spread = (pre_shadow.spread_radius * 2.0) + (pre_shadow.blur_radius * 2.0); - clip_rect.size.height = clip_rect.size.height + spread; - clip_rect.size.width = clip_rect.size.width + spread; + let spread = (pre_shadow.spread_radius * 2.0) + (pre_shadow.blur_radius * 2.0); + clip_rect.size.height = clip_rect.size.height + spread; + clip_rect.size.width = clip_rect.size.width + spread; - // prevent shadows that are larger than the full screen - let clip_rect = clip_rect.intersection(full_screen_rect).unwrap_or(clip_rect); + // prevent shadows that are larger than the full screen + clip_rect.intersection(full_screen_rect).unwrap_or(clip_rect) + }; let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), clip_rect); builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); - } #[inline] From 0b0a6bc7f0ed79786f803a9810fc9ecb2e81f159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 7 Apr 2018 04:44:39 +0200 Subject: [PATCH 033/868] Updated constraint system to make use of the dom hashes --- src/cache.rs | 2 +- src/display_list.rs | 37 +++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index fbae8522c..3aea9449a 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -52,7 +52,7 @@ pub(crate) struct DomTreeCache { #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DomChangeSet { - // todo: calculate the constraints that have to be updated + // TODO: calculate the constraints that have to be updated pub(crate) added_nodes: BTreeMap, } diff --git a/src/display_list.rs b/src/display_list.rs index a5b19ae39..c9ae93e8b 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -218,16 +218,19 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } if css.needs_relayout { -/* + + println!("relayout!"); + // constraints were added or removed during the last frame - for rect_id in self.rectangles.keys() { - let mut layout_contraints = Vec::::new(); + for (rect_idx, rect) in self.rectangles.iter() { let arena = &*self.ui_descr.ui_descr_arena.borrow(); - create_layout_constraints(&rect, arena, ui_solver); - let cassowary_constraints = css_constraints_to_cassowary_constraints(rect.rect, &layout_contraints); + let dom_hash = &ui_solver.dom_tree_cache.previous_layout.arena[*rect_idx]; + let display_rect = ui_solver.edit_variable_cache.map[&dom_hash.data]; + let layout_contraints = create_layout_constraints(rect, *rect_idx, arena, &ui_solver.window_dimensions); + let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, &layout_contraints); ui_solver.solver.add_constraints(&cassowary_constraints).unwrap(); } -*/ + // if we push or pop constraints that means we also need to re-layout the window has_window_size_changed = true; } @@ -690,27 +693,25 @@ fn parse_css_layout_properties(rect: &mut DisplayRectangle) rect.layout.align_content = parse(constraint_list, "align-content", parse_layout_align_content); } -// Adds and removes layout constraints if necessary +// Returns the constraints for one rectangle fn create_layout_constraints( rect: &DisplayRectangle, + rect_id: NodeId, arena: &Arena>, - ui_solver: &mut UiSolver) + window_dimensions: &WindowDimensions) +-> Vec where T: LayoutScreen { use css_parser; - // todo: put these to use! - let window_dimensions = &ui_solver.window_dimensions; - let solver = &mut ui_solver.solver; - let previous_layout = &mut ui_solver.solved_layout; - use cassowary::strength::*; use constraints::{SizeConstraint, Strength}; - /* - // centering a rectangle: - center(&root), - bound_by(&root).padding(50.0).strength(WEAK), - */ + let mut layout_constraints = Vec::::new(); + + layout_constraints.push(CssConstraint::Size((SizeConstraint::Width(200.0), Strength(STRONG)))); + layout_constraints.push(CssConstraint::Size((SizeConstraint::Height(200.0), Strength(STRONG)))); + + layout_constraints } fn css_constraints_to_cassowary_constraints(rect: &DisplayRect, css: &Vec) From 2fbecb75fb354f9287be6294c53137325c9e3e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 20 May 2018 09:59:14 +0200 Subject: [PATCH 034/868] Removed trailing spaces + converted tabs to spaces --- examples/debug.rs | 129 ++-- examples/test_content.css | 47 +- src/app.rs | 916 +++++++++++++------------- src/app_state.rs | 300 ++++----- src/cache.rs | 56 +- src/constraints.rs | 506 +++++++-------- src/css.rs | 450 ++++++------- src/css_parser.rs | 94 +-- src/display_list.rs | 198 +++--- src/dom.rs | 928 +++++++++++++-------------- src/font.rs | 82 +-- src/id_tree.rs | 1284 ++++++++++++++++++------------------- src/images.rs | 362 +++++------ src/input.rs | 216 +++---- src/lib.rs | 26 +- src/resources.rs | 260 ++++---- src/text_layout.rs | 40 +- src/traits.rs | 398 ++++++------ src/ui_description.rs | 126 ++-- src/ui_state.rs | 88 +-- src/window.rs | 22 +- 21 files changed, 3273 insertions(+), 3255 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 94c27abaa..08c4e4dd7 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,56 +1,73 @@ -extern crate azul; - -use azul::prelude::*; - -const TEST_CSS: &str = include_str!("test_content.css"); -const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); -const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); - -#[derive(Debug)] -pub struct MyAppData { - // Your app data goes here - pub my_data: u32, -} - -impl LayoutScreen for MyAppData { - - fn get_dom(&self, _window_id: WindowId) -> Dom { - - let mut dom = Dom::new(NodeType::Div); - dom.class("__azul-native-button"); - dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - - dom.add_sibling(Dom::new(NodeType::Label { - text: String::from("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."), - })); - - dom - } -} - -fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { - app_state.data.my_data += 1; - UpdateScreen::Redraw -} - -fn main() { - - let css = Css::new_from_string(TEST_CSS).unwrap(); - - let my_app_data = MyAppData { - my_data: 0, - }; - - let mut app = App::new(my_app_data); - - app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); - // app.delete_font("Webly Sleeky UI"); - - app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); - // app.delete_image("Cat01"); - - // TODO: Multi-window apps currently crash - // Need to re-factor the event loop for that - app.create_window(WindowCreateOptions::default(), css).unwrap(); - app.run(); -} +extern crate azul; + +use azul::prelude::*; + +const TEST_CSS: &str = include_str!("test_content.css"); +const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); +const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); + +#[derive(Debug)] +pub struct MyAppData { + // Your app data goes here + pub my_data: u32, +} + +impl LayoutScreen for MyAppData { + + fn get_dom(&self, _window_id: WindowId) -> Dom { + + let mut dom = Dom::new(NodeType::Label { + text: String::from(/*"\ + Lorem ipsum dolor sit amet, \ + consetetur sadipscing elitr, sed diam \ + nonumy eirmod tempor invidunt ut \ + labore et dolore magna aliquyam \ + erat, sed diam voluptua. At vero eos \ + et accusam et justo duo dolores et ea \ + rebum. Stet clita kasd gubergren, no \ + sea takimata sanctus est Lorem ipsum \ + dolor sit amet. Lorem ipsum dolor sit \ + amet, consetetur sadipscing elitr, sed diam \ + nonumy eirmod tempor invidunt \ + ut labore et dolore magna aliquyam \ + \ + erat, sed diam voluptua. At vero eos \ + et accusam et justo duo dolores et ea rebum. \ + Stet clita kasd gubergren, no sea takimata \ + sanctus est Lorem ipsum dolor sit amet."*/ "Azul"), + }); + dom.class("__azul-native-button"); + dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); + + // dom.add_sibling(Dom::new(NodeType::)); + + dom + } +} + +fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { + app_state.data.my_data += 1; + UpdateScreen::DontRedraw +} + +fn main() { + + let css = Css::new_from_string(TEST_CSS).unwrap(); + + let my_app_data = MyAppData { + my_data: 0, + }; + + let mut app = App::new(my_app_data); + + app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + // app.delete_font("Webly Sleeky UI"); + + app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); + // app.delete_image("Cat01"); + + // TODO: Multi-window apps currently crash + // Need to re-factor the event loop for that + app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run(); +} diff --git a/examples/test_content.css b/examples/test_content.css index 32fe3417c..3ab9e4b38 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -1,24 +1,25 @@ -.__azul-native-button { - background-color: #fcfcfc; - color: tomato; - border: 1px solid #b7b7b7; - border-radius: 4px; - box-shadow: 0px 0px 3px #c5c5c5ad; - /*background: image("Cat01");*/ - background: linear-gradient(#fcfcfc, #efefef); - width: 200px; - height: 200px; - min-height: 400px; - min-width: 400px; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-around; - align-items: center; - align-content: center; -} - -* { - font-size: 10px; - font-family: "Webly Sleeky UI", sans-serif; - color: black; +.__azul-native-button { + background-color: #fcfcfc; + color: tomato; + border: 1px solid #b7b7b7; + border-radius: 4px; + box-shadow: 0px 0px 3px #c5c5c5ad; + /*background: image("Cat01");*/ + background: linear-gradient(#fcfcfc, #efefef); + text-align: right; + width: 200px; + height: 200px; + min-height: 400px; + min-width: 400px; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-around; + align-items: center; + align-content: center; +} + +* { + font-size: 10px; + font-family: "Webly Sleeky UI", sans-serif; + color: black; } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 904eed401..55d97fbb6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,459 +1,459 @@ -use css::Css; -use resources::AppResources; -use app_state::AppState; -use traits::LayoutScreen; -use input::hit_test_ui; -use ui_state::UiState; -use ui_description::UiDescription; - -use std::sync::{Arc, Mutex}; -use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; -use glium::glutin::Event; -use euclid::TypedScale; -use std::io::Read; -use images::{ImageType}; -use image::ImageError; -use font::FontError; -use webrender::api::RenderApi; - -/// Graphical application that maintains some kind of application state -pub struct App<'a, T: LayoutScreen> { - /// The graphical windows, indexed by ID - windows: Vec>, - /// The global application state - pub app_state: Arc>>, -} - -pub(crate) struct FrameEventInfo { - pub(crate) should_redraw_window: bool, - pub(crate) should_swap_window: bool, - pub(crate) should_hittest: bool, - pub(crate) cur_cursor_pos: (f64, f64), - pub(crate) new_window_size: Option<(u32, u32)>, - pub(crate) new_dpi_factor: Option, -} - -impl Default for FrameEventInfo { - fn default() -> Self { - Self { - should_redraw_window: false, - should_swap_window: false, - should_hittest: false, - cur_cursor_pos: (0.0, 0.0), - new_window_size: None, - new_dpi_factor: None, - } - } -} - -impl<'a, T: LayoutScreen> App<'a, T> { - - /// Create a new, empty application. This does not open any windows. - pub fn new(initial_data: T) -> Self { - Self { - windows: Vec::new(), - app_state: Arc::new(Mutex::new(AppState::new(initial_data))), - } - } - - /// Spawn a new window on the screen. If an application has no windows, - /// the [`run`](#method.run) function will exit immediately. - pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { - self.windows.push(Window::new(options, css)?); - Ok(()) - } - - /// Start the rendering loop for the currently open windows - /// This is the "main app loop", "main game loop" or whatever you want to call it. - /// Usually this is the last function you call in your `main()` function, since exiting - /// it means that the user has closed all windows and wants to close the app. - pub fn run(&mut self) - { - let mut ui_state_cache = Vec::with_capacity(self.windows.len()); - let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - - // first redraw, initialize cache - { - let mut app_state = self.app_state.lock().unwrap(); - for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); - } - - // First repaint, otherwise the window would be black on startup - for (idx, window) in self.windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, - &ui_description_cache[idx], - &mut app_state.resources, - true); - window.display.swap_buffers().unwrap(); - } - } - - 'render_loop: loop { - - use webrender::api::{DeviceUintSize, WorldPoint, DeviceUintPoint, - DeviceUintRect, LayoutSize, Transaction}; - use dom::UpdateScreen; - - let mut closed_windows = Vec::::new(); - - let time_start = ::std::time::Instant::now(); - let mut debug_has_repainted = None; - - // TODO: Use threads on a per-window basis. - // Currently, events in one window will block all others - for (idx, ref mut window) in self.windows.iter_mut().enumerate() { - - let current_window_id = WindowId { id: idx }; - - let mut frame_event_info = FrameEventInfo::default(); - - window.events_loop.poll_events(|event| { - let should_close = process_event(event, &mut frame_event_info); - if should_close { - closed_windows.push(idx); - } - }); - - // update the state - if frame_event_info.should_swap_window { - window.display.swap_buffers().unwrap(); - } - - if frame_event_info.should_hittest { - - let cursor_x = frame_event_info.cur_cursor_pos.0 as f32; - let cursor_y = frame_event_info.cur_cursor_pos.1 as f32; - let point = WorldPoint::new(cursor_x, cursor_y); - let hit_test_results = hit_test_ui(&window.internal.api, - window.internal.document_id, - Some(window.internal.pipeline_id), - point); - - let mut should_update_screen = UpdateScreen::DontRedraw; - - for item in hit_test_results.items { - let callback_list_opt = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0); - if let Some(callback_list) = callback_list_opt { - // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) - // currently, just invoke all actions - for callback_id in callback_list.values() { - use dom::Callback::*; - let update = match ui_state_cache[idx].callback_list[callback_id] { - Sync(callback) => { (callback)(&mut *self.app_state.lock().unwrap()) }, - Async(callback) => { (callback)(self.app_state.clone()) }, - }; - if update == UpdateScreen::Redraw { - should_update_screen = UpdateScreen::Redraw; - } - } - } - } - - if should_update_screen == UpdateScreen::Redraw { - frame_event_info.should_redraw_window = true; - } - } - - let mut app_state = self.app_state.lock().unwrap(); - ui_state_cache[idx] = UiState::from_app_state(&*app_state, WindowId { id: idx }); - - if window.css.is_dirty { - frame_event_info.should_redraw_window = true; - } - - // Macro to avoid duplication between the new_window_size and the new_dpi_factor event - // TODO: refactor this into proper functions (when the WindowState is working) - macro_rules! update_display { - () => ( - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, - ¤t_window_id, - &ui_description_cache[idx], - &mut app_state.resources, - true); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - ) - } - - if let Some((w, h)) = frame_event_info.new_window_size { - window.internal.layout_size = LayoutSize::new(w as f32, h as f32); - window.internal.framebuffer_size = DeviceUintSize::new(w, h); - update_display!(); - continue; - } - - if let Some(dpi) = frame_event_info.new_dpi_factor { - window.internal.hidpi_factor = dpi; - update_display!(); - continue; - } - - if frame_event_info.should_redraw_window { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, - ¤t_window_id, - &ui_description_cache[idx], - &mut app_state.resources, - frame_event_info.new_window_size.is_some()); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - } - } - - // close windows if necessary - for closed_window_id in closed_windows { - let closed_window_id = closed_window_id; - ui_state_cache.remove(closed_window_id); - ui_description_cache.remove(closed_window_id); - self.windows.remove(closed_window_id); - } - - if self.windows.is_empty() { - break; - } else { - if let Some(restate_time) = debug_has_repainted { - println!("frame time: {:?} ms", restate_time.subsec_nanos() as f32 / 1_000_000.0); - } - ::std::thread::sleep(::std::time::Duration::from_millis(16)); - } - } - } - - /// Add an image to the internal resources - /// - /// ## Returns - /// - /// - `Ok(Some(()))` if an image with the same ID already exists. - /// - `Ok(None)` if the image was added, but didn't exist previously. - /// - `Err(e)` if the image couldn't be decoded - pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) - -> Result, ImageError> - { - (*self.app_state.lock().unwrap()).add_image(id, data, image_type) - } - - /// Removes an image from the internal app resources. - /// Returns `Some` if the image existed and was removed. - /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn delete_image>(&mut self, id: S) - -> Option<()> - { - (*self.app_state.lock().unwrap()).delete_image(id) - } - - /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) - -> bool - { - (*self.app_state.lock().unwrap()).has_image(id) - } - - /// Add a font (TTF or OTF) as a resource, identified by ID - /// - /// ## Returns - /// - /// - `Ok(Some(()))` if an font with the same ID already exists. - /// - `Ok(None)` if the font was added, but didn't exist previously. - /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, FontError> - { - (*self.app_state.lock().unwrap()).add_font(id, data) - } - - /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) - -> bool - { - (*self.app_state.lock().unwrap()).has_font(id) - } - - /// Deletes a font from the internal app resources. - /// - /// ## Arguments - /// - /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. - /// - /// ## Returns - /// - /// - `Some(())` if if the image existed and was successfully removed - /// - `None` if the given ID doesn't exist. In that case, the function does - /// nothing. - /// - /// Wrapper function for [`AppState::delete_font`]. After this function has been - /// called, you can be sure that the renderer doesn't know about your font anymore. - /// This also means that the font needs to be re-parsed if you want to add it again. - /// Use with care. - /// - /// ## Example - /// - #[cfg_attr(feature = "no-opengl-tests", doc = " ```no_run")] - #[cfg_attr(not(feature = "no-opengl-tests"), doc = " ```")] - /// # use azul::prelude::*; - /// # const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); - /// # - /// # struct MyAppData { } - /// # - /// # impl LayoutScreen for MyAppData { - /// # fn get_dom(&self, _window_id: WindowId) -> Dom { - /// # Dom::new(NodeType::Div) - /// # } - /// # } - /// # - /// # fn main() { - /// let mut app = App::new(MyAppData { }); - /// app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); - /// app.delete_font("Webly Sleeky UI"); - /// // NOTE: The font isn't immediately removed, only in the next draw call - /// app.mock_render_frame(); - /// assert!(!app.has_font("Webly Sleeky UI")); - /// # } - /// ``` - /// - /// [`AppState::delete_font`]: ../app_state/struct.AppState.html#method.delete_font - pub fn delete_font>(&mut self, id: S) - -> Option<()> - { - (*self.app_state.lock().unwrap()).delete_font(id) - } - - /// Mock rendering function, for creating a hidden window and rendering one frame - /// Used in unit tests. You **have** to enable software rendering, otherwise, - /// this function won't work in a headless environment. - /// - /// **NOTE**: In a headless environment, such as Travis, you have to use XVFB to - /// create a fake X11 server. XVFB also has a bug where it loads with the default of - /// 8-bit greyscale color (see [here]). In order to fix that, you have to run: - /// - /// `xvfb-run --server-args "-screen 0 1920x1080x24" cargo test --features "doc-test"` - /// - /// [here]: https://unix.stackexchange.com/questions/104914/ - /// - #[cfg(any(feature = "doc-test"))] - pub fn mock_render_frame(&mut self) { - use prelude::*; - let hidden_create_options = WindowCreateOptions { - class: WindowClass::Hidden, - /// force sofware renderer (OSMesa) - renderer_type: RendererType::Software, - .. Default::default() - }; - self.create_window(hidden_create_options, Css::native()).unwrap(); - let mut ui_state_cache = Vec::with_capacity(self.windows.len()); - let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - let mut app_state = self.app_state.lock().unwrap(); - - for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); - } - - for (idx, window) in self.windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, - &ui_description_cache[idx], - &mut app_state.resources, - true); - window.display.swap_buffers().unwrap(); - } - } -} - -fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { - use glium::glutin::WindowEvent; - match event { - Event::WindowEvent { - window_id, - event - } => { - match event { - WindowEvent::CursorMoved { - device_id, - position, - modifiers, - } => { - frame_event_info.should_hittest = true; - frame_event_info.cur_cursor_pos = position; - - let _ = window_id; - let _ = device_id; - let _ = modifiers; - }, - WindowEvent::Resized(w, h) => { - frame_event_info.new_window_size = Some((w, h)); - }, - WindowEvent::Refresh => { - frame_event_info.should_redraw_window = true; - }, - WindowEvent::HiDPIFactorChanged(dpi) => { - frame_event_info.new_dpi_factor = Some(dpi); - }, - WindowEvent::Closed => { - return true; - } - _ => { }, - } - }, - Event::Awakened => { - frame_event_info.should_swap_window = true; - }, - _ => { }, - } - - false -} - -fn render( - window: &mut Window, - _window_id: &WindowId, - ui_description: &UiDescription, - app_resources: &mut AppResources, - has_window_size_changed: bool) -{ - use webrender::api::*; - use display_list::DisplayList; - - let display_list = DisplayList::new_from_ui_description(ui_description); - let builder = display_list.into_display_list_builder( - window.internal.pipeline_id, - &mut window.solver, - &mut window.css, - app_resources, - &window.internal.api, - has_window_size_changed); - - if let Some(new_builder) = builder { - // only finalize the list if we actually need to. Otherwise just redraw the last display list - window.internal.last_display_list_builder = new_builder.finalize().2; - } - - let resources = ResourceUpdates::new(); - let mut txn = Transaction::new(); - - // TODO: something is wrong, the redraw times increase, even if the same display list is redrawn - txn.set_display_list( - window.internal.epoch, - None, - window.internal.layout_size, - (window.internal.pipeline_id, - window.solver.window_dimensions.layout_size, - window.internal.last_display_list_builder.clone()), - true, - ); - - txn.update_resources(resources); - txn.set_root_pipeline(window.internal.pipeline_id); - txn.generate_frame(); - window.internal.api.send_transaction(window.internal.document_id, txn); - - window.renderer.as_mut().unwrap().update(); - window.renderer.as_mut().unwrap().render(window.internal.framebuffer_size).unwrap(); +use css::Css; +use resources::AppResources; +use app_state::AppState; +use traits::LayoutScreen; +use input::hit_test_ui; +use ui_state::UiState; +use ui_description::UiDescription; + +use std::sync::{Arc, Mutex}; +use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; +use glium::glutin::Event; +use euclid::TypedScale; +use std::io::Read; +use images::{ImageType}; +use image::ImageError; +use font::FontError; +use webrender::api::RenderApi; + +/// Graphical application that maintains some kind of application state +pub struct App<'a, T: LayoutScreen> { + /// The graphical windows, indexed by ID + windows: Vec>, + /// The global application state + pub app_state: Arc>>, +} + +pub(crate) struct FrameEventInfo { + pub(crate) should_redraw_window: bool, + pub(crate) should_swap_window: bool, + pub(crate) should_hittest: bool, + pub(crate) cur_cursor_pos: (f64, f64), + pub(crate) new_window_size: Option<(u32, u32)>, + pub(crate) new_dpi_factor: Option, +} + +impl Default for FrameEventInfo { + fn default() -> Self { + Self { + should_redraw_window: false, + should_swap_window: false, + should_hittest: false, + cur_cursor_pos: (0.0, 0.0), + new_window_size: None, + new_dpi_factor: None, + } + } +} + +impl<'a, T: LayoutScreen> App<'a, T> { + + /// Create a new, empty application. This does not open any windows. + pub fn new(initial_data: T) -> Self { + Self { + windows: Vec::new(), + app_state: Arc::new(Mutex::new(AppState::new(initial_data))), + } + } + + /// Spawn a new window on the screen. If an application has no windows, + /// the [`run`](#method.run) function will exit immediately. + pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { + self.windows.push(Window::new(options, css)?); + Ok(()) + } + + /// Start the rendering loop for the currently open windows + /// This is the "main app loop", "main game loop" or whatever you want to call it. + /// Usually this is the last function you call in your `main()` function, since exiting + /// it means that the user has closed all windows and wants to close the app. + pub fn run(&mut self) + { + let mut ui_state_cache = Vec::with_capacity(self.windows.len()); + let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; + + // first redraw, initialize cache + { + let mut app_state = self.app_state.lock().unwrap(); + for (idx, _) in self.windows.iter().enumerate() { + ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); + } + + // First repaint, otherwise the window would be black on startup + for (idx, window) in self.windows.iter_mut().enumerate() { + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + render(window, &WindowId { id: idx, }, + &ui_description_cache[idx], + &mut app_state.resources, + true); + window.display.swap_buffers().unwrap(); + } + } + + 'render_loop: loop { + + use webrender::api::{DeviceUintSize, WorldPoint, DeviceUintPoint, + DeviceUintRect, LayoutSize, Transaction}; + use dom::UpdateScreen; + + let mut closed_windows = Vec::::new(); + + let time_start = ::std::time::Instant::now(); + let mut debug_has_repainted = None; + + // TODO: Use threads on a per-window basis. + // Currently, events in one window will block all others + for (idx, ref mut window) in self.windows.iter_mut().enumerate() { + + let current_window_id = WindowId { id: idx }; + + let mut frame_event_info = FrameEventInfo::default(); + + window.events_loop.poll_events(|event| { + let should_close = process_event(event, &mut frame_event_info); + if should_close { + closed_windows.push(idx); + } + }); + + // update the state + if frame_event_info.should_swap_window { + window.display.swap_buffers().unwrap(); + } + + if frame_event_info.should_hittest { + + let cursor_x = frame_event_info.cur_cursor_pos.0 as f32; + let cursor_y = frame_event_info.cur_cursor_pos.1 as f32; + let point = WorldPoint::new(cursor_x, cursor_y); + let hit_test_results = hit_test_ui(&window.internal.api, + window.internal.document_id, + Some(window.internal.pipeline_id), + point); + + let mut should_update_screen = UpdateScreen::DontRedraw; + + for item in hit_test_results.items { + let callback_list_opt = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0); + if let Some(callback_list) = callback_list_opt { + // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) + // currently, just invoke all actions + for callback_id in callback_list.values() { + use dom::Callback::*; + let update = match ui_state_cache[idx].callback_list[callback_id] { + Sync(callback) => { (callback)(&mut *self.app_state.lock().unwrap()) }, + Async(callback) => { (callback)(self.app_state.clone()) }, + }; + if update == UpdateScreen::Redraw { + should_update_screen = UpdateScreen::Redraw; + } + } + } + } + + if should_update_screen == UpdateScreen::Redraw { + frame_event_info.should_redraw_window = true; + } + } + + let mut app_state = self.app_state.lock().unwrap(); + ui_state_cache[idx] = UiState::from_app_state(&*app_state, WindowId { id: idx }); + + if window.css.is_dirty { + frame_event_info.should_redraw_window = true; + } + + // Macro to avoid duplication between the new_window_size and the new_dpi_factor event + // TODO: refactor this into proper functions (when the WindowState is working) + macro_rules! update_display { + () => ( + let mut txn = Transaction::new(); + let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); + + txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); + window.internal.api.send_transaction(window.internal.document_id, txn); + render(window, + ¤t_window_id, + &ui_description_cache[idx], + &mut app_state.resources, + true); + + let time_end = ::std::time::Instant::now(); + debug_has_repainted = Some(time_end - time_start); + ) + } + + if let Some((w, h)) = frame_event_info.new_window_size { + window.internal.layout_size = LayoutSize::new(w as f32, h as f32); + window.internal.framebuffer_size = DeviceUintSize::new(w, h); + update_display!(); + continue; + } + + if let Some(dpi) = frame_event_info.new_dpi_factor { + window.internal.hidpi_factor = dpi; + update_display!(); + continue; + } + + if frame_event_info.should_redraw_window { + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + render(window, + ¤t_window_id, + &ui_description_cache[idx], + &mut app_state.resources, + frame_event_info.new_window_size.is_some()); + + let time_end = ::std::time::Instant::now(); + debug_has_repainted = Some(time_end - time_start); + } + } + + // close windows if necessary + for closed_window_id in closed_windows { + let closed_window_id = closed_window_id; + ui_state_cache.remove(closed_window_id); + ui_description_cache.remove(closed_window_id); + self.windows.remove(closed_window_id); + } + + if self.windows.is_empty() { + break; + } else { + if let Some(restate_time) = debug_has_repainted { + println!("frame time: {:?} ms", restate_time.subsec_nanos() as f32 / 1_000_000.0); + } + ::std::thread::sleep(::std::time::Duration::from_millis(16)); + } + } + } + + /// Add an image to the internal resources + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an image with the same ID already exists. + /// - `Ok(None)` if the image was added, but didn't exist previously. + /// - `Err(e)` if the image couldn't be decoded + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) + -> Result, ImageError> + { + (*self.app_state.lock().unwrap()).add_image(id, data, image_type) + } + + /// Removes an image from the internal app resources. + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn delete_image>(&mut self, id: S) + -> Option<()> + { + (*self.app_state.lock().unwrap()).delete_image(id) + } + + /// Checks if an image is currently registered and ready-to-use + pub fn has_image>(&mut self, id: S) + -> bool + { + (*self.app_state.lock().unwrap()).has_image(id) + } + + /// Add a font (TTF or OTF) as a resource, identified by ID + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an font with the same ID already exists. + /// - `Ok(None)` if the font was added, but didn't exist previously. + /// - `Err(e)` if the font couldn't be decoded + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + -> Result, FontError> + { + (*self.app_state.lock().unwrap()).add_font(id, data) + } + + /// Checks if a font is currently registered and ready-to-use + pub fn has_font>(&mut self, id: S) + -> bool + { + (*self.app_state.lock().unwrap()).has_font(id) + } + + /// Deletes a font from the internal app resources. + /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. + /// + /// ## Returns + /// + /// - `Some(())` if if the image existed and was successfully removed + /// - `None` if the given ID doesn't exist. In that case, the function does + /// nothing. + /// + /// Wrapper function for [`AppState::delete_font`]. After this function has been + /// called, you can be sure that the renderer doesn't know about your font anymore. + /// This also means that the font needs to be re-parsed if you want to add it again. + /// Use with care. + /// + /// ## Example + /// + #[cfg_attr(feature = "no-opengl-tests", doc = " ```no_run")] + #[cfg_attr(not(feature = "no-opengl-tests"), doc = " ```")] + /// # use azul::prelude::*; + /// # const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); + /// # + /// # struct MyAppData { } + /// # + /// # impl LayoutScreen for MyAppData { + /// # fn get_dom(&self, _window_id: WindowId) -> Dom { + /// # Dom::new(NodeType::Div) + /// # } + /// # } + /// # + /// # fn main() { + /// let mut app = App::new(MyAppData { }); + /// app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + /// app.delete_font("Webly Sleeky UI"); + /// // NOTE: The font isn't immediately removed, only in the next draw call + /// app.mock_render_frame(); + /// assert!(!app.has_font("Webly Sleeky UI")); + /// # } + /// ``` + /// + /// [`AppState::delete_font`]: ../app_state/struct.AppState.html#method.delete_font + pub fn delete_font>(&mut self, id: S) + -> Option<()> + { + (*self.app_state.lock().unwrap()).delete_font(id) + } + + /// Mock rendering function, for creating a hidden window and rendering one frame + /// Used in unit tests. You **have** to enable software rendering, otherwise, + /// this function won't work in a headless environment. + /// + /// **NOTE**: In a headless environment, such as Travis, you have to use XVFB to + /// create a fake X11 server. XVFB also has a bug where it loads with the default of + /// 8-bit greyscale color (see [here]). In order to fix that, you have to run: + /// + /// `xvfb-run --server-args "-screen 0 1920x1080x24" cargo test --features "doc-test"` + /// + /// [here]: https://unix.stackexchange.com/questions/104914/ + /// + #[cfg(any(feature = "doc-test"))] + pub fn mock_render_frame(&mut self) { + use prelude::*; + let hidden_create_options = WindowCreateOptions { + class: WindowClass::Hidden, + /// force sofware renderer (OSMesa) + renderer_type: RendererType::Software, + .. Default::default() + }; + self.create_window(hidden_create_options, Css::native()).unwrap(); + let mut ui_state_cache = Vec::with_capacity(self.windows.len()); + let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; + let mut app_state = self.app_state.lock().unwrap(); + + for (idx, _) in self.windows.iter().enumerate() { + ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); + } + + for (idx, window) in self.windows.iter_mut().enumerate() { + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + render(window, &WindowId { id: idx, }, + &ui_description_cache[idx], + &mut app_state.resources, + true); + window.display.swap_buffers().unwrap(); + } + } +} + +fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { + use glium::glutin::WindowEvent; + match event { + Event::WindowEvent { + window_id, + event + } => { + match event { + WindowEvent::CursorMoved { + device_id, + position, + modifiers, + } => { + frame_event_info.should_hittest = true; + frame_event_info.cur_cursor_pos = position; + + let _ = window_id; + let _ = device_id; + let _ = modifiers; + }, + WindowEvent::Resized(w, h) => { + frame_event_info.new_window_size = Some((w, h)); + }, + WindowEvent::Refresh => { + frame_event_info.should_redraw_window = true; + }, + WindowEvent::HiDPIFactorChanged(dpi) => { + frame_event_info.new_dpi_factor = Some(dpi); + }, + WindowEvent::Closed => { + return true; + } + _ => { }, + } + }, + Event::Awakened => { + frame_event_info.should_swap_window = true; + }, + _ => { }, + } + + false +} + +fn render( + window: &mut Window, + _window_id: &WindowId, + ui_description: &UiDescription, + app_resources: &mut AppResources, + has_window_size_changed: bool) +{ + use webrender::api::*; + use display_list::DisplayList; + + let display_list = DisplayList::new_from_ui_description(ui_description); + let builder = display_list.into_display_list_builder( + window.internal.pipeline_id, + &mut window.solver, + &mut window.css, + app_resources, + &window.internal.api, + has_window_size_changed); + + if let Some(new_builder) = builder { + // only finalize the list if we actually need to. Otherwise just redraw the last display list + window.internal.last_display_list_builder = new_builder.finalize().2; + } + + let resources = ResourceUpdates::new(); + let mut txn = Transaction::new(); + + // TODO: something is wrong, the redraw times increase, even if the same display list is redrawn + txn.set_display_list( + window.internal.epoch, + None, + window.internal.layout_size, + (window.internal.pipeline_id, + window.solver.window_dimensions.layout_size, + window.internal.last_display_list_builder.clone()), + true, + ); + + txn.update_resources(resources); + txn.set_root_pipeline(window.internal.pipeline_id); + txn.generate_frame(); + window.internal.api.send_transaction(window.internal.document_id, txn); + + window.renderer.as_mut().unwrap().update(); + window.renderer.as_mut().unwrap().render(window.internal.framebuffer_size).unwrap(); } \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index f07e8794b..7ace53032 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,150 +1,150 @@ -use traits::LayoutScreen; -use resources::{AppResources}; -use std::io::Read; -use images::ImageType; -use image::ImageError; -use font::FontError; - -/// Wrapper for your application data. In order to be layout-able, -/// you need to satisfy the `LayoutScreen` trait (how the application -/// should be laid out) -pub struct AppState<'a, T: LayoutScreen> { - /// Your data (the global struct which all callbacks will have access to) - pub data: T, - /// Fonts and images that are currently loaded into the app - pub(crate) resources: AppResources<'a>, -} - -impl<'a, T: LayoutScreen> AppState<'a, T> { - - /// Creates a new `AppState` - pub fn new(initial_data: T) -> Self { - Self { - data: initial_data, - resources: AppResources::default(), - } - } - - /// Add an image to the internal resources. - /// - /// ## Arguments - /// - /// - `id`: A stringified ID (hash) for the image. It's recommended to use the - /// file path as the hash, maybe combined with a timestamp or a hash - /// of the file contents if the image will change. - /// - `data`: The data of the image - can be a File, a network stream, etc. - /// - `image_type`: If you know the type of image that you are adding, it is - /// recommended to specify it. In case you don't know, use - /// [`ImageType::GuessImageFormat`] - /// - /// ## Returns - /// - /// - `Ok(Some(()))` if an image with the same ID already exists. - /// - `Ok(None)` if the image was added, but didn't exist previously. - /// - `Err(e)` if the image couldn't be decoded - /// - /// **NOTE:** This function blocks the current thread. - /// - /// [`ImageType::GuessImageFormat`]: ../prelude/enum.ImageType.html#variant.GuessImageFormat - /// - pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) - -> Result, ImageError> - { - self.resources.add_image(id, data, image_type) - } - /// Checks if an image is currently registered and ready-to-use - pub fn has_image>(&mut self, id: S) - -> bool - { - self.resources.has_image(id) - } - - /// Removes an image from the internal app resources. - /// Returns `Some` if the image existed and was removed. - /// If the given ID doesn't exist, this function does nothing and returns `None`. - pub fn delete_image>(&mut self, id: S) - -> Option<()> - { - self.resources.delete_image(id) - } - - /// Add a font (TTF or OTF) to the internal resources - /// - /// ## Arguments - /// - /// - `id`: The stringified ID of the font to add, e.g. `"Helvetica-Bold"`. - /// - `data`: The bytes of the .ttf or .otf font file. Can be anything - /// that is read-able, i.e. a File, a network stream, etc. - /// - /// ## Returns - /// - /// - `Ok(Some(()))` if an font with the same ID already exists. - /// - `Ok(None)` if the font was added, but didn't exist previously. - /// - `Err(e)` if the font couldn't be decoded - /// - /// ## Example - /// - /// This function exists so you can add functions to the app-internal state - /// at runtime in a [`Callback`](../dom/enum.Callback.html) function. - /// - /// Here is an example of how to add a font at runtime (when the app is already running): - /// - /// ``` - /// # use azul::prelude::*; - /// const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); - /// - /// struct MyAppData { } - /// - /// impl LayoutScreen for MyAppData { - /// fn get_dom(&self, _window_id: WindowId) -> Dom { - /// let mut dom = Dom::new(NodeType::Div); - /// dom.event(On::MouseEnter, Callback::Sync(my_callback)); - /// dom - /// } - /// } - /// - /// fn my_callback(app_state: &mut AppState) -> UpdateScreen { - /// /// Here you can add your font at runtime to the app_state - /// app_state.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); - /// UpdateScreen::DontRedraw - /// } - /// ``` - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, FontError> - { - self.resources.add_font(id, data) - } - - /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) - -> bool - { - self.resources.has_font(id) - } - - /// Deletes a font from the internal app resources. - /// - /// ## Arguments - /// - /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. - /// - /// ## Returns - /// - /// - `Some(())` if if the image existed and was successfully removed - /// - `None` if the given ID doesn't exist. In that case, the function does - /// nothing. - /// - /// After this function has been - /// called, you can be sure that the renderer doesn't know about your font anymore. - /// This also means that the font needs to be re-parsed if you want to add it again. - /// Use with care. - /// - /// You can also call this function on an `App` struct, see [`App::add_font`]. - /// - /// [`App::add_font`]: ../app/struct.App.html#method.add_font - pub fn delete_font>(&mut self, id: S) - -> Option<()> - { - self.resources.delete_font(id) - } -} +use traits::LayoutScreen; +use resources::{AppResources}; +use std::io::Read; +use images::ImageType; +use image::ImageError; +use font::FontError; + +/// Wrapper for your application data. In order to be layout-able, +/// you need to satisfy the `LayoutScreen` trait (how the application +/// should be laid out) +pub struct AppState<'a, T: LayoutScreen> { + /// Your data (the global struct which all callbacks will have access to) + pub data: T, + /// Fonts and images that are currently loaded into the app + pub(crate) resources: AppResources<'a>, +} + +impl<'a, T: LayoutScreen> AppState<'a, T> { + + /// Creates a new `AppState` + pub fn new(initial_data: T) -> Self { + Self { + data: initial_data, + resources: AppResources::default(), + } + } + + /// Add an image to the internal resources. + /// + /// ## Arguments + /// + /// - `id`: A stringified ID (hash) for the image. It's recommended to use the + /// file path as the hash, maybe combined with a timestamp or a hash + /// of the file contents if the image will change. + /// - `data`: The data of the image - can be a File, a network stream, etc. + /// - `image_type`: If you know the type of image that you are adding, it is + /// recommended to specify it. In case you don't know, use + /// [`ImageType::GuessImageFormat`] + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an image with the same ID already exists. + /// - `Ok(None)` if the image was added, but didn't exist previously. + /// - `Err(e)` if the image couldn't be decoded + /// + /// **NOTE:** This function blocks the current thread. + /// + /// [`ImageType::GuessImageFormat`]: ../prelude/enum.ImageType.html#variant.GuessImageFormat + /// + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) + -> Result, ImageError> + { + self.resources.add_image(id, data, image_type) + } + /// Checks if an image is currently registered and ready-to-use + pub fn has_image>(&mut self, id: S) + -> bool + { + self.resources.has_image(id) + } + + /// Removes an image from the internal app resources. + /// Returns `Some` if the image existed and was removed. + /// If the given ID doesn't exist, this function does nothing and returns `None`. + pub fn delete_image>(&mut self, id: S) + -> Option<()> + { + self.resources.delete_image(id) + } + + /// Add a font (TTF or OTF) to the internal resources + /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to add, e.g. `"Helvetica-Bold"`. + /// - `data`: The bytes of the .ttf or .otf font file. Can be anything + /// that is read-able, i.e. a File, a network stream, etc. + /// + /// ## Returns + /// + /// - `Ok(Some(()))` if an font with the same ID already exists. + /// - `Ok(None)` if the font was added, but didn't exist previously. + /// - `Err(e)` if the font couldn't be decoded + /// + /// ## Example + /// + /// This function exists so you can add functions to the app-internal state + /// at runtime in a [`Callback`](../dom/enum.Callback.html) function. + /// + /// Here is an example of how to add a font at runtime (when the app is already running): + /// + /// ``` + /// # use azul::prelude::*; + /// const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); + /// + /// struct MyAppData { } + /// + /// impl LayoutScreen for MyAppData { + /// fn get_dom(&self, _window_id: WindowId) -> Dom { + /// let mut dom = Dom::new(NodeType::Div); + /// dom.event(On::MouseEnter, Callback::Sync(my_callback)); + /// dom + /// } + /// } + /// + /// fn my_callback(app_state: &mut AppState) -> UpdateScreen { + /// /// Here you can add your font at runtime to the app_state + /// app_state.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + /// UpdateScreen::DontRedraw + /// } + /// ``` + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + -> Result, FontError> + { + self.resources.add_font(id, data) + } + + /// Checks if a font is currently registered and ready-to-use + pub fn has_font>(&mut self, id: S) + -> bool + { + self.resources.has_font(id) + } + + /// Deletes a font from the internal app resources. + /// + /// ## Arguments + /// + /// - `id`: The stringified ID of the font to remove, e.g. `"Helvetica-Bold"`. + /// + /// ## Returns + /// + /// - `Some(())` if if the image existed and was successfully removed + /// - `None` if the given ID doesn't exist. In that case, the function does + /// nothing. + /// + /// After this function has been + /// called, you can be sure that the renderer doesn't know about your font anymore. + /// This also means that the font needs to be re-parsed if you want to add it again. + /// Use with care. + /// + /// You can also call this function on an `App` struct, see [`App::add_font`]. + /// + /// [`App::add_font`]: ../app/struct.App.html#method.add_font + pub fn delete_font>(&mut self, id: S) + -> Option<()> + { + self.resources.delete_font(id) + } +} diff --git a/src/cache.rs b/src/cache.rs index 3aea9449a..de7ee35dd 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,22 +1,22 @@ //! DOM cache for library-internal use -//! +//! //! # Diffing the DOM -//! +//! //! Changes in the DOM can happen in three ways: -//! +//! //! - An element changes its content An element is pushed as a child The order / childs of an element //! - are restructured -//! +//! //! In order for the caching to be effective, we need to solve the problem of only adding //! EditVariable-s if needed. In order to do that, we need two elements for each DOM node: -//! -//! - The self-hash (the hash of the current DOM node, including hashing the content) -//! - The hashes of the individual children (like a `Vec`), in their correct order -//! +//! +//! - The self-hash (the hash of the current DOM node, including hashing the content) +//! - The hashes of the individual children (like a `Vec`), in their correct order +//! //! For detecting these changes, we build an `Arena` (empty on startup) and a //! `HashMap<(DomHash, bool) -> LayoutRect>`. The latter stores all active EditVariables. Whenever we //! insert or remove from the HashMap, we also remove the variables from the solver -//! +//! //! When a re-layout is required, we hash the nodes from the UiDescription, starting from the root. Each //! time we go to the next sibling / next child, this change is also reflected by going through the //! `Arena`. For each node, we calculate the self-hash of the node and compare it with the hash @@ -24,13 +24,13 @@ //! insert it in the `HashMap<(DomHash, bool)`, create a new LayoutRect and add the variables to the //! solver. We set the `bool` to true to indicate, that this hash is currently active in the DOM and //! should not be removed. Then we add the hash to the `Arena`. -//! +//! //! If there is a hash, but the hashes differ, this means that either the order of the current siblings //! were changed or the actual contents of the node were changed. So we look up the hash in the //! `HashMap<(DomHash, bool)>`. If we can find it, this means that we already have EditVariables in the //! solver corresponding to the node and the node was simply reordered. //! If we can't find it, it's either a completely new DOM element or the contents of the node have changed. -//! +//! //! Lastly, we go through the `HashMap<(DomHash, bool)>` and remove the edit variables if the `bool` is false, //! meaning that the variable was not present in the current DOM tree, so leaving the variables in the solver //! would be garbage. @@ -44,7 +44,7 @@ use dom::NodeData; use std::ops::Deref; /// We keep the tree from the previous re-layout. Then, when a re-layout is required, -/// we re-hash all the nodes, insert the +/// we re-hash all the nodes, insert the #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub(crate) struct DomTreeCache { pub(crate) previous_layout: HashedDomTree, @@ -84,8 +84,8 @@ impl DomTreeCache { } pub(crate) fn update(&mut self, new_root: NodeId, new_nodes_arena: &Arena>) -> DomChangeSet { - - use std::hash::Hash; + + use std::hash::Hash; if let Some(previous_root) = self.previous_layout.root { // let mut changeset = DomChangeSet::empty(); @@ -106,7 +106,7 @@ impl DomTreeCache { } fn update_tree_inner_2(previous_arena: &Arena, next_arena: &Arena) -> DomChangeSet { - + let mut previous_iter = previous_arena.nodes.iter(); let mut next_iter = next_arena.nodes.iter().enumerate(); let mut changeset = DomChangeSet::empty(); @@ -127,7 +127,7 @@ impl DomTreeCache { (None, None) => { // println!("chrildren: old has no children, new has no children!"); break; - }, + }, (Some(_), None) => { prev = previous_iter.next(); }, @@ -151,10 +151,10 @@ impl DomTreeCache { changeset } - fn update_tree_inner(previous_root: NodeId, - previous_hash_arena: &Arena, - current_root: NodeId, - current_dom_arena: &Arena>, + fn update_tree_inner(previous_root: NodeId, + previous_hash_arena: &Arena, + current_root: NodeId, + current_dom_arena: &Arena>, changeset: &mut DomChangeSet) where T: LayoutScreen { @@ -173,7 +173,7 @@ impl DomTreeCache { (None, None) => { // println!("chrildren: old has no children, new has no children!"); break; - }, + }, (Some(old_hash), None) => { // meaning, the whole subtree should be removed // println!("chrildren: old has children at id: {:?}, new has children at id:", old_hash); @@ -195,8 +195,8 @@ impl DomTreeCache { } } } - } - + } + let mut old_iterator = previous_root.following_siblings(previous_hash_arena); let mut new_iterator = current_root.following_siblings(current_dom_arena); @@ -208,7 +208,7 @@ impl DomTreeCache { match (old_next, new_next) { (None, None) => { // both old and new node have the same length - break; + break; }, (None, Some(new_next_node_id)) => { // new node was pushed as a child @@ -221,7 +221,7 @@ impl DomTreeCache { // mark node as inactive let old_hash = previous_hash_arena[old_hash_id].data; // println!("siblings: node was removed as a child: {:?}", old_hash); - }, + }, (Some(old_hash_id), Some(new_next_node_id)) => { let old_hash = previous_hash_arena[old_hash_id].data; let new_hash = current_dom_arena[new_next_node_id].data.calculate_node_data_hash(); @@ -287,16 +287,16 @@ impl EditVariableCache { } } - /// Last step of the caching algorithm: + /// Last step of the caching algorithm: /// Remove all edit variables where the `bool` is set to false pub(crate) fn remove_unused_variables(&mut self, solver: &mut Solver) { - + let mut to_be_removed = Vec::::new(); for (key, &(active, variable_rect)) in &self.map { if !active { variable_rect.remove_from_solver(solver); - to_be_removed.push(*key); + to_be_removed.push(*key); } } diff --git a/src/constraints.rs b/src/constraints.rs index f58fe0bd4..ac3866ba5 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -1,254 +1,254 @@ -//! Constraint building (mostly taken from `limn_layout`) - -use cassowary::{Solver, Variable, Constraint}; -use cassowary::WeightedRelation::{EQ, GE}; -use cassowary::strength::{WEAK, REQUIRED}; -use euclid::{Point2D, Size2D}; - -pub type Size = Size2D; -pub type Point = Point2D; - -/// A set of cassowary `Variable`s representing the -/// bounding rectangle of a layout. -#[derive(Debug, Copy, Clone)] -pub(crate) struct DisplayRect { - pub left: Variable, - pub top: Variable, - pub right: Variable, - pub bottom: Variable, - pub width: Variable, - pub height: Variable, -} - -impl Default for DisplayRect { - fn default() -> Self { - Self { - left: Variable::new(), - top: Variable::new(), - right: Variable::new(), - bottom: Variable::new(), - width: Variable::new(), - height: Variable::new(), - } - } -} - -impl DisplayRect { - - pub fn add_to_solver(&self, solver: &mut Solver) { - solver.add_edit_variable(self.left, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.top, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.right, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.bottom, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.width, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.height, WEAK).unwrap_or_else(|_e| { }); - } - - pub fn remove_from_solver(&self, solver: &mut Solver) { - solver.remove_edit_variable(self.left).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.top).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.right).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.bottom).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.width).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.height).unwrap_or_else(|_e| { }); - } - -} - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Strength(pub f64); - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Padding(pub f32); - -#[derive(Debug, Copy, Clone)] -pub(crate) enum CssConstraint { - Size((SizeConstraint, Strength)), - Padding((PaddingConstraint, Strength, Padding)) -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum SizeConstraint { - Width(f32), - Height(f32), - MinWidth(f32), - MinHeight(f32), - Size(Size), - MinSize(Size), - AspectRatio(f32), - Shrink, - ShrinkHorizontal, - ShrinkVertical, - TopLeft(Point), - Center(DisplayRect), - CenterHorizontal(Variable, Variable), - CenterVertical(Variable, Variable), -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum PaddingConstraint { - AlignTop(Variable), - AlignBottom(Variable), - AlignLeft(Variable), - AlignRight(Variable), - AlignAbove(Variable), - AlignBelow(Variable), - AlignToLeftOf(Variable), - AlignToRightOf(Variable), - Above(Variable), - Below(Variable), - ToLeftOf(Variable), - ToRightOf(Variable), - BoundLeft(Variable), - BoundTop(Variable), - BoundRight(Variable), - BoundBottom(Variable), - BoundBy(DisplayRect), - MatchLayout(DisplayRect), - MatchWidth(Variable), - MatchHeight(Variable), -} - -impl SizeConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64) -> Vec { - use self::SizeConstraint::*; - - match *self { - Width(width) => { - vec![ rect.width | EQ(strength) | width ] - }, - Height(height) => { - vec![ rect.height | EQ(strength) | height ] - }, - MinWidth(width) => { - vec![ rect.width | GE(strength) | width ] - }, - MinHeight(height) => { - vec![ rect.height | GE(strength) | height ] - }, - Size(size) => { - vec![ - rect.width | EQ(strength) | size.width, - rect.height | EQ(strength) | size.height, - ] - }, - MinSize(size) => { - vec![ - rect.width | GE(strength) | size.width, - rect.height | GE(strength) | size.height, - ] - }, - AspectRatio(aspect_ratio) => { - vec![ aspect_ratio * rect.width | EQ(strength) | rect.height ] - }, - Shrink => { - vec![ - rect.width | EQ(strength) | 0.0, - rect.height | EQ(strength) | 0.0, - ] - }, - ShrinkHorizontal => { - vec![ rect.width | EQ(strength) | 0.0 ] - }, - ShrinkVertical => { - vec![ rect.height | EQ(strength) | 0.0 ] - }, - TopLeft(point) => { - vec![ - rect.left | EQ(strength) | point.x, - rect.top | EQ(strength) | point.y, - ] - }, - Center(other) => { - vec![ - rect.left - other.left | EQ(REQUIRED) | other.right - rect.right, - rect.top - other.top | EQ(REQUIRED) | other.bottom - rect.bottom, - ] - }, - CenterHorizontal(left, right) => { - vec![ rect.left - left | EQ(REQUIRED) | right - rect.right ] - }, - CenterVertical(top, bottom) => { - vec![ rect.top - top | EQ(REQUIRED) | bottom - rect.bottom ] - }, - } - } -} - -impl PaddingConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64, padding: f32) -> Vec { - use self::PaddingConstraint::*; - match *self { - AlignTop(top) => { - vec![ rect.top - top | EQ(strength) | padding ] - }, - AlignBottom(bottom) => { - vec![ bottom - rect.bottom | EQ(strength) | padding ] - }, - AlignLeft(left) => { - vec![ rect.left - left | EQ(strength) | padding ] - }, - AlignRight(right) => { - vec![ right - rect.right | EQ(strength) | padding ] - }, - AlignAbove(top) => { - vec![ top - rect.bottom | EQ(strength) | padding ] - }, - AlignBelow(bottom) => { - vec![ rect.top - bottom | EQ(strength) | padding ] - }, - AlignToLeftOf(left) => { - vec![ left - rect.right | EQ(strength) | padding ] - }, - AlignToRightOf(right) => { - vec![ rect.left - right | EQ(strength) | padding ] - }, - Above(top) => { - vec![ top - rect.bottom | GE(strength) | padding ] - }, - Below(bottom) => { - vec![ rect.top - bottom | GE(strength) | padding ] - }, - ToLeftOf(left) => { - vec![ left - rect.right | GE(strength) | padding ] - }, - ToRightOf(right) => { - vec![ rect.left - right | GE(strength) | padding ] - }, - BoundLeft(left) => { - vec![ rect.left - left | GE(strength) | padding ] - }, - BoundTop(top) => { - vec![ rect.top - top | GE(strength) | padding ] - }, - BoundRight(right) => { - vec![ right - rect.right | GE(strength) | padding ] - }, - BoundBottom(bottom) => { - vec![ bottom - rect.bottom | GE(strength) | padding ] - }, - BoundBy(other) => { - vec![ - rect.left - other.left | GE(strength) | padding, - rect.top - other.top | GE(strength) | padding, - other.right - rect.right | GE(strength) | padding, - other.bottom - rect.bottom | GE(strength) | padding, - ] - }, - MatchLayout(other) => { - vec![ - rect.left - other.left | EQ(strength) | padding, - rect.top - other.top | EQ(strength) | padding, - other.right - rect.right | EQ(strength) | padding, - other.bottom - rect.bottom | EQ(strength) | padding, - ] - }, - MatchWidth(width) => { - vec![ width - rect.width | EQ(strength) | padding ] - }, - MatchHeight(height) => { - vec![ height - rect.height | EQ(strength) | padding ] - }, - } - } +//! Constraint building (mostly taken from `limn_layout`) + +use cassowary::{Solver, Variable, Constraint}; +use cassowary::WeightedRelation::{EQ, GE}; +use cassowary::strength::{WEAK, REQUIRED}; +use euclid::{Point2D, Size2D}; + +pub type Size = Size2D; +pub type Point = Point2D; + +/// A set of cassowary `Variable`s representing the +/// bounding rectangle of a layout. +#[derive(Debug, Copy, Clone)] +pub(crate) struct DisplayRect { + pub left: Variable, + pub top: Variable, + pub right: Variable, + pub bottom: Variable, + pub width: Variable, + pub height: Variable, +} + +impl Default for DisplayRect { + fn default() -> Self { + Self { + left: Variable::new(), + top: Variable::new(), + right: Variable::new(), + bottom: Variable::new(), + width: Variable::new(), + height: Variable::new(), + } + } +} + +impl DisplayRect { + + pub fn add_to_solver(&self, solver: &mut Solver) { + solver.add_edit_variable(self.left, WEAK).unwrap_or_else(|_e| { }); + solver.add_edit_variable(self.top, WEAK).unwrap_or_else(|_e| { }); + solver.add_edit_variable(self.right, WEAK).unwrap_or_else(|_e| { }); + solver.add_edit_variable(self.bottom, WEAK).unwrap_or_else(|_e| { }); + solver.add_edit_variable(self.width, WEAK).unwrap_or_else(|_e| { }); + solver.add_edit_variable(self.height, WEAK).unwrap_or_else(|_e| { }); + } + + pub fn remove_from_solver(&self, solver: &mut Solver) { + solver.remove_edit_variable(self.left).unwrap_or_else(|_e| { }); + solver.remove_edit_variable(self.top).unwrap_or_else(|_e| { }); + solver.remove_edit_variable(self.right).unwrap_or_else(|_e| { }); + solver.remove_edit_variable(self.bottom).unwrap_or_else(|_e| { }); + solver.remove_edit_variable(self.width).unwrap_or_else(|_e| { }); + solver.remove_edit_variable(self.height).unwrap_or_else(|_e| { }); + } + +} + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +pub struct Strength(pub f64); + +#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] +pub struct Padding(pub f32); + +#[derive(Debug, Copy, Clone)] +pub(crate) enum CssConstraint { + Size((SizeConstraint, Strength)), + Padding((PaddingConstraint, Strength, Padding)) +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum SizeConstraint { + Width(f32), + Height(f32), + MinWidth(f32), + MinHeight(f32), + Size(Size), + MinSize(Size), + AspectRatio(f32), + Shrink, + ShrinkHorizontal, + ShrinkVertical, + TopLeft(Point), + Center(DisplayRect), + CenterHorizontal(Variable, Variable), + CenterVertical(Variable, Variable), +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum PaddingConstraint { + AlignTop(Variable), + AlignBottom(Variable), + AlignLeft(Variable), + AlignRight(Variable), + AlignAbove(Variable), + AlignBelow(Variable), + AlignToLeftOf(Variable), + AlignToRightOf(Variable), + Above(Variable), + Below(Variable), + ToLeftOf(Variable), + ToRightOf(Variable), + BoundLeft(Variable), + BoundTop(Variable), + BoundRight(Variable), + BoundBottom(Variable), + BoundBy(DisplayRect), + MatchLayout(DisplayRect), + MatchWidth(Variable), + MatchHeight(Variable), +} + +impl SizeConstraint { + pub(crate) fn build(&self, rect: &DisplayRect, strength: f64) -> Vec { + use self::SizeConstraint::*; + + match *self { + Width(width) => { + vec![ rect.width | EQ(strength) | width ] + }, + Height(height) => { + vec![ rect.height | EQ(strength) | height ] + }, + MinWidth(width) => { + vec![ rect.width | GE(strength) | width ] + }, + MinHeight(height) => { + vec![ rect.height | GE(strength) | height ] + }, + Size(size) => { + vec![ + rect.width | EQ(strength) | size.width, + rect.height | EQ(strength) | size.height, + ] + }, + MinSize(size) => { + vec![ + rect.width | GE(strength) | size.width, + rect.height | GE(strength) | size.height, + ] + }, + AspectRatio(aspect_ratio) => { + vec![ aspect_ratio * rect.width | EQ(strength) | rect.height ] + }, + Shrink => { + vec![ + rect.width | EQ(strength) | 0.0, + rect.height | EQ(strength) | 0.0, + ] + }, + ShrinkHorizontal => { + vec![ rect.width | EQ(strength) | 0.0 ] + }, + ShrinkVertical => { + vec![ rect.height | EQ(strength) | 0.0 ] + }, + TopLeft(point) => { + vec![ + rect.left | EQ(strength) | point.x, + rect.top | EQ(strength) | point.y, + ] + }, + Center(other) => { + vec![ + rect.left - other.left | EQ(REQUIRED) | other.right - rect.right, + rect.top - other.top | EQ(REQUIRED) | other.bottom - rect.bottom, + ] + }, + CenterHorizontal(left, right) => { + vec![ rect.left - left | EQ(REQUIRED) | right - rect.right ] + }, + CenterVertical(top, bottom) => { + vec![ rect.top - top | EQ(REQUIRED) | bottom - rect.bottom ] + }, + } + } +} + +impl PaddingConstraint { + pub(crate) fn build(&self, rect: &DisplayRect, strength: f64, padding: f32) -> Vec { + use self::PaddingConstraint::*; + match *self { + AlignTop(top) => { + vec![ rect.top - top | EQ(strength) | padding ] + }, + AlignBottom(bottom) => { + vec![ bottom - rect.bottom | EQ(strength) | padding ] + }, + AlignLeft(left) => { + vec![ rect.left - left | EQ(strength) | padding ] + }, + AlignRight(right) => { + vec![ right - rect.right | EQ(strength) | padding ] + }, + AlignAbove(top) => { + vec![ top - rect.bottom | EQ(strength) | padding ] + }, + AlignBelow(bottom) => { + vec![ rect.top - bottom | EQ(strength) | padding ] + }, + AlignToLeftOf(left) => { + vec![ left - rect.right | EQ(strength) | padding ] + }, + AlignToRightOf(right) => { + vec![ rect.left - right | EQ(strength) | padding ] + }, + Above(top) => { + vec![ top - rect.bottom | GE(strength) | padding ] + }, + Below(bottom) => { + vec![ rect.top - bottom | GE(strength) | padding ] + }, + ToLeftOf(left) => { + vec![ left - rect.right | GE(strength) | padding ] + }, + ToRightOf(right) => { + vec![ rect.left - right | GE(strength) | padding ] + }, + BoundLeft(left) => { + vec![ rect.left - left | GE(strength) | padding ] + }, + BoundTop(top) => { + vec![ rect.top - top | GE(strength) | padding ] + }, + BoundRight(right) => { + vec![ right - rect.right | GE(strength) | padding ] + }, + BoundBottom(bottom) => { + vec![ bottom - rect.bottom | GE(strength) | padding ] + }, + BoundBy(other) => { + vec![ + rect.left - other.left | GE(strength) | padding, + rect.top - other.top | GE(strength) | padding, + other.right - rect.right | GE(strength) | padding, + other.bottom - rect.bottom | GE(strength) | padding, + ] + }, + MatchLayout(other) => { + vec![ + rect.left - other.left | EQ(strength) | padding, + rect.top - other.top | EQ(strength) | padding, + other.right - rect.right | EQ(strength) | padding, + other.bottom - rect.bottom | EQ(strength) | padding, + ] + }, + MatchWidth(width) => { + vec![ width - rect.width | EQ(strength) | padding ] + }, + MatchHeight(height) => { + vec![ height - rect.height | EQ(strength) | padding ] + }, + } + } } \ No newline at end of file diff --git a/src/css.rs b/src/css.rs index f773ab03d..efedba697 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,226 +1,226 @@ -//! CSS parsing and styling -use std::ops::Add; - -#[cfg(target_os="windows")] -const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); -#[cfg(target_os="linux")] -const NATIVE_CSS_LINUX: &str = include_str!("../assets/native_linux.css"); -#[cfg(target_os="macos")] -const NATIVE_CSS_MACOS: &str = include_str!("../assets/native_macos.css"); - -/// All the keys that, when changed, can trigger a re-layout -const RELAYOUT_RULES: [&str;11] = [ - "border", "width", "height", "min-width", "min-height", - "direction", "wrap", "justify-content", "align-items", "align-content", - "order" -]; - -/// Wrapper for a `Vec`. Fields are private, because the `Css` -/// struct does caching - each time you add / subtract a `Css`, it is checked -/// if the added / removed CSS rules change the actual layout. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Css { - // NOTE: Each time the rules are modified, the `dirty` flag - // has to be set accordingly for the CSS to update! - pub(crate) rules: Vec, - /// - pub(crate) is_dirty: bool, - /// Has the CSS changed in a way where it needs a re-layout? - /// - /// Ex. if only a background color has changed, we need to redraw, but we - /// don't need to re-layout the frame - pub(crate) needs_relayout: bool, -} - -/// Error that can happen during the parsing of a CSS value -#[derive(Debug, Clone)] -pub enum CssParseError { - /// A hard error in the CSS syntax - ParseError(::simplecss::Error), - /// Braces are not balanced properly - UnclosedBlock, - /// Invalid syntax, such as `#div { #div: "my-value" }` - MalformedCss, -} - -/// Rule that applies to some "path" in the CSS, i.e. -/// `div#myid.myclass -> ("justify-content", "center")` -/// -/// The CSS rule is currently not cascaded, use `Css::new_from_string()` -/// to do the cascading. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct CssRule { - /// `div` (`*` by default) - pub html_type: String, - /// `#myid` (`None` by default) - pub id: Option, - /// `.myclass .myotherclass` (vec![] by default) - pub classes: Vec, - /// `("justify-content", "center")` - pub declaration: (String, String), -} - -impl CssRule { - pub fn needs_relayout(&self) -> bool { - RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) - } -} - -impl Css { - - /// Creates an empty set of CSS rules - pub fn empty() -> Self { - Self { - rules: Vec::new(), - is_dirty: false, - needs_relayout: false, - } - } - - /// Parses a CSS string (single-threaded) and returns the parsed rules - pub fn new_from_string(css_string: &str) -> Result { - use simplecss::{Tokenizer, Token}; - use std::collections::HashSet; - - let mut tokenizer = Tokenizer::new(css_string); - - let mut block_nesting = 0_usize; - let mut css_rules = Vec::::new(); - - // TODO: For now, rules may not be nested, otherwise, this won't work - // TODO: This could be more efficient. We don't even need to clone the - // strings, but this is just a quick-n-dirty CSS parser - // This will also use up a lot of memory, since the strings get duplicated - - let mut parser_in_block = false; - let mut current_type = "*"; - let mut current_id = None; - let mut current_classes = HashSet::<&str>::new(); - - 'css_parse_loop: loop { - let tokenize_result = tokenizer.parse_next(); - match tokenize_result { - Ok(token) => { - match token { - Token::EndOfStream => { - break 'css_parse_loop; - }, - Token::BlockStart => { - parser_in_block = true; - block_nesting += 1; - }, - Token::BlockEnd => { - block_nesting -= 1; - parser_in_block = false; - current_type = "*"; - current_id = None; - current_classes = HashSet::<&str>::new(); - }, - Token::TypeSelector(div_type) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_type = div_type; - }, - Token::IdSelector(id) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_id = Some(id.to_string()); - } - Token::ClassSelector(class) => { - if parser_in_block { - return Err(CssParseError::MalformedCss); - } - current_classes.insert(class); - } - Token::Declaration(key, val) => { - if !parser_in_block { - return Err(CssParseError::MalformedCss); - } - let mut css_rule = CssRule { - html_type: current_type.to_string(), - id: current_id.clone(), - classes: current_classes.iter().map(|e| e.to_string()).collect::>(), - declaration: (key.to_string(), val.to_string()), - }; - // IMPORTANT! - css_rule.classes.sort(); - css_rules.push(css_rule); - }, - _ => { } - } - }, - Err(e) => { - return Err(CssParseError::ParseError(e)); - } - } - } - - // non-even number of blocks - if block_nesting != 0 { - return Err(CssParseError::UnclosedBlock); - } - - Ok(Self { - rules: css_rules, - // force repaint for the first frame - is_dirty: true, - // force re-layout for the first frame - needs_relayout: true, - }) - } - - /// Adds a CSS rule - pub fn add_rule(&mut self, css_rule: CssRule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.push(css_rule); - self.is_dirty = true; - } - - /// Removes a rule from the current stylesheet - pub fn remove_rule(&mut self, css_rule: &CssRule) { - if let Some(pos) = self.rules.iter().position(|x| *x == *css_rule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.remove(pos); - self.is_dirty = true; - } - } - - /// Returns the native style for the OS - #[cfg(target_os="windows")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_WINDOWS).unwrap() - } - - /// Returns the native style for the OS - #[cfg(target_os="linux")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_LINUX).unwrap() - } - - /// Returns the native style for the OS - #[cfg(target_os="macos")] - pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_MACOS).unwrap() - } -} - -impl Add for Css { - type Output = Css; - - fn add(mut self, mut other: Css) -> Css { - let needs_relayout = if !other.needs_relayout { - other.rules.iter().any(|r| r.needs_relayout()) - } else { - other.needs_relayout - }; - - self.rules.append(&mut other.rules); - Css { - rules: self.rules, - is_dirty: true, - needs_relayout: needs_relayout, - } - } +//! CSS parsing and styling +use std::ops::Add; + +#[cfg(target_os="windows")] +const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); +#[cfg(target_os="linux")] +const NATIVE_CSS_LINUX: &str = include_str!("../assets/native_linux.css"); +#[cfg(target_os="macos")] +const NATIVE_CSS_MACOS: &str = include_str!("../assets/native_macos.css"); + +/// All the keys that, when changed, can trigger a re-layout +const RELAYOUT_RULES: [&str;11] = [ + "border", "width", "height", "min-width", "min-height", + "direction", "wrap", "justify-content", "align-items", "align-content", + "order" +]; + +/// Wrapper for a `Vec`. Fields are private, because the `Css` +/// struct does caching - each time you add / subtract a `Css`, it is checked +/// if the added / removed CSS rules change the actual layout. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Css { + // NOTE: Each time the rules are modified, the `dirty` flag + // has to be set accordingly for the CSS to update! + pub(crate) rules: Vec, + /// + pub(crate) is_dirty: bool, + /// Has the CSS changed in a way where it needs a re-layout? + /// + /// Ex. if only a background color has changed, we need to redraw, but we + /// don't need to re-layout the frame + pub(crate) needs_relayout: bool, +} + +/// Error that can happen during the parsing of a CSS value +#[derive(Debug, Clone)] +pub enum CssParseError { + /// A hard error in the CSS syntax + ParseError(::simplecss::Error), + /// Braces are not balanced properly + UnclosedBlock, + /// Invalid syntax, such as `#div { #div: "my-value" }` + MalformedCss, +} + +/// Rule that applies to some "path" in the CSS, i.e. +/// `div#myid.myclass -> ("justify-content", "center")` +/// +/// The CSS rule is currently not cascaded, use `Css::new_from_string()` +/// to do the cascading. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CssRule { + /// `div` (`*` by default) + pub html_type: String, + /// `#myid` (`None` by default) + pub id: Option, + /// `.myclass .myotherclass` (vec![] by default) + pub classes: Vec, + /// `("justify-content", "center")` + pub declaration: (String, String), +} + +impl CssRule { + pub fn needs_relayout(&self) -> bool { + RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) + } +} + +impl Css { + + /// Creates an empty set of CSS rules + pub fn empty() -> Self { + Self { + rules: Vec::new(), + is_dirty: false, + needs_relayout: false, + } + } + + /// Parses a CSS string (single-threaded) and returns the parsed rules + pub fn new_from_string(css_string: &str) -> Result { + use simplecss::{Tokenizer, Token}; + use std::collections::HashSet; + + let mut tokenizer = Tokenizer::new(css_string); + + let mut block_nesting = 0_usize; + let mut css_rules = Vec::::new(); + + // TODO: For now, rules may not be nested, otherwise, this won't work + // TODO: This could be more efficient. We don't even need to clone the + // strings, but this is just a quick-n-dirty CSS parser + // This will also use up a lot of memory, since the strings get duplicated + + let mut parser_in_block = false; + let mut current_type = "*"; + let mut current_id = None; + let mut current_classes = HashSet::<&str>::new(); + + 'css_parse_loop: loop { + let tokenize_result = tokenizer.parse_next(); + match tokenize_result { + Ok(token) => { + match token { + Token::EndOfStream => { + break 'css_parse_loop; + }, + Token::BlockStart => { + parser_in_block = true; + block_nesting += 1; + }, + Token::BlockEnd => { + block_nesting -= 1; + parser_in_block = false; + current_type = "*"; + current_id = None; + current_classes = HashSet::<&str>::new(); + }, + Token::TypeSelector(div_type) => { + if parser_in_block { + return Err(CssParseError::MalformedCss); + } + current_type = div_type; + }, + Token::IdSelector(id) => { + if parser_in_block { + return Err(CssParseError::MalformedCss); + } + current_id = Some(id.to_string()); + } + Token::ClassSelector(class) => { + if parser_in_block { + return Err(CssParseError::MalformedCss); + } + current_classes.insert(class); + } + Token::Declaration(key, val) => { + if !parser_in_block { + return Err(CssParseError::MalformedCss); + } + let mut css_rule = CssRule { + html_type: current_type.to_string(), + id: current_id.clone(), + classes: current_classes.iter().map(|e| e.to_string()).collect::>(), + declaration: (key.to_string(), val.to_string()), + }; + // IMPORTANT! + css_rule.classes.sort(); + css_rules.push(css_rule); + }, + _ => { } + } + }, + Err(e) => { + return Err(CssParseError::ParseError(e)); + } + } + } + + // non-even number of blocks + if block_nesting != 0 { + return Err(CssParseError::UnclosedBlock); + } + + Ok(Self { + rules: css_rules, + // force repaint for the first frame + is_dirty: true, + // force re-layout for the first frame + needs_relayout: true, + }) + } + + /// Adds a CSS rule + pub fn add_rule(&mut self, css_rule: CssRule) { + self.needs_relayout = css_rule.needs_relayout(); + self.rules.push(css_rule); + self.is_dirty = true; + } + + /// Removes a rule from the current stylesheet + pub fn remove_rule(&mut self, css_rule: &CssRule) { + if let Some(pos) = self.rules.iter().position(|x| *x == *css_rule) { + self.needs_relayout = css_rule.needs_relayout(); + self.rules.remove(pos); + self.is_dirty = true; + } + } + + /// Returns the native style for the OS + #[cfg(target_os="windows")] + pub fn native() -> Self { + Self::new_from_string(NATIVE_CSS_WINDOWS).unwrap() + } + + /// Returns the native style for the OS + #[cfg(target_os="linux")] + pub fn native() -> Self { + Self::new_from_string(NATIVE_CSS_LINUX).unwrap() + } + + /// Returns the native style for the OS + #[cfg(target_os="macos")] + pub fn native() -> Self { + Self::new_from_string(NATIVE_CSS_MACOS).unwrap() + } +} + +impl Add for Css { + type Output = Css; + + fn add(mut self, mut other: Css) -> Css { + let needs_relayout = if !other.needs_relayout { + other.rules.iter().any(|r| r.needs_relayout()) + } else { + other.needs_relayout + }; + + self.rules.append(&mut other.rules); + Css { + rules: self.rules, + is_dirty: true, + needs_relayout: needs_relayout, + } + } } \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index 01d6a53bc..6ef17da65 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -9,7 +9,7 @@ use euclid::{TypedRotation2D, Angle, TypedPoint2D}; pub(crate) const EM_HEIGHT: f32 = 16.0; -// In case no font size is specified for a node, this will be subsituted as the +// In case no font size is specified for a node, this will be subsituted as the // default font size pub(crate) const DEFAULT_FONT_SIZE: FontSize = FontSize(PixelValue { metric: CssMetric::Px, @@ -52,7 +52,7 @@ macro_rules! typed_pixel_value_parser { ) } -/// Simple "invalid value" error, used for +/// Simple "invalid value" error, used for #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct InvalidValueErr<'a>(pub &'a str); @@ -993,7 +993,7 @@ pub fn parse_css_background<'a>(input: &'a str) } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct CssImageId<'a>(pub(crate) &'a str); +pub struct CssImageId<'a>(pub(crate) &'a str); impl<'a> From> for CssImageId<'a> { fn from(input: QuoteStripped<'a>) -> Self { @@ -1002,7 +1002,7 @@ impl<'a> From> for CssImageId<'a> { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub(crate) struct QuoteStripped<'a>(pub(crate) &'a str); +pub(crate) struct QuoteStripped<'a>(pub(crate) &'a str); fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError<'a>> { Ok(strip_quotes(input)?.into()) @@ -1011,7 +1011,7 @@ fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError< /// Strip quotes from an input, given that both quotes use either `"` or `'`, but not both. /// /// Example: -/// +/// /// `"Helvetica"` - valid /// `'Helvetica'` - valid /// `'Helvetica"` - invalid @@ -1210,13 +1210,13 @@ pub enum LayoutWrap { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutJustifyContent { - /// Default value. Items are positioned at the beginning of the container + /// Default value. Items are positioned at the beginning of the container Start, - /// Items are positioned at the end of the container + /// Items are positioned at the end of the container End, - /// Items are positioned at the center of the container + /// Items are positioned at the center of the container Center, - /// Items are positioned with space between the lines + /// Items are positioned with space between the lines SpaceBetween, /// Items are positioned with space before, between, and after the lines SpaceAround, @@ -1224,29 +1224,29 @@ pub enum LayoutJustifyContent { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutAlignItems { - /// Items are stretched to fit the container + /// Items are stretched to fit the container Stretch, - /// Items are positioned at the center of the container + /// Items are positioned at the center of the container Center, - /// Items are positioned at the beginning of the container + /// Items are positioned at the beginning of the container Start, - /// Items are positioned at the end of the container + /// Items are positioned at the end of the container End, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutAlignContent { - /// Default value. Lines stretch to take up the remaining space + /// Default value. Lines stretch to take up the remaining space Stretch, /// Lines are packed toward the center of the flex container Center, - /// Lines are packed toward the start of the flex container + /// Lines are packed toward the start of the flex container Start, - /// Lines are packed toward the end of the flex container + /// Lines are packed toward the end of the flex container End, - /// Lines are evenly distributed in the flex container + /// Lines are evenly distributed in the flex container SpaceBetween, - /// Lines are evenly distributed in the flex container, with half-size spaces on either end + /// Lines are evenly distributed in the flex container, with half-size spaces on either end SpaceAround, } @@ -1365,7 +1365,7 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result(input: &'a str) -> Result { #[derive(Debug)] pub(crate) struct DisplayRectangle<'a> { - /// `Some(id)` if this rectangle has a callback attached to it - /// Note: this is not the same as the `NodeId`! + /// `Some(id)` if this rectangle has a callback attached to it + /// Note: this is not the same as the `NodeId`! /// These two are completely seperate numbers! pub tag: Option, /// The original styled node @@ -44,7 +44,7 @@ pub(crate) struct DisplayRectangle<'a> { /// It is not very efficient to re-create constraints on every call, the difference /// in performance can be huge. Without re-creating constraints, solving can take 0.3 ms, /// with re-creation it can take up to 9 ms. So the goal is to not re-create constraints -/// if their contents haven't changed. +/// if their contents haven't changed. #[derive(Default)] pub(crate) struct SolvedLayout { // List of previously solved constraints @@ -74,7 +74,7 @@ impl<'a> DisplayRectangle<'a> { impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { /// NOTE: This function assumes that the UiDescription has an initialized arena - /// + /// /// This only looks at the user-facing styles of the `UiDescription`, not the actual /// layout. The layout is done only in the `into_display_list_builder` step. pub fn new_from_ui_description(ui_description: &'a UiDescription) -> Self { @@ -98,18 +98,18 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { /// Looks if any new images need to be uploaded and stores the in the image resources fn update_resources( - api: &RenderApi, - app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) { Self::update_image_resources(api, app_resources, resource_updates); Self::update_font_resources(api, app_resources, resource_updates); } fn update_image_resources( - api: &RenderApi, - app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) { use images::{ImageState, ImageInfo}; @@ -142,20 +142,20 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { for (resource_key, (data, descriptor)) in updated_images.into_iter() { let key = api.generate_image_key(); resource_updates.add_image(key, descriptor, data, None); - *app_resources.images.get_mut(&resource_key).unwrap() = - ImageState::Uploaded(ImageInfo { - key: key, - descriptor: descriptor + *app_resources.images.get_mut(&resource_key).unwrap() = + ImageState::Uploaded(ImageInfo { + key: key, + descriptor: descriptor }); } } - // almost the same as update_image_resources, but fonts + // almost the same as update_image_resources, but fonts // have two HashMaps that need to be updated fn update_font_resources( - api: &RenderApi, - app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + api: &RenderApi, + app_resources: &mut AppResources, + resource_updates: &mut ResourceUpdates) { use font::FontState; @@ -178,10 +178,10 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } } - // Delete the complete font. Maybe a more granular option to + // Delete the complete font. Maybe a more granular option to // keep the font data in memory should be added later for (resource_key, to_delete_instances) in to_delete_fonts.into_iter() { - if let Some((font_key, font_instance_keys)) = to_delete_instances { + if let Some((font_key, font_instance_keys)) = to_delete_instances { for instance in font_instance_keys { resource_updates.delete_font_instance(instance); } @@ -200,15 +200,15 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } pub fn into_display_list_builder( - &self, - pipeline_id: PipelineId, - ui_solver: &mut UiSolver, + &self, + pipeline_id: PipelineId, + ui_solver: &mut UiSolver, css: &mut Css, app_resources: &mut AppResources, render_api: &RenderApi, mut has_window_size_changed: bool) -> Option - { + { let mut changeset = None; if let Some(root) = self.ui_descr.ui_descr_root { let local_changeset = ui_solver.dom_tree_cache.update(root, &*(self.ui_descr.ui_descr_arena.borrow())); @@ -295,9 +295,9 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // Push the "outset" box shadow, before the clip is active push_box_shadow( - &mut builder, - &rect.style, - &bounds, + &mut builder, + &rect.style, + &bounds, &full_screen_rect, BoxShadowClipMode::Outset); @@ -316,38 +316,38 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } push_rect( - &info, - &mut builder, + &info, + &mut builder, &rect.style); push_background( - &info, - &bounds, - &mut builder, - &rect.style, + &info, + &bounds, + &mut builder, + &rect.style, &app_resources); // push the inset shadow (if any) - push_box_shadow(&mut builder, - &rect.style, - &bounds, + push_box_shadow(&mut builder, + &rect.style, + &bounds, &full_screen_rect, BoxShadowClipMode::Inset); push_border( - &info, - &mut builder, + &info, + &mut builder, &rect.style); - + push_text( - &info, - &self, - *rect_idx, - &mut builder, - &rect.style, - app_resources, - &render_api, - &bounds, + &info, + &self, + *rect_idx, + &mut builder, + &rect.style, + app_resources, + &render_api, + &bounds, &mut resource_updates); // Pop clip @@ -374,15 +374,15 @@ fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, #[inline] fn push_text( - info: &PrimitiveInfo, - display_list: &DisplayList, - rect_idx: NodeId, - builder: &mut DisplayListBuilder, + info: &PrimitiveInfo, + display_list: &DisplayList, + rect_idx: NodeId, + builder: &mut DisplayListBuilder, style: &RectStyle, app_resources: &mut AppResources, render_api: &RenderApi, - bounds: &TypedRect, - resource_updates: &mut ResourceUpdates) + bounds: &TypedRect, + resource_updates: &mut ResourceUpdates) { use dom::NodeType::*; use euclid::{TypedPoint2D, Length}; @@ -391,9 +391,9 @@ fn push_text( // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it let arena = display_list.ui_descr.ui_descr_arena.borrow(); - + let text = match arena[rect_idx].data.node_type { - Div => return, + Div => return, Label { ref text } => { text }, @@ -410,19 +410,19 @@ fn push_text( return; } - let font_family = match style.font_family { + let font_family = match style.font_family { Some(ref ff) => ff, None => return, }; - // TODO: border + // TODO: border let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size = Length::new(font_size.0.to_pixels()); let font_size_app_units = (font_size.0 as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); let font_size_app_units = Au(font_size_app_units as i32); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); - + let font_instance_key = match font_result { Some(f) => f, None => return, @@ -433,7 +433,7 @@ fn push_text( let font_size_adjustment_hack = { let font = &app_resources.font_data[font_id].0; let v_metrics = font.v_metrics_unscaled(); - let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / + let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / font.units_per_em() as f32; 1.0 + ((v_scale_factor - 1.0) * 2.0) }; @@ -445,10 +445,10 @@ fn push_text( text, font, font_size * font_size_adjustment_hack, alignment, overflow_behaviour, bounds); // TODO: webrender doesn't respect the DPI of the monitor its on: - // + // // See: https://github.com/servo/webrender/pull/2597 // and: https://github.com/servo/webrender/issues/2596 - + let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); let flags = FontInstanceFlags::SUBPIXEL_BGR; let options = GlyphOptions { @@ -459,18 +459,18 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); } -/// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the +/// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the /// shadow will not show up. /// /// To prevent a shadow from being pushed twice, you have to annotate the clip -/// mode for this - outset or inset. +/// mode for this - outset or inset. #[inline] fn push_box_shadow( - builder: &mut DisplayListBuilder, - style: &RectStyle, - bounds: &TypedRect, + builder: &mut DisplayListBuilder, + style: &RectStyle, + bounds: &TypedRect, full_screen_rect: &TypedRect, - shadow_type: BoxShadowClipMode) + shadow_type: BoxShadowClipMode) { let pre_shadow = match style.box_shadow { Some(ref ps) => ps, @@ -486,10 +486,10 @@ fn push_box_shadow( let clip_rect = if pre_shadow.clip_mode == BoxShadowClipMode::Inset { // inset shadows do not work like outset shadows - // for inset shadows, you have to push a clip ID first, so that they are + // for inset shadows, you have to push a clip ID first, so that they are // clipped to the bounds -we trust that the calling function knows to do this *bounds - } else { + } else { // calculate the maximum extent of the outset shadow let mut clip_rect = *bounds; @@ -513,11 +513,11 @@ fn push_box_shadow( #[inline] fn push_background( - info: &PrimitiveInfo, - bounds: &TypedRect, - builder: &mut DisplayListBuilder, + info: &PrimitiveInfo, + bounds: &TypedRect, + builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &AppResources) + app_resources: &AppResources) { let background = match style.background { Some(ref bg) => bg, @@ -568,9 +568,9 @@ fn push_background( #[inline] fn push_border( - info: &PrimitiveInfo, - builder: &mut DisplayListBuilder, - style: &RectStyle) + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + style: &RectStyle) { if let Some((border_widths, mut border_details)) = style.border { if let Some(border_radius) = style.border_radius { @@ -586,19 +586,19 @@ use css_parser; #[inline] fn push_font( - font_id: &css_parser::Font, - font_size_app_units: Au, - resource_updates: &mut ResourceUpdates, - app_resources: &mut AppResources, - render_api: &RenderApi) --> Option + font_id: &css_parser::Font, + font_size_app_units: Au, + resource_updates: &mut ResourceUpdates, + app_resources: &mut AppResources, + render_api: &RenderApi) +-> Option { use font::FontState; if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { println!("warning: too big or too small font size"); return None; - } + } let &(ref font, ref font_state) = match app_resources.font_data.get(font_id) { Some(f) => f, @@ -639,20 +639,20 @@ use std::fmt::Debug; /// Internal helper function - gets a key from the constraint list and passes it through /// the parse_func - if an error occurs, then the error gets printed fn parse<'a, T, E: Debug>( - constraint_list: &'a CssConstraintList, - key: &'static str, - parse_func: fn(&'a str) -> Result) --> Option + constraint_list: &'a CssConstraintList, + key: &'static str, + parse_func: fn(&'a str) -> Result) +-> Option { #[inline(always)] fn print_error_debug(err: &E, key: &'static str) { eprintln!("ERROR - invalid {:?}: {:?}", err, key); } - constraint_list.list.get(key).and_then(|w| parse_func(w).map_err(|e| { + constraint_list.list.get(key).and_then(|w| parse_func(w).map_err(|e| { #[cfg(debug_assertions)] print_error_debug(&e, key); - e + e }).ok()) } @@ -670,22 +670,22 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); rect.style.text_overflow = parse(constraint_list, "overflow", parse_text_overflow); rect.style.text_align = parse(constraint_list, "text-align", parse_text_align); - + if let Some(box_shadow_opt) = parse(constraint_list, "box-shadow", parse_css_box_shadow) { rect.style.box_shadow = box_shadow_opt; } } /// Populate and parse the CSS layout properties -fn parse_css_layout_properties(rect: &mut DisplayRectangle) +fn parse_css_layout_properties(rect: &mut DisplayRectangle) { let constraint_list = &rect.styled_node.css_constraints; - + rect.layout.width = parse(constraint_list, "width", parse_layout_width); rect.layout.height = parse(constraint_list, "height", parse_layout_height); rect.layout.min_width = parse(constraint_list, "min-width", parse_layout_min_width); rect.layout.min_height = parse(constraint_list, "min-height", parse_layout_min_height); - + rect.layout.wrap = parse(constraint_list, "flex-wrap", parse_layout_wrap); rect.layout.direction = parse(constraint_list, "flex-direction", parse_layout_direction); rect.layout.justify_content = parse(constraint_list, "justify-content", parse_layout_justify_content); @@ -695,9 +695,9 @@ fn parse_css_layout_properties(rect: &mut DisplayRectangle) // Returns the constraints for one rectangle fn create_layout_constraints( - rect: &DisplayRectangle, + rect: &DisplayRectangle, rect_id: NodeId, - arena: &Arena>, + arena: &Arena>, window_dimensions: &WindowDimensions) -> Vec where T: LayoutScreen @@ -721,11 +721,11 @@ fn css_constraints_to_cassowary_constraints(rect: &DisplayRect, css: &Vec { - constraint.build(&rect, strength.0) + Size((constraint, strength)) => { + constraint.build(&rect, strength.0) } - Padding((constraint, strength, padding)) => { - constraint.build(&rect, strength.0, padding.0) + Padding((constraint, strength, padding)) => { + constraint.build(&rect, strength.0, padding.0) } } ).collect() diff --git a/src/dom.rs b/src/dom.rs index 8de38e06c..630ce36a8 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,465 +1,465 @@ -use app_state::AppState; -use traits::LayoutScreen; -use std::collections::BTreeMap; -use id_tree::{NodeId, Arena}; -use std::sync::{Arc, Mutex}; -use std::fmt; -use std::rc::Rc; -use std::cell::RefCell; -use std::hash::{Hash, Hasher}; -use webrender::api::ColorU; - -/// This is only accessed from the main thread, so it's safe to use -pub(crate) static mut NODE_ID: u64 = 0; -pub(crate) static mut CALLBACK_ID: u64 = 0; - -/// A callback function has to return if the screen should -/// be updated after the function has run.PartialEq -/// -/// This is necessary for updating the screen only if it is absolutely necessary. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum UpdateScreen { - /// Redraw the screen - Redraw, - /// Don't redraw the screen - DontRedraw, -} - -/// Stores a function pointer that is executed when the given UI element is hit -/// -/// Must return an `UpdateScreen` that denotes if the screen should be redrawn. -/// The CSS is not affected by this, so if you push to the windows' CSS inside the -/// function, the screen will not be automatically redrawn, unless you return an -/// `UpdateScreen::Redraw` from the function -pub enum Callback { - /// One-off function (for ex. exporting a file) - /// - /// This is best for actions where you don't care if or when they complete. - /// Because you accept a Mutex, you can create a background thread - /// (azul won't create this for you) - Async(fn(Arc>>) -> UpdateScreen), - /// Same as the `FnOnceNonBlocking`, but it blocks the current - /// thread and does not require the type to be `Send`. - Sync(fn(&mut AppState) -> UpdateScreen), -} - -impl fmt::Debug for Callback { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Callback::*; - match *self { - Async(func) => write!(f, "Callback::Async @ {:?}", func as usize), - Sync(func) => write!(f, "Callback::Sync @ {:?}", func as usize), - } - } -} - -impl Clone for Callback -{ - fn clone(&self) -> Self { - match *self { - Callback::Async(ref f) => Callback::Async(f.clone()), - Callback::Sync(ref f) => Callback::Sync(f.clone()), - } - } -} - -/// As a hashing function, we use the function pointer casted to a usize -/// as a unique ID for the function. This way, we can hash and compare DOM nodes -/// (to create diffs between two states). Comparing usizes is more efficient -/// than re-creating the whole DOM and serves as a caching mechanism. -impl Hash for Callback { - fn hash(&self, state: &mut H) where H: Hasher { - use self::Callback::*; - match *self { - Async(f) => { state.write_usize(f as usize); } - Sync(f) => { state.write_usize(f as usize); } - } - } -} - -/// Basically compares the function pointers and types for equality -impl PartialEq for Callback { - fn eq(&self, rhs: &Self) -> bool { - use self::Callback::*; - if let (Async(self_f), Async(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } else if let (Sync(self_f), Sync(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } - false - } -} - -impl Eq for Callback { } - -impl Copy for Callback { } - -/// List of allowed DOM node types that are supported by `azul`. -/// -/// All node types are purely convenience functions around `Div`, -/// `Image` and `Label`. For example a `Ul` is simply a convenience -/// wrapper around a repeated (`Div` + `Label`) clone where the first -/// `Div` is shaped like a circle (for `Ul`). -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum NodeType { - /// Regular div - Div, - /// Image: The actual contents of the image are determined by the CSS - Image, - /// A label that can be (optionally) be selectable with the mouse - Label { - /// Text of the label - text: String, - }, - /// Button - Button { - /// The text on the button - label: String, - }, - /// Unordered list - Ul, - /// Ordered list - Ol, - /// List item. Only valid if the parent is `NodeType::Ol` or `NodeType::Ul`. - Li, - /// This is more or less like a `GroupBox` in Visual Basic, draws a border - Form { - /// The text of the label - text: Option, - }, - /// Single-line text input - TextInput { - content: String, - placeholder: Option - }, - /// Multi line text input - TextEdit { - content: String, - placeholder: Option, - }, - /// A register-like tab - Tab { - label: String, - }, - /// Checkbox - Checkbox { - /// active - state: CheckboxState, - }, - /// Dropdown item - Dropdown { - items: Vec, - }, - /// Small (default yellow) tooltip for help - ToolTip { - title: String, - content: String, - }, - /// Password input, like the TextInput, but the items are rendered as dots - /// (if `use_dots` is active) - Password { - content: String, - placeholder: Option, - use_dots: bool, - }, -} - -/// State of a checkbox (disabled, checked, etc.) -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub enum CheckboxState { - /// `[■]` - Active, - /// `[✔]` - Checked, - /// Greyed out checkbox - Disabled { - /// Should the checkbox fire on a mouseover / mouseup, etc. event - /// - /// This can be useful for showing warnings / tooltips / help messages - /// as to why this checkbox is disabled - fire_on_click: bool, - }, - /// `[ ]` - Unchecked -} - -impl NodeType { - - /// Get the CSS / HTML identifier "p", "ul", "li", etc. - /// - /// Full list of the types you can use in CSS: - /// - /// ```ignore - /// Div => "div" - /// Image => "img" - /// Button => "button" - /// Ul => "ul" - /// Ol => "ol" - /// Li => "li" - /// Label => "label" - /// Form => "form" - /// TextInput => "text-input" - /// TextEdit => "text-edit" - /// Tab => "tab" - /// Checkbox => "checkbox" - /// Color => "color" - /// Drowdown => "dropdown" - /// ToolTip => "tooltip" - /// Password => "password" - /// ``` - pub fn get_css_identifier(&self) -> &'static str { - use self::NodeType::*; - match *self { - Div => "div", - Image => "img", - Label { .. } => "label", - Button { .. } => "button", - Ul => "ul", - Ol => "ol", - Li => "li", - Form { .. } => "form", - TextInput { .. } => "text-input", - TextEdit { .. } => "text-edit", - Tab { .. } => "tab", - Checkbox { .. } => "checkbox", - Dropdown { .. } => "dropdown", - ToolTip { .. } => "tooltip", - Password { .. } => "password", - } - } -} - -/// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum On { - /// Mouse cursor is hovering over the element - MouseOver, - /// Mouse cursor has is over element and is pressed - /// (not good for "click" events - use `MouseUp` instead) - MouseDown, - /// Mouse button has been released while cursor was over the element - MouseUp, - /// Mouse cursor has entered the element - MouseEnter, - /// Mouse cursor has left the element - MouseLeave, -} - -#[derive(PartialEq, Eq)] -pub(crate) struct NodeData { - /// `div` - pub node_type: NodeType, - /// `#main` - pub id: Option, - /// `.myclass .otherclass` - pub classes: Vec, - /// `onclick` -> `my_button_click_handler` - pub events: CallbackList, - /// Tag for hit-testing - pub tag: Option, -} - -impl Hash for NodeData { - fn hash(&self, state: &mut H) { - self.node_type.hash(state); - self.id.hash(state); - for class in &self.classes { - class.hash(state); - } - self.events.hash(state); - } -} - -use cache::DomHash; - -impl NodeData { - pub fn calculate_node_data_hash(&self) -> DomHash { - use std::hash::Hash; - use twox_hash::XxHash; - let mut hasher = XxHash::default(); - self.hash(&mut hasher); - DomHash(hasher.finish()) - } -} - -impl Clone for NodeData { - fn clone(&self) -> Self { - Self { - node_type: self.node_type.clone(), - id: self.id.clone(), - classes: self.classes.clone(), - events: self.events.special_clone(), - tag: self.tag.clone(), - } - } -} - -impl fmt::Debug for NodeData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "NodeData {{ - node_type: {:?}, - id: {:?}, - classes: {:?}, - events: {:?}, - tag: {:?} -}}", - self.node_type, self.id, self.classes, self.events, self.tag) - } -} - -impl CallbackList { - fn special_clone(&self) -> Self { - Self { - callbacks: self.callbacks.clone(), - } - } -} - -impl NodeData { - /// Creates a new NodeData - pub fn new(node_type: NodeType) -> Self { - Self { - node_type: node_type, - id: None, - classes: Vec::new(), - events: CallbackList::::new(), - tag: None, - } - } - - /// Since `#[derive(Clone)]` requires `T: Clone`, we currently - /// have to make our own version - fn special_clone(&self) -> Self { - Self { - node_type: self.node_type.clone(), - id: self.id.clone(), - classes: self.classes.clone(), - events: self.events.special_clone(), - tag: self.tag.clone(), - } - } -} - -/// The document model, similar to HTML. This is a create-only structure, you don't actually read anything back -#[derive(Clone, PartialEq, Eq)] -pub struct Dom { - pub(crate) arena: Rc>>>, - pub(crate) root: NodeId, - pub(crate) current_root: NodeId, - pub(crate) last: NodeId, -} - -impl fmt::Debug for Dom { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Dom {{ - arena: {:?}, - root: {:?}, - current_root: {:?}, - last: {:?} -}}", - self.arena, - self.root, - self.current_root, - self.last) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub(crate) struct CallbackList { - pub(crate) callbacks: BTreeMap> -} - -impl Hash for CallbackList { - fn hash(&self, state: &mut H) { - for callback in &self.callbacks { - callback.hash(state); - } - } -} - -impl fmt::Debug for CallbackList { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "CallbackList (length: {:?})", self.callbacks.len()) - } -} - -impl CallbackList { - pub fn new() -> Self { - Self { - callbacks: BTreeMap::new(), - } - } -} - -impl Dom { - - /// Creates an empty DOM - #[inline] - pub fn new(node_type: NodeType) -> Self { - let mut arena = Arena::new(); - let root = arena.new_node(NodeData::new(node_type)); - Self { - arena: Rc::new(RefCell::new(arena)), - root: root, - current_root: root, - last: root, - } - } - - /// Adds a child DOM to the current DOM - #[inline] - pub fn add_child(&mut self, child: Self) { - for ch in child.root.children(&*child.arena.borrow()) { - let new_last = (*self.arena.borrow_mut()).new_node((*child.arena.borrow())[ch].data.special_clone()); - self.last.append(new_last, &mut self.arena.borrow_mut()); - self.last = new_last; - } - } - - /// Adds a sibling to the current DOM - #[inline] - pub fn add_sibling(&mut self, sibling: Self) { - for sib in sibling.root.following_siblings(&*sibling.arena.borrow()) { - let sibling_clone = (*sibling.arena.borrow())[sib].data.special_clone(); - let new_sibling = (*self.arena.borrow_mut()).new_node(sibling_clone); - self.current_root.insert_after(new_sibling, &mut self.arena.borrow_mut()); - self.current_root = new_sibling; - } - } - - #[inline] - pub fn id>(&mut self, id: S) { - self.arena.borrow_mut()[self.last].data.id = Some(id.into()); - } - - #[inline] - pub fn class>(&mut self, class: S) { - self.arena.borrow_mut()[self.last].data.classes.push(class.into()); - } - - #[inline] - pub fn event(&mut self, on: On, callback: Callback) { - self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); - self.arena.borrow_mut()[self.last].data.tag = Some(unsafe { NODE_ID }); - unsafe { NODE_ID += 1; }; - } -} - -impl Dom { - - pub(crate) fn collect_callbacks(&self, callback_list: &mut BTreeMap>, nodes_to_callback_id_list: &mut BTreeMap>) { - for item in self.root.traverse(&*self.arena.borrow()) { - let mut cb_id_list = BTreeMap::::new(); - let item = &self.arena.borrow()[item.inner_value()]; - for (on, callback) in item.data.events.callbacks.iter() { - let callback_id = unsafe { CALLBACK_ID }; - unsafe { CALLBACK_ID += 1; } - callback_list.insert(callback_id, *callback); - cb_id_list.insert(*on, callback_id); - } - if let Some(tag) = item.data.tag { - nodes_to_callback_id_list.insert(tag, cb_id_list); - } - } - } +use app_state::AppState; +use traits::LayoutScreen; +use std::collections::BTreeMap; +use id_tree::{NodeId, Arena}; +use std::sync::{Arc, Mutex}; +use std::fmt; +use std::rc::Rc; +use std::cell::RefCell; +use std::hash::{Hash, Hasher}; +use webrender::api::ColorU; + +/// This is only accessed from the main thread, so it's safe to use +pub(crate) static mut NODE_ID: u64 = 0; +pub(crate) static mut CALLBACK_ID: u64 = 0; + +/// A callback function has to return if the screen should +/// be updated after the function has run.PartialEq +/// +/// This is necessary for updating the screen only if it is absolutely necessary. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum UpdateScreen { + /// Redraw the screen + Redraw, + /// Don't redraw the screen + DontRedraw, +} + +/// Stores a function pointer that is executed when the given UI element is hit +/// +/// Must return an `UpdateScreen` that denotes if the screen should be redrawn. +/// The CSS is not affected by this, so if you push to the windows' CSS inside the +/// function, the screen will not be automatically redrawn, unless you return an +/// `UpdateScreen::Redraw` from the function +pub enum Callback { + /// One-off function (for ex. exporting a file) + /// + /// This is best for actions where you don't care if or when they complete. + /// Because you accept a Mutex, you can create a background thread + /// (azul won't create this for you) + Async(fn(Arc>>) -> UpdateScreen), + /// Same as the `FnOnceNonBlocking`, but it blocks the current + /// thread and does not require the type to be `Send`. + Sync(fn(&mut AppState) -> UpdateScreen), +} + +impl fmt::Debug for Callback { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Callback::*; + match *self { + Async(func) => write!(f, "Callback::Async @ {:?}", func as usize), + Sync(func) => write!(f, "Callback::Sync @ {:?}", func as usize), + } + } +} + +impl Clone for Callback +{ + fn clone(&self) -> Self { + match *self { + Callback::Async(ref f) => Callback::Async(f.clone()), + Callback::Sync(ref f) => Callback::Sync(f.clone()), + } + } +} + +/// As a hashing function, we use the function pointer casted to a usize +/// as a unique ID for the function. This way, we can hash and compare DOM nodes +/// (to create diffs between two states). Comparing usizes is more efficient +/// than re-creating the whole DOM and serves as a caching mechanism. +impl Hash for Callback { + fn hash(&self, state: &mut H) where H: Hasher { + use self::Callback::*; + match *self { + Async(f) => { state.write_usize(f as usize); } + Sync(f) => { state.write_usize(f as usize); } + } + } +} + +/// Basically compares the function pointers and types for equality +impl PartialEq for Callback { + fn eq(&self, rhs: &Self) -> bool { + use self::Callback::*; + if let (Async(self_f), Async(other_f)) = (*self, *rhs) { + if self_f as usize == other_f as usize { return true; } + } else if let (Sync(self_f), Sync(other_f)) = (*self, *rhs) { + if self_f as usize == other_f as usize { return true; } + } + false + } +} + +impl Eq for Callback { } + +impl Copy for Callback { } + +/// List of allowed DOM node types that are supported by `azul`. +/// +/// All node types are purely convenience functions around `Div`, +/// `Image` and `Label`. For example a `Ul` is simply a convenience +/// wrapper around a repeated (`Div` + `Label`) clone where the first +/// `Div` is shaped like a circle (for `Ul`). +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NodeType { + /// Regular div + Div, + /// Image: The actual contents of the image are determined by the CSS + Image, + /// A label that can be (optionally) be selectable with the mouse + Label { + /// Text of the label + text: String, + }, + /// Button + Button { + /// The text on the button + label: String, + }, + /// Unordered list + Ul, + /// Ordered list + Ol, + /// List item. Only valid if the parent is `NodeType::Ol` or `NodeType::Ul`. + Li, + /// This is more or less like a `GroupBox` in Visual Basic, draws a border + Form { + /// The text of the label + text: Option, + }, + /// Single-line text input + TextInput { + content: String, + placeholder: Option + }, + /// Multi line text input + TextEdit { + content: String, + placeholder: Option, + }, + /// A register-like tab + Tab { + label: String, + }, + /// Checkbox + Checkbox { + /// active + state: CheckboxState, + }, + /// Dropdown item + Dropdown { + items: Vec, + }, + /// Small (default yellow) tooltip for help + ToolTip { + title: String, + content: String, + }, + /// Password input, like the TextInput, but the items are rendered as dots + /// (if `use_dots` is active) + Password { + content: String, + placeholder: Option, + use_dots: bool, + }, +} + +/// State of a checkbox (disabled, checked, etc.) +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum CheckboxState { + /// `[■]` + Active, + /// `[✔]` + Checked, + /// Greyed out checkbox + Disabled { + /// Should the checkbox fire on a mouseover / mouseup, etc. event + /// + /// This can be useful for showing warnings / tooltips / help messages + /// as to why this checkbox is disabled + fire_on_click: bool, + }, + /// `[ ]` + Unchecked +} + +impl NodeType { + + /// Get the CSS / HTML identifier "p", "ul", "li", etc. + /// + /// Full list of the types you can use in CSS: + /// + /// ```ignore + /// Div => "div" + /// Image => "img" + /// Button => "button" + /// Ul => "ul" + /// Ol => "ol" + /// Li => "li" + /// Label => "label" + /// Form => "form" + /// TextInput => "text-input" + /// TextEdit => "text-edit" + /// Tab => "tab" + /// Checkbox => "checkbox" + /// Color => "color" + /// Drowdown => "dropdown" + /// ToolTip => "tooltip" + /// Password => "password" + /// ``` + pub fn get_css_identifier(&self) -> &'static str { + use self::NodeType::*; + match *self { + Div => "div", + Image => "img", + Label { .. } => "label", + Button { .. } => "button", + Ul => "ul", + Ol => "ol", + Li => "li", + Form { .. } => "form", + TextInput { .. } => "text-input", + TextEdit { .. } => "text-edit", + Tab { .. } => "tab", + Checkbox { .. } => "checkbox", + Dropdown { .. } => "dropdown", + ToolTip { .. } => "tooltip", + Password { .. } => "password", + } + } +} + +/// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum On { + /// Mouse cursor is hovering over the element + MouseOver, + /// Mouse cursor has is over element and is pressed + /// (not good for "click" events - use `MouseUp` instead) + MouseDown, + /// Mouse button has been released while cursor was over the element + MouseUp, + /// Mouse cursor has entered the element + MouseEnter, + /// Mouse cursor has left the element + MouseLeave, +} + +#[derive(PartialEq, Eq)] +pub(crate) struct NodeData { + /// `div` + pub node_type: NodeType, + /// `#main` + pub id: Option, + /// `.myclass .otherclass` + pub classes: Vec, + /// `onclick` -> `my_button_click_handler` + pub events: CallbackList, + /// Tag for hit-testing + pub tag: Option, +} + +impl Hash for NodeData { + fn hash(&self, state: &mut H) { + self.node_type.hash(state); + self.id.hash(state); + for class in &self.classes { + class.hash(state); + } + self.events.hash(state); + } +} + +use cache::DomHash; + +impl NodeData { + pub fn calculate_node_data_hash(&self) -> DomHash { + use std::hash::Hash; + use twox_hash::XxHash; + let mut hasher = XxHash::default(); + self.hash(&mut hasher); + DomHash(hasher.finish()) + } +} + +impl Clone for NodeData { + fn clone(&self) -> Self { + Self { + node_type: self.node_type.clone(), + id: self.id.clone(), + classes: self.classes.clone(), + events: self.events.special_clone(), + tag: self.tag.clone(), + } + } +} + +impl fmt::Debug for NodeData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "NodeData {{ + node_type: {:?}, + id: {:?}, + classes: {:?}, + events: {:?}, + tag: {:?} +}}", + self.node_type, self.id, self.classes, self.events, self.tag) + } +} + +impl CallbackList { + fn special_clone(&self) -> Self { + Self { + callbacks: self.callbacks.clone(), + } + } +} + +impl NodeData { + /// Creates a new NodeData + pub fn new(node_type: NodeType) -> Self { + Self { + node_type: node_type, + id: None, + classes: Vec::new(), + events: CallbackList::::new(), + tag: None, + } + } + + /// Since `#[derive(Clone)]` requires `T: Clone`, we currently + /// have to make our own version + fn special_clone(&self) -> Self { + Self { + node_type: self.node_type.clone(), + id: self.id.clone(), + classes: self.classes.clone(), + events: self.events.special_clone(), + tag: self.tag.clone(), + } + } +} + +/// The document model, similar to HTML. This is a create-only structure, you don't actually read anything back +#[derive(Clone, PartialEq, Eq)] +pub struct Dom { + pub(crate) arena: Rc>>>, + pub(crate) root: NodeId, + pub(crate) current_root: NodeId, + pub(crate) last: NodeId, +} + +impl fmt::Debug for Dom { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Dom {{ + arena: {:?}, + root: {:?}, + current_root: {:?}, + last: {:?} +}}", + self.arena, + self.root, + self.current_root, + self.last) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct CallbackList { + pub(crate) callbacks: BTreeMap> +} + +impl Hash for CallbackList { + fn hash(&self, state: &mut H) { + for callback in &self.callbacks { + callback.hash(state); + } + } +} + +impl fmt::Debug for CallbackList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CallbackList (length: {:?})", self.callbacks.len()) + } +} + +impl CallbackList { + pub fn new() -> Self { + Self { + callbacks: BTreeMap::new(), + } + } +} + +impl Dom { + + /// Creates an empty DOM + #[inline] + pub fn new(node_type: NodeType) -> Self { + let mut arena = Arena::new(); + let root = arena.new_node(NodeData::new(node_type)); + Self { + arena: Rc::new(RefCell::new(arena)), + root: root, + current_root: root, + last: root, + } + } + + /// Adds a child DOM to the current DOM + #[inline] + pub fn add_child(&mut self, child: Self) { + for ch in child.root.children(&*child.arena.borrow()) { + let new_last = (*self.arena.borrow_mut()).new_node((*child.arena.borrow())[ch].data.special_clone()); + self.last.append(new_last, &mut self.arena.borrow_mut()); + self.last = new_last; + } + } + + /// Adds a sibling to the current DOM + #[inline] + pub fn add_sibling(&mut self, sibling: Self) { + for sib in sibling.root.following_siblings(&*sibling.arena.borrow()) { + let sibling_clone = (*sibling.arena.borrow())[sib].data.special_clone(); + let new_sibling = (*self.arena.borrow_mut()).new_node(sibling_clone); + self.current_root.insert_after(new_sibling, &mut self.arena.borrow_mut()); + self.current_root = new_sibling; + } + } + + #[inline] + pub fn id>(&mut self, id: S) { + self.arena.borrow_mut()[self.last].data.id = Some(id.into()); + } + + #[inline] + pub fn class>(&mut self, class: S) { + self.arena.borrow_mut()[self.last].data.classes.push(class.into()); + } + + #[inline] + pub fn event(&mut self, on: On, callback: Callback) { + self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); + self.arena.borrow_mut()[self.last].data.tag = Some(unsafe { NODE_ID }); + unsafe { NODE_ID += 1; }; + } +} + +impl Dom { + + pub(crate) fn collect_callbacks(&self, callback_list: &mut BTreeMap>, nodes_to_callback_id_list: &mut BTreeMap>) { + for item in self.root.traverse(&*self.arena.borrow()) { + let mut cb_id_list = BTreeMap::::new(); + let item = &self.arena.borrow()[item.inner_value()]; + for (on, callback) in item.data.events.callbacks.iter() { + let callback_id = unsafe { CALLBACK_ID }; + unsafe { CALLBACK_ID += 1; } + callback_list.insert(callback_id, *callback); + cb_id_list.insert(*on, callback_id); + } + if let Some(tag) = item.data.tag { + nodes_to_callback_id_list.insert(tag, cb_id_list); + } + } + } } \ No newline at end of file diff --git a/src/font.rs b/src/font.rs index 6386444c6..8ee34724d 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,42 +1,42 @@ -//! Module for loading and handling fonts -use webrender::api::FontKey; -use rusttype::{Font, FontCollection}; -use rusttype::Error as RusttypeError; - -#[derive(Debug, Clone)] -pub(crate) enum FontState { - // Font is available for the renderer - Uploaded(FontKey), - // Raw bytes for the font, to be uploaded in the next - // draw call (for webrenders add_raw_font function) - ReadyForUpload(Vec), - /// Font that is about to be deleted - /// We need both the ID (to delete the bytes of the font) - /// as well as the FontKey to delete all the font instances - AboutToBeDeleted(Option), -} - -#[derive(Debug)] -pub enum FontError { - /// Font failed to upload to the GPU - UploadError, - /// - InvalidFormat, - /// Rusttype failed to parse the font - ParseError(RusttypeError), - /// IO error - IoError(::std::io::Error), -} - -impl From for FontError { - fn from(e: RusttypeError) -> Self { - FontError::ParseError(e) - } -} - -/// Read font data to get font information, v_metrics, glyph info etc. -pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontError> { - let collection = FontCollection::from_bytes(data)?; - let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); - Ok(font) +//! Module for loading and handling fonts +use webrender::api::FontKey; +use rusttype::{Font, FontCollection}; +use rusttype::Error as RusttypeError; + +#[derive(Debug, Clone)] +pub(crate) enum FontState { + // Font is available for the renderer + Uploaded(FontKey), + // Raw bytes for the font, to be uploaded in the next + // draw call (for webrenders add_raw_font function) + ReadyForUpload(Vec), + /// Font that is about to be deleted + /// We need both the ID (to delete the bytes of the font) + /// as well as the FontKey to delete all the font instances + AboutToBeDeleted(Option), +} + +#[derive(Debug)] +pub enum FontError { + /// Font failed to upload to the GPU + UploadError, + /// + InvalidFormat, + /// Rusttype failed to parse the font + ParseError(RusttypeError), + /// IO error + IoError(::std::io::Error), +} + +impl From for FontError { + fn from(e: RusttypeError) -> Self { + FontError::ParseError(e) + } +} + +/// Read font data to get font information, v_metrics, glyph info etc. +pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontError> { + let collection = FontCollection::from_bytes(data)?; + let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); + Ok(font) } \ No newline at end of file diff --git a/src/id_tree.rs b/src/id_tree.rs index b9903aec0..7cd79b64c 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -1,643 +1,643 @@ -//! ID-based node tree - -use std::mem; -use std::ops::{Index, IndexMut}; -use std::fmt; -use std::hash::{Hasher, Hash}; -use std::collections::BTreeMap; - -/// A node identifier within a particular `Arena`. -#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug, Hash)] -pub struct NodeId { - pub(crate) index: usize, // FIXME: use NonZero to optimize the size of Option -} - -#[derive(Clone, PartialEq)] -pub struct Node { - // Keep these private (with read-only accessors) so that we can keep them consistent. - // E.g. the parent of a node’s child is that node. - parent: Option, - previous_sibling: Option, - next_sibling: Option, - first_child: Option, - last_child: Option, - pub data: T, -} - -// Manual implementation, since `#[derive(Debug)]` requires `T: Debug` -impl fmt::Debug for Node { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Node {{ \ - parent: {:?}, \ - previous_sibling: {:?}, \ - next_sibling: {:?}, \ - first_child: {:?}, \ - last_child: {:?}, \ - data: {:?}, \ - }}", - self.parent, - self.previous_sibling, - self.next_sibling, - self.first_child, - self.last_child, - self.data) - } -} - -#[derive(Debug, Clone)] -pub struct Arena { - pub(crate) nodes: Vec>, -} - -impl PartialEq for Arena { - fn eq(&self, other: &Self) -> bool { - self.nodes == other.nodes - } -} - -impl Eq for Arena { -} - -impl Hash for Arena { - fn hash(&self, state: &mut H) { - for node in &self.nodes { - node.data.hash(state); - } - } -} - -impl Arena { - - /// Transform keeps the relative order of parents / children - /// but transforms an Arena into an Arena, by running the closure on each of the - /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. - pub fn transform(&self, closure: F) -> Arena where F: Fn(&T) -> U { - Arena { - nodes: self.nodes.iter().map(|node| Node { - parent: node.parent, - previous_sibling: node.previous_sibling, - next_sibling: node.next_sibling, - first_child: node.first_child, - last_child: node.last_child, - data: closure(&node.data) - }).collect() - } - } - - pub fn from_nodes(nodes: Vec>) -> Arena { - Self { - nodes: nodes, - } - } - - pub fn new() -> Arena { - Arena { - nodes: Vec::new(), - } - } - - /// Create a new node from its associated data. - pub fn new_node(&mut self, data: T) -> NodeId { - let next_index = self.nodes.len(); - self.nodes.push(Node { - parent: None, - first_child: None, - last_child: None, - previous_sibling: None, - next_sibling: None, - data: data, - }); - NodeId { - index: next_index, - } - } - - // Useful for debugging - returns how many - // nodes there are in the arena - pub fn nodes_len(&self) -> usize { - self.nodes.len() - } - - pub fn is_empty(&self) -> bool { - self.nodes_len() == 0 - } -} - -impl Arena { - #[inline] - pub fn get_all_node_ids(&self) -> BTreeMap { - use std::iter::FromIterator; - BTreeMap::from_iter(self.nodes.iter().enumerate().map(|(i, node)| - (NodeId { index: i }, node.data) - )) - } -} - -trait GetPairMut { - /// Get mutable references to two distinct nodes - /// - /// Panic - /// ----- - /// - /// Panics if the two given IDs are the same. - fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) - -> (&mut T, &mut T); -} - -impl GetPairMut for Vec { - fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) - -> (&mut T, &mut T) { - if a == b { - panic!(same_index_error_message) - } - unsafe { - let self2 = mem::transmute_copy::<&mut Vec, &mut Vec>(&self); - (&mut self[a], &mut self2[b]) - } - } -} - -impl Index for Arena { - type Output = Node; - - fn index(&self, node: NodeId) -> &Node { - &self.nodes[node.index] - } -} - -impl IndexMut for Arena { - fn index_mut(&mut self, node: NodeId) -> &mut Node { - &mut self.nodes[node.index] - } -} - - -impl Node { - /// Return the ID of the parent node, unless this node is the root of the tree. - pub fn parent(&self) -> Option { self.parent } - - /// Return the ID of the first child of this node, unless it has no child. - pub fn first_child(&self) -> Option { self.first_child } - - /// Return the ID of the last child of this node, unless it has no child. - pub fn last_child(&self) -> Option { self.last_child } - - /// Return the ID of the previous sibling of this node, unless it is a first child. - pub fn previous_sibling(&self) -> Option { self.previous_sibling } - - /// Return the ID of the previous sibling of this node, unless it is a first child. - pub fn next_sibling(&self) -> Option { self.next_sibling } -} - - -impl NodeId { - /// Return an iterator of references to this node and its ancestors. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn ancestors(self, arena: &Arena) -> Ancestors { - Ancestors { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node and the siblings before it. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn preceding_siblings(self, arena: &Arena) -> PrecedingSiblings { - PrecedingSiblings { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node and the siblings after it. - /// - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn following_siblings(self, arena: &Arena) -> FollowingSiblings { - FollowingSiblings { - arena: arena, - node: Some(self), - } - } - - /// Return an iterator of references to this node’s children. - pub fn children(self, arena: &Arena) -> Children { - Children { - arena: arena, - node: arena[self].first_child, - } - } - - /// Return an iterator of references to this node’s children, in reverse order. - pub fn reverse_children(self, arena: &Arena) -> ReverseChildren { - ReverseChildren { - arena: arena, - node: arena[self].last_child, - } - } - - /// Return an iterator of references to this node and its descendants, in tree order. - /// - /// Parent nodes appear before the descendants. - /// Call `.next().unwrap()` once on the iterator to skip the node itself. - pub fn descendants(self, arena: &Arena) -> Descendants { - Descendants(self.traverse(arena)) - } - - /// Return an iterator of references to this node and its descendants, in tree order. - pub fn traverse(self, arena: &Arena) -> Traverse { - Traverse { - arena: arena, - root: self, - next: Some(NodeEdge::Start(self)), - } - } - - /// Return an iterator of references to this node and its descendants, in tree order. - pub fn reverse_traverse(self, arena: &Arena) -> ReverseTraverse { - ReverseTraverse { - arena: arena, - root: self, - next: Some(NodeEdge::End(self)), - } - } - - /// Detach a node from its parent and siblings. Children are not affected. - pub fn detach(self, arena: &mut Arena) { - let (parent, previous_sibling, next_sibling) = { - let node = &mut arena[self]; - (node.parent.take(), node.previous_sibling.take(), node.next_sibling.take()) - }; - - if let Some(next_sibling) = next_sibling { - arena[next_sibling].previous_sibling = previous_sibling; - } else if let Some(parent) = parent { - arena[parent].last_child = previous_sibling; - } - - if let Some(previous_sibling) = previous_sibling { - arena[previous_sibling].next_sibling = next_sibling; - } else if let Some(parent) = parent { - arena[parent].first_child = next_sibling; - } - } - - /// Append a new child to this node, after existing children. - pub fn append(self, new_child: NodeId, arena: &mut Arena) { - new_child.detach(arena); - let last_child_opt; - { - let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not append a node to itself"); - new_child_borrow.parent = Some(self); - last_child_opt = mem::replace(&mut self_borrow.last_child, Some(new_child)); - if let Some(last_child) = last_child_opt { - new_child_borrow.previous_sibling = Some(last_child); - } else { - debug_assert!(self_borrow.first_child.is_none()); - self_borrow.first_child = Some(new_child); - } - } - if let Some(last_child) = last_child_opt { - debug_assert!(arena[last_child].next_sibling.is_none()); - arena[last_child].next_sibling = Some(new_child); - } - } - - /// Prepend a new child to this node, before existing children. - pub fn prepend(self, new_child: NodeId, arena: &mut Arena) { - new_child.detach(arena); - let first_child_opt; - { - let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not prepend a node to itself"); - new_child_borrow.parent = Some(self); - first_child_opt = mem::replace(&mut self_borrow.first_child, Some(new_child)); - if let Some(first_child) = first_child_opt { - new_child_borrow.next_sibling = Some(first_child); - } else { - debug_assert!(&self_borrow.first_child.is_none()); - self_borrow.last_child = Some(new_child); - } - } - if let Some(first_child) = first_child_opt { - debug_assert!(arena[first_child].previous_sibling.is_none()); - arena[first_child].previous_sibling = Some(new_child); - } - } - - /// Insert a new sibling after this node. - pub fn insert_after(self, new_sibling: NodeId, arena: &mut Arena) { - new_sibling.detach(arena); - let next_sibling_opt; - let parent_opt; - { - let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node after itself"); - parent_opt = self_borrow.parent; - new_sibling_borrow.parent = parent_opt; - new_sibling_borrow.previous_sibling = Some(self); - next_sibling_opt = mem::replace(&mut self_borrow.next_sibling, Some(new_sibling)); - if let Some(next_sibling) = next_sibling_opt { - new_sibling_borrow.next_sibling = Some(next_sibling); - } - } - if let Some(next_sibling) = next_sibling_opt { - debug_assert!(arena[next_sibling].previous_sibling.unwrap() == self); - arena[next_sibling].previous_sibling = Some(new_sibling); - } else if let Some(parent) = parent_opt { - debug_assert!(arena[parent].last_child.unwrap() == self); - arena[parent].last_child = Some(new_sibling); - } - } - - /// Insert a new sibling before this node. - pub fn insert_before(self, new_sibling: NodeId, arena: &mut Arena) { - new_sibling.detach(arena); - let previous_sibling_opt; - let parent_opt; - { - let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node before itself"); - parent_opt = self_borrow.parent; - new_sibling_borrow.parent = parent_opt; - new_sibling_borrow.next_sibling = Some(self); - previous_sibling_opt = mem::replace(&mut self_borrow.previous_sibling, Some(new_sibling)); - if let Some(previous_sibling) = previous_sibling_opt { - new_sibling_borrow.previous_sibling = Some(previous_sibling); - } - } - if let Some(previous_sibling) = previous_sibling_opt { - debug_assert!(arena[previous_sibling].next_sibling.unwrap() == self); - arena[previous_sibling].next_sibling = Some(new_sibling); - } else if let Some(parent) = parent_opt { - debug_assert!(arena[parent].first_child.unwrap() == self); - arena[parent].first_child = Some(new_sibling); - } - } -} - - -macro_rules! impl_node_iterator { - ($name: ident, $next: expr) => { - impl<'a, T> Iterator for $name<'a, T> { - type Item = NodeId; - - fn next(&mut self) -> Option { - match self.node.take() { - Some(node) => { - self.node = $next(&self.arena[node]); - Some(node) - } - None => None - } - } - } - } -} - -/// An iterator of references to the ancestors a given node. -pub struct Ancestors<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(Ancestors, |node: &Node| node.parent); - -/// An iterator of references to the siblings before a given node. -pub struct PrecedingSiblings<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(PrecedingSiblings, |node: &Node| node.previous_sibling); - -/// An iterator of references to the siblings after a given node. -pub struct FollowingSiblings<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(FollowingSiblings, |node: &Node| node.next_sibling); - -/// An iterator of references to the children of a given node. -pub struct Children<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(Children, |node: &Node| node.next_sibling); - -/// An iterator of references to the children of a given node, in reverse order. -pub struct ReverseChildren<'a, T: 'a> { - arena: &'a Arena, - node: Option, -} - -impl_node_iterator!(ReverseChildren, |node: &Node| node.previous_sibling); - - -/// An iterator of references to a given node and its descendants, in tree order. -pub struct Descendants<'a, T: 'a>(Traverse<'a, T>); - -impl<'a, T> Iterator for Descendants<'a, T> { - type Item = NodeId; - - fn next(&mut self) -> Option { - loop { - match self.0.next() { - Some(NodeEdge::Start(node)) => return Some(node), - Some(NodeEdge::End(_)) => {} - None => return None - } - } - } -} - -#[derive(Debug, Clone)] -pub enum NodeEdge { - /// Indicates that start of a node that has children. - /// Yielded by `Traverse::next` before the node’s descendants. - /// In HTML or XML, this corresponds to an opening tag like `
` - Start(T), - - /// Indicates that end of a node that has children. - /// Yielded by `Traverse::next` after the node’s descendants. - /// In HTML or XML, this corresponds to a closing tag like `
` - End(T), -} - -impl NodeEdge { - pub fn inner_value(self) -> T { - use self::NodeEdge::*; - match self { - Start(t) => t, - End(t) => t, - } - } -} - -/// An iterator of references to a given node and its descendants, in tree order. -pub struct Traverse<'a, T: 'a> { - arena: &'a Arena, - root: NodeId, - next: Option>, -} - -impl<'a, T> Iterator for Traverse<'a, T> { - type Item = NodeEdge; - - fn next(&mut self) -> Option> { - match self.next.take() { - Some(item) => { - self.next = match item { - NodeEdge::Start(node) => { - match self.arena[node].first_child { - Some(first_child) => Some(NodeEdge::Start(first_child)), - None => Some(NodeEdge::End(node.clone())) - } - } - NodeEdge::End(node) => { - if node == self.root { - None - } else { - match self.arena[node].next_sibling { - Some(next_sibling) => Some(NodeEdge::Start(next_sibling)), - None => match self.arena[node].parent { - Some(parent) => Some(NodeEdge::End(parent)), - - // `node.parent()` here can only be `None` - // if the tree has been modified during iteration, - // but silently stoping iteration - // seems a more sensible behavior than panicking. - None => None - } - } - } - } - }; - Some(item) - } - None => None - } - } -} - -/// An iterator of references to a given node and its descendants, in reverse tree order. -pub struct ReverseTraverse<'a, T: 'a> { - arena: &'a Arena, - root: NodeId, - next: Option>, -} - -impl<'a, T> Iterator for ReverseTraverse<'a, T> { - type Item = NodeEdge; - - fn next(&mut self) -> Option> { - match self.next.take() { - Some(item) => { - self.next = match item { - NodeEdge::End(node) => { - match self.arena[node].last_child { - Some(last_child) => Some(NodeEdge::End(last_child)), - None => Some(NodeEdge::Start(node.clone())) - } - } - NodeEdge::Start(node) => { - if node == self.root { - None - } else { - match self.arena[node].previous_sibling { - Some(previous_sibling) => Some(NodeEdge::End(previous_sibling)), - None => match self.arena[node].parent { - Some(parent) => Some(NodeEdge::Start(parent)), - - // `node.parent()` here can only be `None` - // if the tree has been modified during iteration, - // but silently stoping iteration - // seems a more sensible behavior than panicking. - None => None - } - } - } - } - }; - Some(item) - } - None => None - } - } -} - -#[cfg(test)] -mod id_tree_tests { - use super::*; - - #[test] - fn drop_allocator() { - use std::cell::Cell; - - struct DropTracker<'a>(&'a Cell); - impl<'a> Drop for DropTracker<'a> { - fn drop(&mut self) { - self.0.set(&self.0.get() + 1); - } - } - - let drop_counter = Cell::new(0); - { - let mut new_counter = 0; - let arena = &mut Arena::new(); - macro_rules! new { - () => { - { - new_counter += 1; - arena.new_node((new_counter, DropTracker(&drop_counter))) - } - } - }; - - let a = new!(); // 1 - a.append(new!(), arena); // 2 - a.append(new!(), arena); // 3 - a.prepend(new!(), arena); // 4 - let b = new!(); // 5 - b.append(a, arena); - a.insert_before(new!(), arena); // 6 - a.insert_before(new!(), arena); // 7 - a.insert_after(new!(), arena); // 8 - a.insert_after(new!(), arena); // 9 - let c = new!(); // 10 - b.append(c, arena); - - assert_eq!(drop_counter.get(), 0); - arena[c].previous_sibling().unwrap().detach(arena); - assert_eq!(drop_counter.get(), 0); - - assert_eq!(b.descendants(arena).map(|node| arena[node].data.0).collect::>(), [ - 5, 6, 7, 1, 4, 2, 3, 9, 10 - ]); - } - - assert_eq!(drop_counter.get(), 10); - } - - - #[test] - fn children_ordering() { - - let arena = &mut Arena::new(); - let root = arena.new_node("".to_string()); - - root.append(arena.new_node("b".to_string()), arena); - root.prepend(arena.new_node("a".to_string()), arena); - root.append(arena.new_node("c".to_string()), arena); - - let children = root.children(arena).map(|node| &*arena[node].data).collect::>(); - let reverse_children = root.reverse_children(arena).map(|node| &*arena[node].data).collect::>(); - - assert_eq!(children, vec!["a", "b", "c"]); - assert_eq!(reverse_children, vec!["c", "b", "a"]); - } +//! ID-based node tree + +use std::mem; +use std::ops::{Index, IndexMut}; +use std::fmt; +use std::hash::{Hasher, Hash}; +use std::collections::BTreeMap; + +/// A node identifier within a particular `Arena`. +#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug, Hash)] +pub struct NodeId { + pub(crate) index: usize, // FIXME: use NonZero to optimize the size of Option +} + +#[derive(Clone, PartialEq)] +pub struct Node { + // Keep these private (with read-only accessors) so that we can keep them consistent. + // E.g. the parent of a node’s child is that node. + parent: Option, + previous_sibling: Option, + next_sibling: Option, + first_child: Option, + last_child: Option, + pub data: T, +} + +// Manual implementation, since `#[derive(Debug)]` requires `T: Debug` +impl fmt::Debug for Node { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Node {{ \ + parent: {:?}, \ + previous_sibling: {:?}, \ + next_sibling: {:?}, \ + first_child: {:?}, \ + last_child: {:?}, \ + data: {:?}, \ + }}", + self.parent, + self.previous_sibling, + self.next_sibling, + self.first_child, + self.last_child, + self.data) + } +} + +#[derive(Debug, Clone)] +pub struct Arena { + pub(crate) nodes: Vec>, +} + +impl PartialEq for Arena { + fn eq(&self, other: &Self) -> bool { + self.nodes == other.nodes + } +} + +impl Eq for Arena { +} + +impl Hash for Arena { + fn hash(&self, state: &mut H) { + for node in &self.nodes { + node.data.hash(state); + } + } +} + +impl Arena { + + /// Transform keeps the relative order of parents / children + /// but transforms an Arena into an Arena, by running the closure on each of the + /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. + pub fn transform(&self, closure: F) -> Arena where F: Fn(&T) -> U { + Arena { + nodes: self.nodes.iter().map(|node| Node { + parent: node.parent, + previous_sibling: node.previous_sibling, + next_sibling: node.next_sibling, + first_child: node.first_child, + last_child: node.last_child, + data: closure(&node.data) + }).collect() + } + } + + pub fn from_nodes(nodes: Vec>) -> Arena { + Self { + nodes: nodes, + } + } + + pub fn new() -> Arena { + Arena { + nodes: Vec::new(), + } + } + + /// Create a new node from its associated data. + pub fn new_node(&mut self, data: T) -> NodeId { + let next_index = self.nodes.len(); + self.nodes.push(Node { + parent: None, + first_child: None, + last_child: None, + previous_sibling: None, + next_sibling: None, + data: data, + }); + NodeId { + index: next_index, + } + } + + // Useful for debugging - returns how many + // nodes there are in the arena + pub fn nodes_len(&self) -> usize { + self.nodes.len() + } + + pub fn is_empty(&self) -> bool { + self.nodes_len() == 0 + } +} + +impl Arena { + #[inline] + pub fn get_all_node_ids(&self) -> BTreeMap { + use std::iter::FromIterator; + BTreeMap::from_iter(self.nodes.iter().enumerate().map(|(i, node)| + (NodeId { index: i }, node.data) + )) + } +} + +trait GetPairMut { + /// Get mutable references to two distinct nodes + /// + /// Panic + /// ----- + /// + /// Panics if the two given IDs are the same. + fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) + -> (&mut T, &mut T); +} + +impl GetPairMut for Vec { + fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) + -> (&mut T, &mut T) { + if a == b { + panic!(same_index_error_message) + } + unsafe { + let self2 = mem::transmute_copy::<&mut Vec, &mut Vec>(&self); + (&mut self[a], &mut self2[b]) + } + } +} + +impl Index for Arena { + type Output = Node; + + fn index(&self, node: NodeId) -> &Node { + &self.nodes[node.index] + } +} + +impl IndexMut for Arena { + fn index_mut(&mut self, node: NodeId) -> &mut Node { + &mut self.nodes[node.index] + } +} + + +impl Node { + /// Return the ID of the parent node, unless this node is the root of the tree. + pub fn parent(&self) -> Option { self.parent } + + /// Return the ID of the first child of this node, unless it has no child. + pub fn first_child(&self) -> Option { self.first_child } + + /// Return the ID of the last child of this node, unless it has no child. + pub fn last_child(&self) -> Option { self.last_child } + + /// Return the ID of the previous sibling of this node, unless it is a first child. + pub fn previous_sibling(&self) -> Option { self.previous_sibling } + + /// Return the ID of the previous sibling of this node, unless it is a first child. + pub fn next_sibling(&self) -> Option { self.next_sibling } +} + + +impl NodeId { + /// Return an iterator of references to this node and its ancestors. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + pub fn ancestors(self, arena: &Arena) -> Ancestors { + Ancestors { + arena: arena, + node: Some(self), + } + } + + /// Return an iterator of references to this node and the siblings before it. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + pub fn preceding_siblings(self, arena: &Arena) -> PrecedingSiblings { + PrecedingSiblings { + arena: arena, + node: Some(self), + } + } + + /// Return an iterator of references to this node and the siblings after it. + /// + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + pub fn following_siblings(self, arena: &Arena) -> FollowingSiblings { + FollowingSiblings { + arena: arena, + node: Some(self), + } + } + + /// Return an iterator of references to this node’s children. + pub fn children(self, arena: &Arena) -> Children { + Children { + arena: arena, + node: arena[self].first_child, + } + } + + /// Return an iterator of references to this node’s children, in reverse order. + pub fn reverse_children(self, arena: &Arena) -> ReverseChildren { + ReverseChildren { + arena: arena, + node: arena[self].last_child, + } + } + + /// Return an iterator of references to this node and its descendants, in tree order. + /// + /// Parent nodes appear before the descendants. + /// Call `.next().unwrap()` once on the iterator to skip the node itself. + pub fn descendants(self, arena: &Arena) -> Descendants { + Descendants(self.traverse(arena)) + } + + /// Return an iterator of references to this node and its descendants, in tree order. + pub fn traverse(self, arena: &Arena) -> Traverse { + Traverse { + arena: arena, + root: self, + next: Some(NodeEdge::Start(self)), + } + } + + /// Return an iterator of references to this node and its descendants, in tree order. + pub fn reverse_traverse(self, arena: &Arena) -> ReverseTraverse { + ReverseTraverse { + arena: arena, + root: self, + next: Some(NodeEdge::End(self)), + } + } + + /// Detach a node from its parent and siblings. Children are not affected. + pub fn detach(self, arena: &mut Arena) { + let (parent, previous_sibling, next_sibling) = { + let node = &mut arena[self]; + (node.parent.take(), node.previous_sibling.take(), node.next_sibling.take()) + }; + + if let Some(next_sibling) = next_sibling { + arena[next_sibling].previous_sibling = previous_sibling; + } else if let Some(parent) = parent { + arena[parent].last_child = previous_sibling; + } + + if let Some(previous_sibling) = previous_sibling { + arena[previous_sibling].next_sibling = next_sibling; + } else if let Some(parent) = parent { + arena[parent].first_child = next_sibling; + } + } + + /// Append a new child to this node, after existing children. + pub fn append(self, new_child: NodeId, arena: &mut Arena) { + new_child.detach(arena); + let last_child_opt; + { + let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( + self.index, new_child.index, "Can not append a node to itself"); + new_child_borrow.parent = Some(self); + last_child_opt = mem::replace(&mut self_borrow.last_child, Some(new_child)); + if let Some(last_child) = last_child_opt { + new_child_borrow.previous_sibling = Some(last_child); + } else { + debug_assert!(self_borrow.first_child.is_none()); + self_borrow.first_child = Some(new_child); + } + } + if let Some(last_child) = last_child_opt { + debug_assert!(arena[last_child].next_sibling.is_none()); + arena[last_child].next_sibling = Some(new_child); + } + } + + /// Prepend a new child to this node, before existing children. + pub fn prepend(self, new_child: NodeId, arena: &mut Arena) { + new_child.detach(arena); + let first_child_opt; + { + let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( + self.index, new_child.index, "Can not prepend a node to itself"); + new_child_borrow.parent = Some(self); + first_child_opt = mem::replace(&mut self_borrow.first_child, Some(new_child)); + if let Some(first_child) = first_child_opt { + new_child_borrow.next_sibling = Some(first_child); + } else { + debug_assert!(&self_borrow.first_child.is_none()); + self_borrow.last_child = Some(new_child); + } + } + if let Some(first_child) = first_child_opt { + debug_assert!(arena[first_child].previous_sibling.is_none()); + arena[first_child].previous_sibling = Some(new_child); + } + } + + /// Insert a new sibling after this node. + pub fn insert_after(self, new_sibling: NodeId, arena: &mut Arena) { + new_sibling.detach(arena); + let next_sibling_opt; + let parent_opt; + { + let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( + self.index, new_sibling.index, "Can not insert a node after itself"); + parent_opt = self_borrow.parent; + new_sibling_borrow.parent = parent_opt; + new_sibling_borrow.previous_sibling = Some(self); + next_sibling_opt = mem::replace(&mut self_borrow.next_sibling, Some(new_sibling)); + if let Some(next_sibling) = next_sibling_opt { + new_sibling_borrow.next_sibling = Some(next_sibling); + } + } + if let Some(next_sibling) = next_sibling_opt { + debug_assert!(arena[next_sibling].previous_sibling.unwrap() == self); + arena[next_sibling].previous_sibling = Some(new_sibling); + } else if let Some(parent) = parent_opt { + debug_assert!(arena[parent].last_child.unwrap() == self); + arena[parent].last_child = Some(new_sibling); + } + } + + /// Insert a new sibling before this node. + pub fn insert_before(self, new_sibling: NodeId, arena: &mut Arena) { + new_sibling.detach(arena); + let previous_sibling_opt; + let parent_opt; + { + let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( + self.index, new_sibling.index, "Can not insert a node before itself"); + parent_opt = self_borrow.parent; + new_sibling_borrow.parent = parent_opt; + new_sibling_borrow.next_sibling = Some(self); + previous_sibling_opt = mem::replace(&mut self_borrow.previous_sibling, Some(new_sibling)); + if let Some(previous_sibling) = previous_sibling_opt { + new_sibling_borrow.previous_sibling = Some(previous_sibling); + } + } + if let Some(previous_sibling) = previous_sibling_opt { + debug_assert!(arena[previous_sibling].next_sibling.unwrap() == self); + arena[previous_sibling].next_sibling = Some(new_sibling); + } else if let Some(parent) = parent_opt { + debug_assert!(arena[parent].first_child.unwrap() == self); + arena[parent].first_child = Some(new_sibling); + } + } +} + + +macro_rules! impl_node_iterator { + ($name: ident, $next: expr) => { + impl<'a, T> Iterator for $name<'a, T> { + type Item = NodeId; + + fn next(&mut self) -> Option { + match self.node.take() { + Some(node) => { + self.node = $next(&self.arena[node]); + Some(node) + } + None => None + } + } + } + } +} + +/// An iterator of references to the ancestors a given node. +pub struct Ancestors<'a, T: 'a> { + arena: &'a Arena, + node: Option, +} + +impl_node_iterator!(Ancestors, |node: &Node| node.parent); + +/// An iterator of references to the siblings before a given node. +pub struct PrecedingSiblings<'a, T: 'a> { + arena: &'a Arena, + node: Option, +} + +impl_node_iterator!(PrecedingSiblings, |node: &Node| node.previous_sibling); + +/// An iterator of references to the siblings after a given node. +pub struct FollowingSiblings<'a, T: 'a> { + arena: &'a Arena, + node: Option, +} + +impl_node_iterator!(FollowingSiblings, |node: &Node| node.next_sibling); + +/// An iterator of references to the children of a given node. +pub struct Children<'a, T: 'a> { + arena: &'a Arena, + node: Option, +} + +impl_node_iterator!(Children, |node: &Node| node.next_sibling); + +/// An iterator of references to the children of a given node, in reverse order. +pub struct ReverseChildren<'a, T: 'a> { + arena: &'a Arena, + node: Option, +} + +impl_node_iterator!(ReverseChildren, |node: &Node| node.previous_sibling); + + +/// An iterator of references to a given node and its descendants, in tree order. +pub struct Descendants<'a, T: 'a>(Traverse<'a, T>); + +impl<'a, T> Iterator for Descendants<'a, T> { + type Item = NodeId; + + fn next(&mut self) -> Option { + loop { + match self.0.next() { + Some(NodeEdge::Start(node)) => return Some(node), + Some(NodeEdge::End(_)) => {} + None => return None + } + } + } +} + +#[derive(Debug, Clone)] +pub enum NodeEdge { + /// Indicates that start of a node that has children. + /// Yielded by `Traverse::next` before the node’s descendants. + /// In HTML or XML, this corresponds to an opening tag like `
` + Start(T), + + /// Indicates that end of a node that has children. + /// Yielded by `Traverse::next` after the node’s descendants. + /// In HTML or XML, this corresponds to a closing tag like `
` + End(T), +} + +impl NodeEdge { + pub fn inner_value(self) -> T { + use self::NodeEdge::*; + match self { + Start(t) => t, + End(t) => t, + } + } +} + +/// An iterator of references to a given node and its descendants, in tree order. +pub struct Traverse<'a, T: 'a> { + arena: &'a Arena, + root: NodeId, + next: Option>, +} + +impl<'a, T> Iterator for Traverse<'a, T> { + type Item = NodeEdge; + + fn next(&mut self) -> Option> { + match self.next.take() { + Some(item) => { + self.next = match item { + NodeEdge::Start(node) => { + match self.arena[node].first_child { + Some(first_child) => Some(NodeEdge::Start(first_child)), + None => Some(NodeEdge::End(node.clone())) + } + } + NodeEdge::End(node) => { + if node == self.root { + None + } else { + match self.arena[node].next_sibling { + Some(next_sibling) => Some(NodeEdge::Start(next_sibling)), + None => match self.arena[node].parent { + Some(parent) => Some(NodeEdge::End(parent)), + + // `node.parent()` here can only be `None` + // if the tree has been modified during iteration, + // but silently stoping iteration + // seems a more sensible behavior than panicking. + None => None + } + } + } + } + }; + Some(item) + } + None => None + } + } +} + +/// An iterator of references to a given node and its descendants, in reverse tree order. +pub struct ReverseTraverse<'a, T: 'a> { + arena: &'a Arena, + root: NodeId, + next: Option>, +} + +impl<'a, T> Iterator for ReverseTraverse<'a, T> { + type Item = NodeEdge; + + fn next(&mut self) -> Option> { + match self.next.take() { + Some(item) => { + self.next = match item { + NodeEdge::End(node) => { + match self.arena[node].last_child { + Some(last_child) => Some(NodeEdge::End(last_child)), + None => Some(NodeEdge::Start(node.clone())) + } + } + NodeEdge::Start(node) => { + if node == self.root { + None + } else { + match self.arena[node].previous_sibling { + Some(previous_sibling) => Some(NodeEdge::End(previous_sibling)), + None => match self.arena[node].parent { + Some(parent) => Some(NodeEdge::Start(parent)), + + // `node.parent()` here can only be `None` + // if the tree has been modified during iteration, + // but silently stoping iteration + // seems a more sensible behavior than panicking. + None => None + } + } + } + } + }; + Some(item) + } + None => None + } + } +} + +#[cfg(test)] +mod id_tree_tests { + use super::*; + + #[test] + fn drop_allocator() { + use std::cell::Cell; + + struct DropTracker<'a>(&'a Cell); + impl<'a> Drop for DropTracker<'a> { + fn drop(&mut self) { + self.0.set(&self.0.get() + 1); + } + } + + let drop_counter = Cell::new(0); + { + let mut new_counter = 0; + let arena = &mut Arena::new(); + macro_rules! new { + () => { + { + new_counter += 1; + arena.new_node((new_counter, DropTracker(&drop_counter))) + } + } + }; + + let a = new!(); // 1 + a.append(new!(), arena); // 2 + a.append(new!(), arena); // 3 + a.prepend(new!(), arena); // 4 + let b = new!(); // 5 + b.append(a, arena); + a.insert_before(new!(), arena); // 6 + a.insert_before(new!(), arena); // 7 + a.insert_after(new!(), arena); // 8 + a.insert_after(new!(), arena); // 9 + let c = new!(); // 10 + b.append(c, arena); + + assert_eq!(drop_counter.get(), 0); + arena[c].previous_sibling().unwrap().detach(arena); + assert_eq!(drop_counter.get(), 0); + + assert_eq!(b.descendants(arena).map(|node| arena[node].data.0).collect::>(), [ + 5, 6, 7, 1, 4, 2, 3, 9, 10 + ]); + } + + assert_eq!(drop_counter.get(), 10); + } + + + #[test] + fn children_ordering() { + + let arena = &mut Arena::new(); + let root = arena.new_node("".to_string()); + + root.append(arena.new_node("b".to_string()), arena); + root.prepend(arena.new_node("a".to_string()), arena); + root.append(arena.new_node("c".to_string()), arena); + + let children = root.children(arena).map(|node| &*arena[node].data).collect::>(); + let reverse_children = root.reverse_children(arena).map(|node| &*arena[node].data).collect::>(); + + assert_eq!(children, vec!["a", "b", "c"]); + assert_eq!(reverse_children, vec!["c", "b", "a"]); + } } \ No newline at end of file diff --git a/src/images.rs b/src/images.rs index 8e56997a7..b585c20c3 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,182 +1,182 @@ -//! Module for loading and handling images - -use webrender::api::ImageFormat as WebrenderImageFormat; -use image::{ImageResult, ImageFormat, guess_format}; -use image::{self, ImageError, DynamicImage, GenericImage}; -use webrender::api::{ImageData, ImageDescriptor, ImageKey}; - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum ImageType { - Bmp, - Gif, - Hdr, - Ico, - Jpeg, - Png, - Pnm, - Tga, - Tiff, - WebP, - /// Try to guess the image format, unknown data - GuessImageFormat, -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct ImageInfo { - pub(crate) key: ImageKey, - pub(crate) descriptor: ImageDescriptor, -} - -#[derive(Debug, Clone)] -pub(crate) enum ImageState { - // resource is available for the renderer - Uploaded(ImageInfo), - // image is loaded & decoded, but not yet available - ReadyForUpload((ImageData, ImageDescriptor)), - // Image is about to get deleted in the next frame - AboutToBeDeleted(Option), -} - - -impl ImageType { - pub(crate) fn into_image_format(&self, data: &[u8]) -> ImageResult { - use self::ImageType::*; - match *self { - Bmp => Ok(ImageFormat::BMP), - Gif => Ok(ImageFormat::GIF), - Hdr => Ok(ImageFormat::HDR), - Ico => Ok(ImageFormat::ICO), - Jpeg => Ok(ImageFormat::JPEG), - Png => Ok(ImageFormat::PNG), - Pnm => Ok(ImageFormat::PNM), - Tga => Ok(ImageFormat::TGA), - Tiff => Ok(ImageFormat::TIFF), - WebP => Ok(ImageFormat::WEBP), - GuessImageFormat => { - guess_format(data) - } - } - } -} - -// The next three functions are taken from: -// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs - -use std::path::Path; - -/// Convenience function to get the image type from a path -/// -/// This function looks at the extension of the image. However, this -/// extension could be wrong, i.e. a user labeling a PNG as a JPG and so on. -/// If you don't know the format of the image, simply use Image::GuessImageType -/// - which will guess the type of the image from the magic header in the -/// actual image data. -pub fn get_image_type_from_extension(path: &Path) -> Option { - let ext = path.extension().and_then(|s| s.to_str()) - .map_or(String::new(), |s| s.to_ascii_lowercase()); - - match &ext[..] { - "jpg" | - "jpeg" => Some(ImageType::Jpeg), - "png" => Some(ImageType::Png), - "gif" => Some(ImageType::Gif), - "webp" => Some(ImageType::WebP), - "tif" | - "tiff" => Some(ImageType::Tiff), - "tga" => Some(ImageType::Tga), - "bmp" => Some(ImageType::Bmp), - "ico" => Some(ImageType::Ico), - "hdr" => Some(ImageType::Hdr), - "pbm" | - "pam" | - "ppm" | - "pgm" => Some(ImageType::Pnm), - _ => None, - } -} - -pub(crate) fn prepare_image(image_decoded: DynamicImage) - -> Result<(ImageData, ImageDescriptor), ImageError> -{ - let image_dims = image_decoded.dimensions(); - - // see: https://github.com/servo/webrender/blob/80c614ab660bf6cca52594d0e33a0be262a7ac12/wrench/src/yaml_frame_reader.rs#L401-L427 - let (format, bytes) = match image_decoded { - image::ImageLuma8(_) => { - (WebrenderImageFormat::R8, image_decoded.raw_pixels()) - }, - image::ImageLumaA8(_) => { - let bytes = image_decoded.raw_pixels(); - let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); - for greyscale_alpha in bytes.chunks(2) { - pixels.extend_from_slice(&[ - greyscale_alpha[0], - greyscale_alpha[0], - greyscale_alpha[0], - greyscale_alpha[1] - ]); - } - // TODO: necessary for greyscale? - premultiply(pixels.as_mut_slice()); - (WebrenderImageFormat::BGRA8, pixels) - }, - image::ImageRgba8(_) => { - let mut pixels = image_decoded.raw_pixels(); - premultiply(pixels.as_mut_slice()); - (WebrenderImageFormat::BGRA8, pixels) - }, - image::ImageRgb8(_) => { - let bytes = image_decoded.raw_pixels(); - let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); - for bgr in bytes.chunks(3) { - pixels.extend_from_slice(&[ - bgr[2], - bgr[1], - bgr[0], - 0xff - ]); - } - (WebrenderImageFormat::BGRA8, pixels) - } - }; - - let opaque = is_image_opaque(format, &bytes[..]); - let allow_mipmaps = true; - let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); - let data = ImageData::new(bytes); - Ok((data, descriptor)) -} - -pub(crate) fn is_image_opaque(format: WebrenderImageFormat, bytes: &[u8]) -> bool { - match format { - WebrenderImageFormat::BGRA8 => { - let mut is_opaque = true; - for i in 0..(bytes.len() / 4) { - if bytes[i * 4 + 3] != 255 { - is_opaque = false; - break; - } - } - is_opaque - } - WebrenderImageFormat::R8 => true, - _ => unreachable!(), - } -} - -// From webrender/wrench -// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions -// This function also converts from RGBA8 to BRGA8 -pub(crate) fn premultiply(data: &mut [u8]) { - for pixel in data.chunks_mut(4) { - let a = u32::from(pixel[3]); - let r = u32::from(pixel[2]); - let g = u32::from(pixel[1]); - let b = u32::from(pixel[0]); - - pixel[3] = a as u8; - pixel[2] = ((r * a + 128) / 255) as u8; - pixel[1] = ((g * a + 128) / 255) as u8; - pixel[0] = ((b * a + 128) / 255) as u8; - } +//! Module for loading and handling images + +use webrender::api::ImageFormat as WebrenderImageFormat; +use image::{ImageResult, ImageFormat, guess_format}; +use image::{self, ImageError, DynamicImage, GenericImage}; +use webrender::api::{ImageData, ImageDescriptor, ImageKey}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ImageType { + Bmp, + Gif, + Hdr, + Ico, + Jpeg, + Png, + Pnm, + Tga, + Tiff, + WebP, + /// Try to guess the image format, unknown data + GuessImageFormat, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ImageInfo { + pub(crate) key: ImageKey, + pub(crate) descriptor: ImageDescriptor, +} + +#[derive(Debug, Clone)] +pub(crate) enum ImageState { + // resource is available for the renderer + Uploaded(ImageInfo), + // image is loaded & decoded, but not yet available + ReadyForUpload((ImageData, ImageDescriptor)), + // Image is about to get deleted in the next frame + AboutToBeDeleted(Option), +} + + +impl ImageType { + pub(crate) fn into_image_format(&self, data: &[u8]) -> ImageResult { + use self::ImageType::*; + match *self { + Bmp => Ok(ImageFormat::BMP), + Gif => Ok(ImageFormat::GIF), + Hdr => Ok(ImageFormat::HDR), + Ico => Ok(ImageFormat::ICO), + Jpeg => Ok(ImageFormat::JPEG), + Png => Ok(ImageFormat::PNG), + Pnm => Ok(ImageFormat::PNM), + Tga => Ok(ImageFormat::TGA), + Tiff => Ok(ImageFormat::TIFF), + WebP => Ok(ImageFormat::WEBP), + GuessImageFormat => { + guess_format(data) + } + } + } +} + +// The next three functions are taken from: +// https://github.com/christolliday/limn/blob/master/core/src/resources/image.rs + +use std::path::Path; + +/// Convenience function to get the image type from a path +/// +/// This function looks at the extension of the image. However, this +/// extension could be wrong, i.e. a user labeling a PNG as a JPG and so on. +/// If you don't know the format of the image, simply use Image::GuessImageType +/// - which will guess the type of the image from the magic header in the +/// actual image data. +pub fn get_image_type_from_extension(path: &Path) -> Option { + let ext = path.extension().and_then(|s| s.to_str()) + .map_or(String::new(), |s| s.to_ascii_lowercase()); + + match &ext[..] { + "jpg" | + "jpeg" => Some(ImageType::Jpeg), + "png" => Some(ImageType::Png), + "gif" => Some(ImageType::Gif), + "webp" => Some(ImageType::WebP), + "tif" | + "tiff" => Some(ImageType::Tiff), + "tga" => Some(ImageType::Tga), + "bmp" => Some(ImageType::Bmp), + "ico" => Some(ImageType::Ico), + "hdr" => Some(ImageType::Hdr), + "pbm" | + "pam" | + "ppm" | + "pgm" => Some(ImageType::Pnm), + _ => None, + } +} + +pub(crate) fn prepare_image(image_decoded: DynamicImage) + -> Result<(ImageData, ImageDescriptor), ImageError> +{ + let image_dims = image_decoded.dimensions(); + + // see: https://github.com/servo/webrender/blob/80c614ab660bf6cca52594d0e33a0be262a7ac12/wrench/src/yaml_frame_reader.rs#L401-L427 + let (format, bytes) = match image_decoded { + image::ImageLuma8(_) => { + (WebrenderImageFormat::R8, image_decoded.raw_pixels()) + }, + image::ImageLumaA8(_) => { + let bytes = image_decoded.raw_pixels(); + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for greyscale_alpha in bytes.chunks(2) { + pixels.extend_from_slice(&[ + greyscale_alpha[0], + greyscale_alpha[0], + greyscale_alpha[0], + greyscale_alpha[1] + ]); + } + // TODO: necessary for greyscale? + premultiply(pixels.as_mut_slice()); + (WebrenderImageFormat::BGRA8, pixels) + }, + image::ImageRgba8(_) => { + let mut pixels = image_decoded.raw_pixels(); + premultiply(pixels.as_mut_slice()); + (WebrenderImageFormat::BGRA8, pixels) + }, + image::ImageRgb8(_) => { + let bytes = image_decoded.raw_pixels(); + let mut pixels = Vec::with_capacity(image_dims.0 as usize * image_dims.1 as usize * 4); + for bgr in bytes.chunks(3) { + pixels.extend_from_slice(&[ + bgr[2], + bgr[1], + bgr[0], + 0xff + ]); + } + (WebrenderImageFormat::BGRA8, pixels) + } + }; + + let opaque = is_image_opaque(format, &bytes[..]); + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(image_dims.0, image_dims.1, format, opaque, allow_mipmaps); + let data = ImageData::new(bytes); + Ok((data, descriptor)) +} + +pub(crate) fn is_image_opaque(format: WebrenderImageFormat, bytes: &[u8]) -> bool { + match format { + WebrenderImageFormat::BGRA8 => { + let mut is_opaque = true; + for i in 0..(bytes.len() / 4) { + if bytes[i * 4 + 3] != 255 { + is_opaque = false; + break; + } + } + is_opaque + } + WebrenderImageFormat::R8 => true, + _ => unreachable!(), + } +} + +// From webrender/wrench +// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +// This function also converts from RGBA8 to BRGA8 +pub(crate) fn premultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = u32::from(pixel[3]); + let r = u32::from(pixel[2]); + let g = u32::from(pixel[1]); + let b = u32::from(pixel[0]); + + pixel[3] = a as u8; + pixel[2] = ((r * a + 128) / 255) as u8; + pixel[1] = ((g * a + 128) / 255) as u8; + pixel[0] = ((b * a + 128) / 255) as u8; + } } \ No newline at end of file diff --git a/src/input.rs b/src/input.rs index accd4896a..a4bee5aa6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,108 +1,108 @@ -use webrender::api::{HitTestResult, PipelineId, DocumentId, HitTestFlags, RenderApi, WorldPoint}; - -pub fn hit_test_ui(api: &RenderApi, document_id: DocumentId, pipeline_id: Option, point: WorldPoint) -> HitTestResult { - api.hit_test(document_id, pipeline_id, point, HitTestFlags::FIND_ALL) -} - -use std::time::{Instant, Duration}; -use glium::glutin::{MouseCursor, VirtualKeyCode}; - -/// Determines which keys are pressed currently (modifiers, etc.) -#[derive(Debug, Clone)] -pub struct KeyboardState -{ - /// Modifier keys that are currently actively pressed during this cycle - pub modifiers: Vec, - /// Hidden keys, such as the "n" in CTRL + n. Always lowercase - pub hidden_keys: Vec, - /// Actual keys pressed during this cycle (i.e. regular text input) - pub keys: Vec, -} - -impl KeyboardState -{ - pub fn new() -> Self - { - Self { - modifiers: Vec::new(), - hidden_keys: Vec::new(), - keys: Vec::new(), - } - } -} - -/// Mouse position on the screen -#[derive(Debug, Copy, Clone)] -pub struct MouseState -{ - /// Current mouse cursor type - pub mouse_cursor_type: MouseCursor, - //// Where the mouse cursor is. None if the window is not focused - pub mouse_cursor: Option<(i32, i32)>, - //// Is the left MB down? - pub left_down: bool, - //// Is the right MB down? - pub right_down: bool, - //// Is the middle MB down? - pub middle_down: bool, - /// How far has the mouse scrolled in x direction? - pub mouse_scroll_x: f32, - /// How far has the mouse scrolled in y direction? - pub mouse_scroll_y: f32, -} - -impl MouseState -{ - /// Creates a new mouse state - /// Input: How fast the scroll (mouse) should be converted into pixels - /// Usually around 10.0 (10 pixels per mouse wheel line) - pub fn new() -> Self - { - MouseState { - mouse_cursor_type: MouseCursor::Default, - mouse_cursor: Some((0, 0)), - left_down: false, - right_down: false, - middle_down: false, - mouse_scroll_x: 0.0, - mouse_scroll_y: 0.0, - } - } -} - -/// State, size, etc of the window, for comparing to the last frame -#[derive(Debug, Clone)] -pub struct WindowState -{ - /// The state of the keyboard - pub(crate) keyboard_state: KeyboardState, - /// The state of the mouse - pub(crate) mouse_state: MouseState, - /// Width of the window - pub width: u32, - /// Height of the window - pub height: u32, - /// Time of the last rendering update, set after the `redraw()` method - pub time_of_last_update: Instant, - /// Minimum frame time - pub min_frame_time: Duration, -} - -impl WindowState -{ - /// Creates a new window state - pub fn new( - width: u32, - height: u32, - ) -> Self - { - Self { - keyboard_state: KeyboardState::new(), - mouse_state: MouseState::new(), - width, - height, - time_of_last_update: Instant::now(), - min_frame_time: Duration::from_millis(16), - } - } -} +use webrender::api::{HitTestResult, PipelineId, DocumentId, HitTestFlags, RenderApi, WorldPoint}; + +pub fn hit_test_ui(api: &RenderApi, document_id: DocumentId, pipeline_id: Option, point: WorldPoint) -> HitTestResult { + api.hit_test(document_id, pipeline_id, point, HitTestFlags::FIND_ALL) +} + +use std::time::{Instant, Duration}; +use glium::glutin::{MouseCursor, VirtualKeyCode}; + +/// Determines which keys are pressed currently (modifiers, etc.) +#[derive(Debug, Clone)] +pub struct KeyboardState +{ + /// Modifier keys that are currently actively pressed during this cycle + pub modifiers: Vec, + /// Hidden keys, such as the "n" in CTRL + n. Always lowercase + pub hidden_keys: Vec, + /// Actual keys pressed during this cycle (i.e. regular text input) + pub keys: Vec, +} + +impl KeyboardState +{ + pub fn new() -> Self + { + Self { + modifiers: Vec::new(), + hidden_keys: Vec::new(), + keys: Vec::new(), + } + } +} + +/// Mouse position on the screen +#[derive(Debug, Copy, Clone)] +pub struct MouseState +{ + /// Current mouse cursor type + pub mouse_cursor_type: MouseCursor, + //// Where the mouse cursor is. None if the window is not focused + pub mouse_cursor: Option<(i32, i32)>, + //// Is the left MB down? + pub left_down: bool, + //// Is the right MB down? + pub right_down: bool, + //// Is the middle MB down? + pub middle_down: bool, + /// How far has the mouse scrolled in x direction? + pub mouse_scroll_x: f32, + /// How far has the mouse scrolled in y direction? + pub mouse_scroll_y: f32, +} + +impl MouseState +{ + /// Creates a new mouse state + /// Input: How fast the scroll (mouse) should be converted into pixels + /// Usually around 10.0 (10 pixels per mouse wheel line) + pub fn new() -> Self + { + MouseState { + mouse_cursor_type: MouseCursor::Default, + mouse_cursor: Some((0, 0)), + left_down: false, + right_down: false, + middle_down: false, + mouse_scroll_x: 0.0, + mouse_scroll_y: 0.0, + } + } +} + +/// State, size, etc of the window, for comparing to the last frame +#[derive(Debug, Clone)] +pub struct WindowState +{ + /// The state of the keyboard + pub(crate) keyboard_state: KeyboardState, + /// The state of the mouse + pub(crate) mouse_state: MouseState, + /// Width of the window + pub width: u32, + /// Height of the window + pub height: u32, + /// Time of the last rendering update, set after the `redraw()` method + pub time_of_last_update: Instant, + /// Minimum frame time + pub min_frame_time: Duration, +} + +impl WindowState +{ + /// Creates a new window state + pub fn new( + width: u32, + height: u32, + ) -> Self + { + Self { + keyboard_state: KeyboardState::new(), + mouse_state: MouseState::new(), + width, + height, + time_of_last_update: Instant::now(), + min_frame_time: Duration::from_millis(16), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c98d9b4d2..3b75c4911 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,22 @@ -//! azul is a library for creating graphical user interfaces in Rust. -//! +//! azul is a library for creating graphical user interfaces in Rust. +//! //! ## How it works -//! -//! azul requires your app data to "serialize" itself into a UI. +//! +//! azul requires your app data to "serialize" itself into a UI. //! This is different from how other GUI frameworks work, so it requires a bit of explanation: -//! -//! Your app data is one global struct for your whole application. This is the "model". +//! +//! Your app data is one global struct for your whole application. This is the "model". //! azul takes your model and requires you to build a DOM tree to translate the model into a view. -//! This (layouting, restyling, constraint solving) is done every 2 milliseconds. However, if your +//! This (layouting, restyling, constraint solving) is done every 2 milliseconds. However, if your //! UI doesn't change, nothing is done (in order to not stress the CPU too much). //! //! This model makes conditional UI elements and conditional styling very easy. azul takes care -//! of caching for you - your CSS and DOM elements are cached and diffed for changes, in order to -//! maximize performance. A full screen redraw should not take longer than 16 milliseconds +//! of caching for you - your CSS and DOM elements are cached and diffed for changes, in order to +//! maximize performance. A full screen redraw should not take longer than 16 milliseconds //! (currently the frame time is around 1 - 2 milliseconds). -//! +//! //! ## Hello world example -//! +//! //! For more examples, please look in the `/examples` folder. @@ -91,11 +91,11 @@ pub mod prelude { pub use webrender::api::{ColorF, ColorU}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, WindowPlacement}; - pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, + pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, WindowCreateError, WindowDecorations, WindowMonitorTarget, RendererType}; pub use font::FontError; pub use images::ImageType; - + // from the extern crate image pub use image::ImageError; } diff --git a/src/resources.rs b/src/resources.rs index 6566020a9..68fd8ca83 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,131 +1,131 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use webrender::api::{ImageKey, FontKey, FontInstanceKey}; -use FastHashMap; -use std::io::Read; -use images::{ImageState, ImageType}; -use font::{FontState, FontError}; -use image::{self, ImageError, DynamicImage, GenericImage}; -use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; -use std::collections::hash_map::Entry::*; -use app_units::Au; -use css_parser; -use css_parser::Font::ExternalFont; - -/// Font and image keys -/// -/// The idea is that azul doesn't know where the resources come from, -/// whether they are loaded from the network or a disk. -/// Fonts and images must be added and removed dynamically. If you have a -/// fonts that should be always accessible, then simply add them before the app -/// starts up. -/// -/// Images and fonts can be references across window contexts -/// (not yet tested, but should work). -#[derive(Default, Clone)] -pub(crate) struct AppResources<'a> { - /// Image cache - pub(crate) images: FastHashMap, - // Fonts are trickier to handle than images. - // First, we duplicate the font - webrender wants the raw font data, - // but we also need access to the font metrics. So we first parse the font - // to make sure that nothing is going wrong. In the next draw call, we - // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: FastHashMap, FontState)>, - // After we've looked up the FontKey in the font_data map, we can then access - // the font instance key (if there is any). If there is no font instance key, - // we first need to create one. - pub(crate) fonts: FastHashMap>, -} - -impl<'a> AppResources<'a> { - - /// See `AppState::add_image()` - pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) - -> Result, ImageError> - { - use images; // the module, not the crate! - - match self.images.entry(id.into()) { - Occupied(_) => Ok(None), - Vacant(v) => { - let mut image_data = Vec::::new(); - data.read_to_end(&mut image_data).map_err(|e| ImageError::IoError(e))?; - let image_format = image_type.into_image_format(&image_data)?; - let decoded = image::load_from_memory_with_format(&image_data, image_format)?; - v.insert(ImageState::ReadyForUpload(images::prepare_image(decoded)?)); - Ok(Some(())) - }, - } - } - - /// See `AppState::delete_image()` - pub(crate) fn delete_image>(&mut self, id: S) - -> Option<()> - { - match self.images.get_mut(id.as_ref()) { - None => None, - Some(v) => { - let to_delete_image_key = match *v { - ImageState::Uploaded(ref image_info) => { - Some(image_info.key.clone()) - }, - _ => None, - }; - *v = ImageState::AboutToBeDeleted(to_delete_image_key); - Some(()) - } - } - } - - /// See `AppState::has_image()` - pub(crate) fn has_image>(&mut self, id: S) - -> bool - { - self.images.get(id.as_ref()).is_some() - } - - /// See `AppState::add_font()` - pub(crate) fn add_font, R: Read>(&mut self, id: S, data: &mut R) - -> Result, FontError> - { - use font; - - match self.font_data.entry(ExternalFont(id.into())) { - Occupied(_) => Ok(None), - Vacant(v) => { - let mut font_data = Vec::::new(); - data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; - let parsed_font = font::rusttype_load_font(font_data.clone())?; - v.insert((parsed_font, FontState::ReadyForUpload(font_data))); - Ok(Some(())) - }, - } - } - - /// Checks if a font is currently registered and ready-to-use - pub(crate) fn has_font>(&mut self, id: S) - -> bool - { - self.font_data.get(&ExternalFont(id.into())).is_some() - } - - /// See `AppState::delete_font()` - pub(crate) fn delete_font>(&mut self, id: S) - -> Option<()> - { - // TODO: can fonts that haven't been uploaded yet be deleted? - match self.font_data.get_mut(&ExternalFont(id.into())) { - None => None, - Some(v) => { - let to_delete_font_key = match v.1 { - FontState::Uploaded(ref font_key) => { - Some(font_key.clone()) - }, - _ => None, - }; - v.1 = FontState::AboutToBeDeleted(to_delete_font_key); - Some(()) - } - } - } +use std::sync::atomic::{AtomicUsize, Ordering}; +use webrender::api::{ImageKey, FontKey, FontInstanceKey}; +use FastHashMap; +use std::io::Read; +use images::{ImageState, ImageType}; +use font::{FontState, FontError}; +use image::{self, ImageError, DynamicImage, GenericImage}; +use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; +use std::collections::hash_map::Entry::*; +use app_units::Au; +use css_parser; +use css_parser::Font::ExternalFont; + +/// Font and image keys +/// +/// The idea is that azul doesn't know where the resources come from, +/// whether they are loaded from the network or a disk. +/// Fonts and images must be added and removed dynamically. If you have a +/// fonts that should be always accessible, then simply add them before the app +/// starts up. +/// +/// Images and fonts can be references across window contexts +/// (not yet tested, but should work). +#[derive(Default, Clone)] +pub(crate) struct AppResources<'a> { + /// Image cache + pub(crate) images: FastHashMap, + // Fonts are trickier to handle than images. + // First, we duplicate the font - webrender wants the raw font data, + // but we also need access to the font metrics. So we first parse the font + // to make sure that nothing is going wrong. In the next draw call, we + // upload the font and replace the FontState with the newly created font key + pub(crate) font_data: FastHashMap, FontState)>, + // After we've looked up the FontKey in the font_data map, we can then access + // the font instance key (if there is any). If there is no font instance key, + // we first need to create one. + pub(crate) fonts: FastHashMap>, +} + +impl<'a> AppResources<'a> { + + /// See `AppState::add_image()` + pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) + -> Result, ImageError> + { + use images; // the module, not the crate! + + match self.images.entry(id.into()) { + Occupied(_) => Ok(None), + Vacant(v) => { + let mut image_data = Vec::::new(); + data.read_to_end(&mut image_data).map_err(|e| ImageError::IoError(e))?; + let image_format = image_type.into_image_format(&image_data)?; + let decoded = image::load_from_memory_with_format(&image_data, image_format)?; + v.insert(ImageState::ReadyForUpload(images::prepare_image(decoded)?)); + Ok(Some(())) + }, + } + } + + /// See `AppState::delete_image()` + pub(crate) fn delete_image>(&mut self, id: S) + -> Option<()> + { + match self.images.get_mut(id.as_ref()) { + None => None, + Some(v) => { + let to_delete_image_key = match *v { + ImageState::Uploaded(ref image_info) => { + Some(image_info.key.clone()) + }, + _ => None, + }; + *v = ImageState::AboutToBeDeleted(to_delete_image_key); + Some(()) + } + } + } + + /// See `AppState::has_image()` + pub(crate) fn has_image>(&mut self, id: S) + -> bool + { + self.images.get(id.as_ref()).is_some() + } + + /// See `AppState::add_font()` + pub(crate) fn add_font, R: Read>(&mut self, id: S, data: &mut R) + -> Result, FontError> + { + use font; + + match self.font_data.entry(ExternalFont(id.into())) { + Occupied(_) => Ok(None), + Vacant(v) => { + let mut font_data = Vec::::new(); + data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; + let parsed_font = font::rusttype_load_font(font_data.clone())?; + v.insert((parsed_font, FontState::ReadyForUpload(font_data))); + Ok(Some(())) + }, + } + } + + /// Checks if a font is currently registered and ready-to-use + pub(crate) fn has_font>(&mut self, id: S) + -> bool + { + self.font_data.get(&ExternalFont(id.into())).is_some() + } + + /// See `AppState::delete_font()` + pub(crate) fn delete_font>(&mut self, id: S) + -> Option<()> + { + // TODO: can fonts that haven't been uploaded yet be deleted? + match self.font_data.get_mut(&ExternalFont(id.into())) { + None => None, + Some(v) => { + let to_delete_font_key = match v.1 { + FontState::Uploaded(ref font_key) => { + Some(font_key.clone()) + }, + _ => None, + }; + v.1 = FontState::AboutToBeDeleted(to_delete_font_key); + Some(()) + } + } + } } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index 3b6795136..fba329da3 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -3,7 +3,7 @@ use euclid::{Length, TypedRect, TypedPoint2D}; use rusttype::{Font, Scale}; use css_parser::{TextAlignment, TextOverflowBehaviour}; -/// Lines is responsible for layouting the lines of the rectangle to +/// Lines is responsible for layouting the lines of the rectangle to struct Lines<'a> { align: TextAlignment, max_lines_before_overflow: usize, @@ -26,11 +26,11 @@ pub(crate) enum TextOverflow { impl<'a> Lines<'a> { pub(crate) fn from_bounds( - bounds: &TypedRect, - alignment: TextAlignment, - font: &'a Font<'a>, - font_size: Length) - -> Self + bounds: &TypedRect, + alignment: TextAlignment, + font: &'a Font<'a>, + font_size: Length) + -> Self { let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; let max_horizontal_width = Length::new(bounds.size.width); @@ -52,14 +52,14 @@ impl<'a> Lines<'a> { } } - /// NOTE: The glyphs are in the space of the bounds, not of the layer! + /// NOTE: The glyphs are in the space of the bounds, not of the layer! /// You'd need to offset them by `bounds.origin` to get the correct position - /// + /// /// This function will only process the glyphs until the overflow. - /// + /// /// TODO: Only process the glyphs until the screen height is filled pub(crate) fn get_glyphs(&mut self, text: &str, _overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { - + use unicode_normalization::UnicodeNormalization; use rusttype::Point; @@ -86,7 +86,7 @@ impl<'a> Lines<'a> { let index = 0; //< face index in the font file let face = Face::from_file(path, index).unwrap(); let mut font = Font::new(face); - + // Use RustType as provider for font information that harfbuzz needs. // You can also use a custom font implementation. For more information look // at the documentation for `FontFuncs`. @@ -115,12 +115,12 @@ impl<'a> Lines<'a> { // TODO: estimate how much of the text is going to fit into the rectangle let mut words = Vec::new(); - { + { for line in text.lines() { for word in line.split_whitespace() { let mut caret = 0.0; - let mut cur_word_length = 0.0; + let mut cur_word_length = 0.0; let mut glyphs_in_this_word = Vec::new(); let mut last_glyph = None; @@ -131,7 +131,7 @@ impl<'a> Lines<'a> { caret += self.font.pair_kerning(self.font_size, last, g.id()); } let g = g.positioned(Point { x: caret, y: 0.0 }); - last_glyph = Some(id); + last_glyph = Some(id); let horiz_advance = g.unpositioned().h_metrics().advance_width; caret += horiz_advance; cur_word_length += horiz_advance; @@ -152,7 +152,7 @@ impl<'a> Lines<'a> { } // Alignment + Knuth-Plass - + // Final positioning let mut positioned_glyphs = Vec::new(); { @@ -189,13 +189,13 @@ impl<'a> Lines<'a> { #[inline] pub(crate) fn put_text_in_bounds<'a>( - text: &str, - font: &Font<'a>, - font_size: Length, + text: &str, + font: &Font<'a>, + font_size: Length, alignment: TextAlignment, overflow_behaviour: TextOverflowBehaviour, - bounds: &TypedRect) --> Vec + bounds: &TypedRect) +-> Vec { let mut lines = Lines::from_bounds(bounds, alignment, font, font_size); let (glyphs, overflow) = lines.get_glyphs(text, overflow_behaviour); diff --git a/src/traits.rs b/src/traits.rs index 6dd3a7f97..b747dea35 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,200 +1,200 @@ -use dom::{NodeData, Dom}; -use ui_description::{StyledNode, CssConstraintList, UiDescription}; -use css::{Css, CssRule}; -use window::WindowId; -use id_tree::{NodeId, Arena}; -use std::rc::Rc; -use std::cell::RefCell; - -pub trait LayoutScreen { - /// Updates the DOM, must be provided by the final application. - /// - /// On each frame, a completely new DOM tree is generated. The final - /// application can cache the DOM tree, but this isn't in the scope of `azul`. - /// - /// The `style_dom` looks through the given DOM rules, applies the style and - /// recalculates the layout. This is done on each frame (except there are shortcuts - /// when the DOM doesn't have to be recalculated). - fn get_dom(&self, window_id: WindowId) -> Dom where Self: Sized; - /// Applies the CSS styles to the nodes calculated from the `layout_screen` - /// function and calculates the final display list that is submitted to the - /// renderer. - fn style_dom(dom: &Dom, css: &mut Css) -> UiDescription where Self: Sized { - css.is_dirty = false; - match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) - } -} - -pub(crate) struct ParsedCss<'a> { - pub(crate) pure_global_rules: Vec<&'a CssRule>, - pub(crate) pure_div_rules: Vec<&'a CssRule>, - pub(crate) pure_class_rules: Vec<&'a CssRule>, - pub(crate) pure_id_rules: Vec<&'a CssRule>, -} - -impl<'a> ParsedCss<'a> { - pub(crate) fn from_css(css: &'a Css) -> Self { - - // Parse the CSS nodes cascading by their importance - // 1. global rules - // 2. div-type ("html { }") specific rules - // 3. class-based rules - // 4. ID-based rules - - /* - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("direction", "row") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("justify-content", "center") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-items", "center") } - CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-content", "center") } - */ - - // note: the following passes can be done in parallel ... - - // Global rules - // * { - // background-color: blue; - // } - let pure_global_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.html_type == "*" && rule.id.is_none() && rule.classes.is_empty() - ).collect(); - - // Pure-div-type specific rules - // button { - // justify-content: center; - // } - let pure_div_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.html_type != "*" && rule.id.is_none() && rule.classes.is_empty() - ).collect(); - - // Pure-class rules - // NOTE: These classes are sorted alphabetically and are not duplicated - // - // .something .otherclass { - // text-color: red; - // } - let pure_class_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.id.is_none() && !rule.classes.is_empty() - ).collect(); - - // Pure-id rules - // #something { - // background-color: red; - // } - let pure_id_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| - rule.id.is_some() && rule.classes.is_empty() - ).collect(); - - Self { - pure_global_rules: pure_global_rules, - pure_div_rules: pure_div_rules, - pure_class_rules: pure_class_rules, - pure_id_rules: pure_id_rules, - } - } -} - -fn match_dom_css_selectors(root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss, css: &Css, parent_z_level: u32) --> UiDescription -{ - let mut root_constraints = CssConstraintList::empty(); - for global_rule in &parsed_css.pure_global_rules { - push_rule(&mut root_constraints, global_rule); - } - - let arena_borrow = &*(*arena).borrow(); - let mut styled_nodes = Vec::::new(); - let sibling_iterator = root.following_siblings(arena_borrow); - // skip the root node itself, see documentation for `following_siblings` in id_tree.rs - // sibling_iterator.next().unwrap(); - - for sibling in sibling_iterator { - styled_nodes.append(&mut match_dom_css_selectors_inner(sibling, arena_borrow, parsed_css, css, &root_constraints, parent_z_level)); - } - - UiDescription { - // note: this clone is neccessary, otherwise, - // we wouldn't be able to update the UiState - ui_descr_arena: (*arena).clone(), - ui_descr_root: Some(root), - styled_nodes: styled_nodes, - } -} - -fn match_dom_css_selectors_inner(root: NodeId, arena: &Arena>, parsed_css: &ParsedCss, css: &Css, parent_constraints: &CssConstraintList, parent_z_level: u32) --> Vec -{ - let mut styled_nodes = Vec::::new(); - - let mut current_constraints = parent_constraints.clone(); - cascade_constraints(&arena[root].data, &mut current_constraints, parsed_css, css); - - let current_node = StyledNode { - id: root, - z_level: parent_z_level, - css_constraints: current_constraints, - }; - - // DFS tree - for child in root.children(arena) { - styled_nodes.append(&mut match_dom_css_selectors_inner(child, arena, parsed_css, css, ¤t_node.css_constraints, parent_z_level + 1)); - } - - styled_nodes.push(current_node); - styled_nodes -} - -/// Cascade the rules, put them into the list -#[allow(unused_variables)] -fn cascade_constraints(node: &NodeData, list: &mut CssConstraintList, parsed_css: &ParsedCss, css: &Css) { - - for div_rule in &parsed_css.pure_div_rules { - if *node.node_type.get_css_identifier() == div_rule.html_type { - push_rule(list, div_rule); - } - } - - let mut node_classes: Vec<&String> = node.classes.iter().map(|x| x).collect(); - node_classes.sort(); - node_classes.dedup_by(|a, b| *a == *b); - - // for all classes that this node has - for class_rule in &parsed_css.pure_class_rules { - // NOTE: class_rule is sorted and de-duplicated - // If the selector matches, the node classes must be identical - let mut should_insert_rule = true; - if class_rule.classes.len() != node_classes.len() { - should_insert_rule = false; - } else { - for i in 0..class_rule.classes.len() { - // we verified that the length of the two classes is the same - if *node_classes[i] != class_rule.classes[i] { - should_insert_rule = false; - break; - } - } - } - - if should_insert_rule { - push_rule(list, class_rule); - } - } - - // first attribute for "id = something" - let node_id = &node.id; - - if let Some(ref node_id) = *node_id { - // if the node has an ID - for id_rule in &parsed_css.pure_id_rules { - if *id_rule.id.as_ref().unwrap() == *node_id { - push_rule(list, id_rule); - } - } - } - - // TODO: all the mixed rules -} - -#[inline] -fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { - list.list.insert(rule.declaration.0.clone(), rule.declaration.1.clone()); +use dom::{NodeData, Dom}; +use ui_description::{StyledNode, CssConstraintList, UiDescription}; +use css::{Css, CssRule}; +use window::WindowId; +use id_tree::{NodeId, Arena}; +use std::rc::Rc; +use std::cell::RefCell; + +pub trait LayoutScreen { + /// Updates the DOM, must be provided by the final application. + /// + /// On each frame, a completely new DOM tree is generated. The final + /// application can cache the DOM tree, but this isn't in the scope of `azul`. + /// + /// The `style_dom` looks through the given DOM rules, applies the style and + /// recalculates the layout. This is done on each frame (except there are shortcuts + /// when the DOM doesn't have to be recalculated). + fn get_dom(&self, window_id: WindowId) -> Dom where Self: Sized; + /// Applies the CSS styles to the nodes calculated from the `layout_screen` + /// function and calculates the final display list that is submitted to the + /// renderer. + fn style_dom(dom: &Dom, css: &mut Css) -> UiDescription where Self: Sized { + css.is_dirty = false; + match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) + } +} + +pub(crate) struct ParsedCss<'a> { + pub(crate) pure_global_rules: Vec<&'a CssRule>, + pub(crate) pure_div_rules: Vec<&'a CssRule>, + pub(crate) pure_class_rules: Vec<&'a CssRule>, + pub(crate) pure_id_rules: Vec<&'a CssRule>, +} + +impl<'a> ParsedCss<'a> { + pub(crate) fn from_css(css: &'a Css) -> Self { + + // Parse the CSS nodes cascading by their importance + // 1. global rules + // 2. div-type ("html { }") specific rules + // 3. class-based rules + // 4. ID-based rules + + /* + CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("direction", "row") } + CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("justify-content", "center") } + CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-items", "center") } + CssRule { html_type: "div", id: Some("main"), classes: [], declaration: ("align-content", "center") } + */ + + // note: the following passes can be done in parallel ... + + // Global rules + // * { + // background-color: blue; + // } + let pure_global_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| + rule.html_type == "*" && rule.id.is_none() && rule.classes.is_empty() + ).collect(); + + // Pure-div-type specific rules + // button { + // justify-content: center; + // } + let pure_div_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| + rule.html_type != "*" && rule.id.is_none() && rule.classes.is_empty() + ).collect(); + + // Pure-class rules + // NOTE: These classes are sorted alphabetically and are not duplicated + // + // .something .otherclass { + // text-color: red; + // } + let pure_class_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| + rule.id.is_none() && !rule.classes.is_empty() + ).collect(); + + // Pure-id rules + // #something { + // background-color: red; + // } + let pure_id_rules: Vec<&CssRule> = css.rules.iter().filter(|rule| + rule.id.is_some() && rule.classes.is_empty() + ).collect(); + + Self { + pure_global_rules: pure_global_rules, + pure_div_rules: pure_div_rules, + pure_class_rules: pure_class_rules, + pure_id_rules: pure_id_rules, + } + } +} + +fn match_dom_css_selectors(root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss, css: &Css, parent_z_level: u32) +-> UiDescription +{ + let mut root_constraints = CssConstraintList::empty(); + for global_rule in &parsed_css.pure_global_rules { + push_rule(&mut root_constraints, global_rule); + } + + let arena_borrow = &*(*arena).borrow(); + let mut styled_nodes = Vec::::new(); + let sibling_iterator = root.following_siblings(arena_borrow); + // skip the root node itself, see documentation for `following_siblings` in id_tree.rs + // sibling_iterator.next().unwrap(); + + for sibling in sibling_iterator { + styled_nodes.append(&mut match_dom_css_selectors_inner(sibling, arena_borrow, parsed_css, css, &root_constraints, parent_z_level)); + } + + UiDescription { + // note: this clone is neccessary, otherwise, + // we wouldn't be able to update the UiState + ui_descr_arena: (*arena).clone(), + ui_descr_root: Some(root), + styled_nodes: styled_nodes, + } +} + +fn match_dom_css_selectors_inner(root: NodeId, arena: &Arena>, parsed_css: &ParsedCss, css: &Css, parent_constraints: &CssConstraintList, parent_z_level: u32) +-> Vec +{ + let mut styled_nodes = Vec::::new(); + + let mut current_constraints = parent_constraints.clone(); + cascade_constraints(&arena[root].data, &mut current_constraints, parsed_css, css); + + let current_node = StyledNode { + id: root, + z_level: parent_z_level, + css_constraints: current_constraints, + }; + + // DFS tree + for child in root.children(arena) { + styled_nodes.append(&mut match_dom_css_selectors_inner(child, arena, parsed_css, css, ¤t_node.css_constraints, parent_z_level + 1)); + } + + styled_nodes.push(current_node); + styled_nodes +} + +/// Cascade the rules, put them into the list +#[allow(unused_variables)] +fn cascade_constraints(node: &NodeData, list: &mut CssConstraintList, parsed_css: &ParsedCss, css: &Css) { + + for div_rule in &parsed_css.pure_div_rules { + if *node.node_type.get_css_identifier() == div_rule.html_type { + push_rule(list, div_rule); + } + } + + let mut node_classes: Vec<&String> = node.classes.iter().map(|x| x).collect(); + node_classes.sort(); + node_classes.dedup_by(|a, b| *a == *b); + + // for all classes that this node has + for class_rule in &parsed_css.pure_class_rules { + // NOTE: class_rule is sorted and de-duplicated + // If the selector matches, the node classes must be identical + let mut should_insert_rule = true; + if class_rule.classes.len() != node_classes.len() { + should_insert_rule = false; + } else { + for i in 0..class_rule.classes.len() { + // we verified that the length of the two classes is the same + if *node_classes[i] != class_rule.classes[i] { + should_insert_rule = false; + break; + } + } + } + + if should_insert_rule { + push_rule(list, class_rule); + } + } + + // first attribute for "id = something" + let node_id = &node.id; + + if let Some(ref node_id) = *node_id { + // if the node has an ID + for id_rule in &parsed_css.pure_id_rules { + if *id_rule.id.as_ref().unwrap() == *node_id { + push_rule(list, id_rule); + } + } + } + + // TODO: all the mixed rules +} + +#[inline] +fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { + list.list.insert(rule.declaration.0.clone(), rule.declaration.1.clone()); } \ No newline at end of file diff --git a/src/ui_description.rs b/src/ui_description.rs index 26abe538e..faf6b7070 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -1,64 +1,64 @@ -use FastHashMap; -use id_tree::{Arena, NodeId}; -use traits::LayoutScreen; -use ui_state::UiState; -use css::Css; -use dom::NodeData; -use std::cell::RefCell; -use std::rc::Rc; - -pub struct UiDescription { - pub(crate) ui_descr_arena: Rc>>>, - pub(crate) ui_descr_root: Option, - pub(crate) styled_nodes: Vec, -} - -impl Clone for UiDescription { - fn clone(&self) -> Self { - Self { - ui_descr_arena: self.ui_descr_arena.clone(), - ui_descr_root: self.ui_descr_root.clone(), - styled_nodes: self.styled_nodes.clone(), - } - } -} - -impl Default for UiDescription { - fn default() -> Self { - Self { - ui_descr_arena: Rc::new(RefCell::new(Arena::new())), - ui_descr_root: None, - styled_nodes: Vec::new(), - } - } -} - -impl UiDescription { - pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self - { - T::style_dom(&ui_state.dom, style) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StyledNode { - /// The current node we are processing (the current HTML element) - pub id: NodeId, - /// The z-index level that we are currently on - pub z_level: u32, - /// The CSS constraints, after the cascading step - pub css_constraints: CssConstraintList -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CssConstraintList { - pub list: FastHashMap -} - -impl CssConstraintList { - pub fn empty() -> Self { - Self { - list: FastHashMap::default(), - } - } +use FastHashMap; +use id_tree::{Arena, NodeId}; +use traits::LayoutScreen; +use ui_state::UiState; +use css::Css; +use dom::NodeData; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct UiDescription { + pub(crate) ui_descr_arena: Rc>>>, + pub(crate) ui_descr_root: Option, + pub(crate) styled_nodes: Vec, +} + +impl Clone for UiDescription { + fn clone(&self) -> Self { + Self { + ui_descr_arena: self.ui_descr_arena.clone(), + ui_descr_root: self.ui_descr_root.clone(), + styled_nodes: self.styled_nodes.clone(), + } + } +} + +impl Default for UiDescription { + fn default() -> Self { + Self { + ui_descr_arena: Rc::new(RefCell::new(Arena::new())), + ui_descr_root: None, + styled_nodes: Vec::new(), + } + } +} + +impl UiDescription { + pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self + { + T::style_dom(&ui_state.dom, style) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StyledNode { + /// The current node we are processing (the current HTML element) + pub id: NodeId, + /// The z-index level that we are currently on + pub z_level: u32, + /// The CSS constraints, after the cascading step + pub css_constraints: CssConstraintList +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CssConstraintList { + pub list: FastHashMap +} + +impl CssConstraintList { + pub fn empty() -> Self { + Self { + list: FastHashMap::default(), + } + } } \ No newline at end of file diff --git a/src/ui_state.rs b/src/ui_state.rs index fe33f3540..50494ee23 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -1,45 +1,45 @@ -use traits::LayoutScreen; -use window::WindowId; -use std::collections::BTreeMap; -use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; -use app_state::AppState; -use std::fmt; - -pub struct UiState { - pub dom: Dom, - pub callback_list: BTreeMap>, - pub node_ids_to_callbacks_list: BTreeMap>, -} - -impl fmt::Debug for UiState { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "UiState {{ - dom: {:?}, - callback_list: {:?}, - node_ids_to_callbacks_list: {:?} -}}", - self.dom, - self.callback_list, - self.node_ids_to_callbacks_list) - } -} - -impl UiState { - pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId) -> Self - { - use dom::{Dom, On}; - - let dom: Dom = app_state.data.get_dom(window_id); - unsafe { NODE_ID = 0 }; - unsafe { CALLBACK_ID = 0 }; - let mut callback_list = BTreeMap::>::new(); - let mut node_ids_to_callbacks_list = BTreeMap::>::new(); - dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); - - UiState { - dom: dom, - callback_list: callback_list, - node_ids_to_callbacks_list: node_ids_to_callbacks_list, - } - } +use traits::LayoutScreen; +use window::WindowId; +use std::collections::BTreeMap; +use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; +use app_state::AppState; +use std::fmt; + +pub struct UiState { + pub dom: Dom, + pub callback_list: BTreeMap>, + pub node_ids_to_callbacks_list: BTreeMap>, +} + +impl fmt::Debug for UiState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "UiState {{ + dom: {:?}, + callback_list: {:?}, + node_ids_to_callbacks_list: {:?} +}}", + self.dom, + self.callback_list, + self.node_ids_to_callbacks_list) + } +} + +impl UiState { + pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId) -> Self + { + use dom::{Dom, On}; + + let dom: Dom = app_state.data.get_dom(window_id); + unsafe { NODE_ID = 0 }; + unsafe { CALLBACK_ID = 0 }; + let mut callback_list = BTreeMap::>::new(); + let mut node_ids_to_callbacks_list = BTreeMap::>::new(); + dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); + + UiState { + dom: dom, + callback_list: callback_list, + node_ids_to_callbacks_list: node_ids_to_callbacks_list, + } + } } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index a738df8f3..05652b75f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -61,7 +61,7 @@ pub struct WindowCreateOptions { pub size: WindowPlacement, /// What type of window (full screen, popup, normal) pub class: WindowClass, - /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? + /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? pub renderer_type: RendererType, } @@ -84,18 +84,18 @@ impl Default for WindowCreateOptions { } } -/// Force a specific renderer. -/// By default, azul will try to use the hardware renderer and fall -/// back to the software renderer if it can't create an OpenGL 3.2 context. +/// Force a specific renderer. +/// By default, azul will try to use the hardware renderer and fall +/// back to the software renderer if it can't create an OpenGL 3.2 context. /// However, in some cases a hardware renderer might create problems -/// or you want to force either a software or hardware renderer. +/// or you want to force either a software or hardware renderer. /// -/// If the field `renderer_type` on the `WindowCreateOptions` is not +/// If the field `renderer_type` on the `WindowCreateOptions` is not /// `RendererType::Default`, the `create_window` method will try to create /// a window with the specific renderer type and **crash** if the renderer is /// not available for whatever reason. /// -/// If you don't know what any of this means, leave it at `Default`. +/// If you don't know what any of this means, leave it at `Default`. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum RendererType { Default, @@ -134,7 +134,7 @@ impl Default for WindowDecorations { } } -/// Where the window should be positioned, +/// Where the window should be positioned, /// from the top left corner of the screen #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct WindowPlacement { @@ -155,7 +155,7 @@ impl Default for WindowPlacement { } } -/// What class the window should have (important for window managers). +/// What class the window should have (important for window managers). /// Currently not in use. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum WindowClass { @@ -388,7 +388,7 @@ pub(crate) struct WindowDimensions { impl WindowDimensions { pub fn new_from_layout_size(layout_size: LayoutSize) -> Self { - Self { + Self { layout_size: layout_size, width_var: Variable::new(), height_var: Variable::new(), @@ -565,7 +565,7 @@ impl Window { }, Default => { // try hardware first, fall back to software - Renderer::new(gl.clone(), notifier.clone(), opts_native).or_else(|_| + Renderer::new(gl.clone(), notifier.clone(), opts_native).or_else(|_| Renderer::new(gl, notifier, opts_osmesa)).unwrap() } }; From 25ad5aac1b0d4a07e1a0e6be0afe29ecce6c7bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 20 May 2018 12:10:00 +0200 Subject: [PATCH 035/868] Added algorithm for centering and right-aligning text --- src/display_list.rs | 9 +- src/text_layout.rs | 400 +++++++++++++++++++++++++++++++------------- 2 files changed, 287 insertions(+), 122 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index bbfa013ea..3a824716d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -18,6 +18,7 @@ use cache::DomChangeSet; use std::sync::atomic::{Ordering, AtomicUsize}; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; +use css_parser; const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); @@ -219,8 +220,6 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { if css.needs_relayout { - println!("relayout!"); - // constraints were added or removed during the last frame for (rect_idx, rect) in self.rectangles.iter() { let arena = &*self.ui_descr.ui_descr_arena.borrow(); @@ -582,8 +581,6 @@ fn push_border( } } -use css_parser; - #[inline] fn push_font( font_id: &css_parser::Font, @@ -596,7 +593,7 @@ fn push_font( use font::FontState; if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { - println!("warning: too big or too small font size"); + eprintln!("warning: too big or too small font size"); return None; } @@ -627,7 +624,7 @@ fn push_font( Some(*font_instance_key) }, _ => { - println!("warning: trying to use font {:?} that isn't available", font_id); + eprintln!("warning: trying to use font {:?} that isn't available", font_id); None }, } diff --git a/src/text_layout.rs b/src/text_layout.rs index fba329da3..aacf7a2dc 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,3 +1,5 @@ +#![allow(unused_variables, dead_code)] + use webrender::api::*; use euclid::{Length, TypedRect, TypedPoint2D}; use rusttype::{Font, Scale}; @@ -17,6 +19,16 @@ struct Lines<'a> { v_scale_factor: f32, } +#[derive(Debug)] +struct Word<'a> { + // the original text + pub text: &'a str, + // glyphs, positions are relative to the first character of the word + pub glyphs: Vec, + // the sum of the width of all the characters + pub total_width: f32, +} + pub(crate) enum TextOverflow { /// Text is overflowing in the vertical direction IsOverflowing, @@ -24,7 +36,12 @@ pub(crate) enum TextOverflow { InBounds, } + +#[derive(Debug, Copy, Clone)] +struct HarfbuzzAdjustment(pub f32); + impl<'a> Lines<'a> { + pub(crate) fn from_bounds( bounds: &TypedRect, alignment: TextAlignment, @@ -55,135 +72,286 @@ impl<'a> Lines<'a> { /// NOTE: The glyphs are in the space of the bounds, not of the layer! /// You'd need to offset them by `bounds.origin` to get the correct position /// - /// This function will only process the glyphs until the overflow. - /// - /// TODO: Only process the glyphs until the screen height is filled + /// This function will only process the glyphs until they overflow + /// (we don't process glyphs that are out of the bounds of the rectangle, since + /// they don't get drawn anyway). pub(crate) fn get_glyphs(&mut self, text: &str, _overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { - use unicode_normalization::UnicodeNormalization; - use rusttype::Point; - - // normalize characters, i.e. A + ^ = Â - // TODO: this is currently done on the whole string - let text = text.nfc().collect::(); - - #[derive(Debug)] - struct Word<'a> { - // the original text - pub text: &'a str, - // glyphs, positions are relative to the first character of the word - pub glyphs: Vec, - // the sum of the width of all the characters - pub total_width: f32, - } + let font = &self.font; + let font_size = self.font_size; + let max_horizontal_width = self.max_horizontal_width.0; + let max_lines_before_overflow = self.max_lines_before_overflow; - // harfbuzz pass - { - use harfbuzz_rs::*; - use harfbuzz_rs::rusttype::SetRustTypeFuncs; - /* - let path = "path/to/some/font_file.otf"; - let index = 0; //< face index in the font file - let face = Face::from_file(path, index).unwrap(); - let mut font = Font::new(face); - - // Use RustType as provider for font information that harfbuzz needs. - // You can also use a custom font implementation. For more information look - // at the documentation for `FontFuncs`. - font.set_rusttype_funcs(); - let output = UnicodeBuffer::new().add_str(text).shape(&font, &[]); - let positions = output.get_glyph_positions(); - let infos = output.get_glyph_infos(); - - // iterate over the shaped glyphs - for (position, info) in positions.iter().zip(infos) { - let gid = info.codepoint; - let cluster = info.cluster; - let x_advance = position.x_advance; - let x_offset = position.x_offset; - let y_offset = position.y_offset; - - // Here you would usually draw the glyphs. - println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); - } - */ - } + // (1) Normalize characters, i.e. A + ^ = Â + let text = normalize_unicode_characters(text); + + // (2) Harfbuzz pass, for getting glyph-individual character shaping offsets + let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text); + + // (3) Split the text into words + let words = split_text_into_words(&text, font, self.font_size); + + // (4) Align text to the left + let (mut positioned_glyphs, line_break_offsets) = words_to_left_aligned_glyphs(words, font, font_size, max_horizontal_width, max_lines_before_overflow); + + // (5) Add the harfbuzz adjustments to the positioned glyphs + apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); + + // (6) Knuth-Plass layout, TODO + knuth_plass(&mut positioned_glyphs); + + // (7) Center- or right align text if necessary (modifies words) + align_text(self.align, &mut positioned_glyphs, &line_break_offsets); + + // (8) (Optional) - Add the self.origin to all the glyphs to bring them from + add_origin(&mut positioned_glyphs, self.origin.x, self.origin.y); + + (positioned_glyphs, TextOverflow::InBounds) + } +} + +/// Adds the X and Y offset to each glyph in the positioned glyph +#[inline] +fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) { + for c in positioned_glyphs { + c.point.x += x; + c.point.y += y; + } +} + +#[inline] +fn normalize_unicode_characters(text: &str) -> String { + // TODO: This is currently done on the whole string + // (should it be done after split_text_into_words?) + // TODO: THis is an expensive operation! + use unicode_normalization::UnicodeNormalization; + text.nfc().collect::() +} + +#[inline] +fn calculate_harfbuzz_adjustments(text: &str) -> Vec { + + use harfbuzz_rs::*; + use harfbuzz_rs::rusttype::SetRustTypeFuncs; + /* + let path = "path/to/some/font_file.otf"; + let index = 0; //< face index in the font file + let face = Face::from_file(path, index).unwrap(); + let mut font = Font::new(face); + + // Use RustType as provider for font information that harfbuzz needs. + // You can also use a custom font implementation. For more information look + // at the documentation for `FontFuncs`. + font.set_rusttype_funcs(); + let output = UnicodeBuffer::new().add_str(text).shape(&font, &[]); + let positions = output.get_glyph_positions(); + let infos = output.get_glyph_infos(); + + // iterate over the shaped glyphs + for (position, info) in positions.iter().zip(infos) { + let gid = info.codepoint; + let cluster = info.cluster; + let x_advance = position.x_advance; + let x_offset = position.x_offset; + let y_offset = position.y_offset; + + // Here you would usually draw the glyphs. + println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); + } + */ + Vec::new() // TODO +} - // HORRIBLE WEBRENDER HACK! - let offset_top = self.font_size.y * 3.0 / 4.0; - - // TODO: estimate how much of the text is going to fit into the rectangle - let mut words = Vec::new(); - - { - for line in text.lines() { - for word in line.split_whitespace() { - - let mut caret = 0.0; - let mut cur_word_length = 0.0; - let mut glyphs_in_this_word = Vec::new(); - let mut last_glyph = None; - - for c in word.chars() { - let g = self.font.glyph(c).scaled(self.font_size); - let id = g.id(); - if let Some(last) = last_glyph { - caret += self.font.pair_kerning(self.font_size, last, g.id()); - } - let g = g.positioned(Point { x: caret, y: 0.0 }); - last_glyph = Some(id); - let horiz_advance = g.unpositioned().h_metrics().advance_width; - caret += horiz_advance; - cur_word_length += horiz_advance; - - glyphs_in_this_word.push(GlyphInstance { - index: id.0, - point: TypedPoint2D::new(g.position().x, g.position().y), - }) - } - - words.push(Word { - text: word, - glyphs: glyphs_in_this_word, - total_width: cur_word_length, - }) +#[inline] +fn split_text_into_words<'a>(text: &'a str, font: &Font<'a>, font_size: Scale) -> Vec> { + + // TODO: this will currently split the whole text (all words) + // + // A basic optimization would be to track whether we have words that will + // step outside the maximum rectangle width + // + // I.e. only split words until the bounds of the rectangle can't contain + // them anymore (using a rough estimation) + + let mut words = Vec::new(); + + for line in text.lines() { + for word in line.split_whitespace() { + + let mut caret = 0.0; + let mut cur_word_length = 0.0; + let mut glyphs_in_this_word = Vec::new(); + let mut last_glyph = None; + + for c in word.chars() { + use rusttype::Point; + + let g = font.glyph(c).scaled(font_size); + let id = g.id(); + + if let Some(last) = last_glyph { + caret += font.pair_kerning(font_size, last, g.id()); } + + let g = g.positioned(Point { x: caret, y: 0.0 }); + last_glyph = Some(id); + let horiz_advance = g.unpositioned().h_metrics().advance_width; + caret += horiz_advance; + cur_word_length += horiz_advance; + + glyphs_in_this_word.push(GlyphInstance { + index: id.0, + point: TypedPoint2D::new(g.position().x, g.position().y), + }) } + + words.push(Word { + text: word, + glyphs: glyphs_in_this_word, + total_width: cur_word_length, + }) } + } - // Alignment + Knuth-Plass + words +} - // Final positioning - let mut positioned_glyphs = Vec::new(); - { - let v_metrics_scaled = self.font.v_metrics(self.font_size); - let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; +#[inline] +fn words_to_left_aligned_glyphs<'a>( + words: Vec>, + font: &Font<'a>, + font_size: Scale, + max_horizontal_width: f32, + max_lines_before_overflow: usize) +-> (Vec, Vec<(usize, f32)>) +{ + // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, + // left-aligned + let mut left_aligned_glyphs = Vec::::new(); - let space_width = self.font.glyph(' ').scaled(self.font_size).h_metrics().advance_width; - let mut word_caret = 0.0; - let mut cur_line = 0; + // The line break offsets (neded for center- / right-aligned text contains: + // + // - The index of the glyph at which the line breaks + // - How much space each line has (to the right edge of the containing rectangle) + let mut line_break_offsets = Vec::<(usize, f32)>::new(); - for word in words { - let text_overflows_rect = word_caret + word.total_width > self.max_horizontal_width.0; - if text_overflows_rect { - word_caret = 0.0; - cur_line += 1; - } - for mut glyph in word.glyphs { - let push_x = self.origin.x + word_caret; - let push_y = self.origin.y + (cur_line as f32 * v_advance_scaled) + offset_top; - glyph.point.x += push_x; - glyph.point.y += push_y; - positioned_glyphs.push(glyph); - } - if cur_line > self.max_lines_before_overflow { - break; - } - word_caret += word.total_width + space_width; // space between words - } + let v_metrics_scaled = font.v_metrics(font_size); + let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; + + // TODO: This is one hack because webrender locks fonts at 76 DPI + // and doesn't scale them correctly + // HORRIBLE WEBRENDER HACK! + let offset_top = font_size.y * 3.0 / 4.0; + + // In order to space between words, we need to + let space_width = font.glyph(' ').scaled(font_size).h_metrics().advance_width; + + // word_caret is the current X position of the "pen" we are writing with + let mut word_caret = 0.0; + let mut current_line_num = 0; + + for word in words { + + let text_overflows_rect = word_caret + word.total_width > max_horizontal_width; + + // Line break occurred + if text_overflows_rect { + line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + word_caret = 0.0; + current_line_num += 1; } - (positioned_glyphs, TextOverflow::InBounds) + for mut glyph in word.glyphs { + let push_x = word_caret; + let push_y = (current_line_num as f32 * v_advance_scaled) + offset_top; + glyph.point.x += push_x; + glyph.point.y += push_y; + left_aligned_glyphs.push(glyph); + } + + // Add the word width to the current word_caret + // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret + // later for the last line + word_caret += word.total_width + space_width; + + if current_line_num > max_lines_before_overflow { + break; + } + } + + // push the infos about the last line + line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + + (left_aligned_glyphs, line_break_offsets) +} + +#[inline] +fn apply_harfbuzz_adjustments(positioned_glyphs: &mut [GlyphInstance], harfbuzz_adjustments: Vec) { + // TODO +} + +#[inline] +fn knuth_plass(positioned_glyphs: &mut [GlyphInstance]) { + // TODO +} + +#[inline] +fn align_text(alignment: TextAlignment, glyphs: &mut Vec, line_breaks: &[(usize, f32)]) { + + use css_parser::TextAlignment::*; + + // Text alignment is theoretically very simple: + // + // If we have a bunch of text, such as this (the `glyphs`): + + // ^^^^^^^^^^^^ + // ^^^^^^^^ + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^ + + // and we have information about how much space each line has to the right: + // (the "---" is the space) + + // ^^^^^^^^^^^^---- + // ^^^^^^^^-------- + // ^^^^^^^^^^^^^^^^ + // ^^^^^^^^^^------ + + // Then we can center-align the text, by just taking the "-----", dividing + // it by 2 and moving all characters to the right: + + // --^^^^^^^^^^^^-- + // ----^^^^^^^^---- + // ^^^^^^^^^^^^^^^^ + // ---^^^^^^^^^^--- + + // Same for right-aligned text, but without the "divide by 2 step" + + if line_breaks.is_empty() { + return; // ??? maybe a 0-height rectangle? + } + + // assert that the last info in the line_breaks vec has the same glyph index + // i.e. the last line has to end with the last glyph + assert!(glyphs.len() - 1 == line_breaks[line_breaks.len() - 1].0); + + if alignment == TextAlignment::Left { + return; + } + + let multiply_factor = match alignment { + Left => { return; }, + Right => 1.0, // move the line by the full width + Center => 0.5, // move the line by the half width + }; + + let mut current_line_num = 0; + for (glyph_idx, glyph) in glyphs.iter_mut().enumerate() { + if glyph_idx > line_breaks[current_line_num].0 { + current_line_num += 1; + } + let space_added_full = line_breaks[current_line_num].1; + glyph.point.x += space_added_full * multiply_factor; } } From 78789031adaff865dc296007370b7a0cb911ed3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 03:34:26 +0200 Subject: [PATCH 036/868] Add macOS to Travis, added appveyor --- .travis.yml | 4 ++ README.md | 125 ++++++++++++++++++++++++++------------------------- appveyor.yml | 42 +++++++++++++++++ 3 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 appveyor.yml diff --git a/.travis.yml b/.travis.yml index 2f1dc5120..512f4bc87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,10 @@ sudo: false cache: cargo +os: + - linux + - osx + rust: - stable - beta diff --git a/README.md b/README.md index dc5a44ba0..df22a4580 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,63 @@ -# azul - -[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Build Status](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) -[![Coverage Status](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) -[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.23%20stable-blue.svg)]() - -azul is a stylable GUI framework using `webrender` and `limn-layout` for rendering - -## Design - -azul is a library, that, in difference to pretty much all other GUI libraries -uses a functional, data-driven design. `azul` requires your application data to -serialize itself into a user interface. Due to CSS stylesheets, your application can -be styled however you want. - -That said, `azul` is probably not the most efficient UI library. - -![azul design diagram](https://i.imgur.com/M5NGnBk.png) - -## Goals - -This library is not done yet. Once it is done, it should support the following: - -- Basic elements - - Label - - List Box - - Checkbox - - Radio - - Three-state checkbox - - Dropdown - - Button - - Menu - - Either / Or checkbox - - GlImage - - Ordered list (1. 2. 3.) - - Unordered list - -- OpenGL helpers - - Rectangle - - Rectangle with borders - - Circle - - Dashed / dotted circles - -- Layout (parent) - - direction (horizontal, vertical, horizontal-reverse, vertical-reverse) - - wrap (nowrap, wrap, wrap-reverse) - - justify-content: start, end, center, space-between, space-around, space-evenly - - align-items: start, end, center, stretch - - align-content: start, end, center, stretch, space-between, space-around - -- Layout (child) - - order: `number` - -- Media rules - - query window width & height - -## Use-cases - -The goal is to be used in desktop applications that require special rendering -(ex. image / vector editors) as well as games. Currently the backend is tied to -OpenGL. +# azul + +[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) +[![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) +[![Coverage Status](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) +[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.23%20stable-blue.svg)]() + +azul is a stylable GUI framework using `webrender` and `limn-layout` for rendering + +## Design + +azul is a library, that, in difference to pretty much all other GUI libraries +uses a functional, data-driven design. `azul` requires your application data to +serialize itself into a user interface. Due to CSS stylesheets, your application can +be styled however you want. + +That said, `azul` is probably not the most efficient UI library. + +![azul design diagram](https://i.imgur.com/M5NGnBk.png) + +## Goals + +This library is not done yet. Once it is done, it should support the following: + +- Basic elements + - Label + - List Box + - Checkbox + - Radio + - Three-state checkbox + - Dropdown + - Button + - Menu + - Either / Or checkbox + - GlImage + - Ordered list (1. 2. 3.) + - Unordered list + +- OpenGL helpers + - Rectangle + - Rectangle with borders + - Circle + - Dashed / dotted circles + +- Layout (parent) + - direction (horizontal, vertical, horizontal-reverse, vertical-reverse) + - wrap (nowrap, wrap, wrap-reverse) + - justify-content: start, end, center, space-between, space-around, space-evenly + - align-items: start, end, center, stretch + - align-content: start, end, center, stretch, space-between, space-around + +- Layout (child) + - order: `number` + +- Media rules + - query window width & height + +## Use-cases + +The goal is to be used in desktop applications that require special rendering +(ex. image / vector editors) as well as games. Currently the backend is tied to +OpenGL. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..4a46bc5db --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,42 @@ +environment: + global: + # This will be used as part of the zipfile name + # TODO change the project name + PROJECT_NAME: azul + # TODO feel free to delete targets/channels you don't need + matrix: + - TARGET: i686-pc-windows-gnu + CHANNEL: stable + - TARGET: i686-pc-windows-msvc + CHANNEL: stable + # This target is commented out because for some reason appveyor only ships + # GCC in 32-bit mode, so when compiling miniz.c (require for zipping), it'll + # fail although the build itself will work fine (because nobody uses a 32-bit + # compiler anymore in the real world). Just use a 64-bit compiler and + # everything will work fine. + # + - TARGET: x86_64-pc-windows-gnu + CHANNEL: stable + - TARGET: x86_64-pc-windows-msvc + CHANNEL: stable + +# Install Rust and Cargo +# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml) +install: + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin;C:\tools\mingw64\bin;C:\MinGW\bin + - gcc -v + - rustc -vV + - cargo -vV + +# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# the "directory does not contain a project or solution file" error. +# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113 +build: false + +# Equivalent to Travis' `script` phase +# TODO modify this phase as you see fit +test_script: + - cargo build --verbose --examples + - cargo test --verbose \ No newline at end of file From 220940edde83436b94dcb2dce02c733a872070d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 04:03:43 +0200 Subject: [PATCH 037/868] Add coveralls integration, fix appveyor build script --- .travis.yml | 53 +++++++++++++++++----------------------------------- appveyor.yml | 5 +++-- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/.travis.yml b/.travis.yml index 512f4bc87..f81da36d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,5 @@ language: rust -# We can't test OpenGL 3.2 on Travis, the shader compilation fails -# because glium does a check first if it has a OGL 3.2 context -script: - - cargo build --all - - cargo build --examples - - RUST_BACKTRACE=1 cargo test --features "doc-test no-opengl-tests" - sudo: false cache: cargo @@ -25,32 +18,20 @@ matrix: - rust: nightly fast_finish: true -# env: -# global: -# - RUSTFLAGS="-C link-dead-code" - -# addons: -# apt: -# packages: -# - libcurl4-openssl-dev -# - libelf-dev -# - libdw-dev -# - cmake -# - gcc -# - binutils-dev -# - libiberty-dev - -# after_success: | -# wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && -# tar xzf master.tar.gz && -# cd kcov-master && -# mkdir build && -# cd build && -# cmake .. && -# make && -# make install DESTDIR=../../kcov-build && -# cd ../.. && -# rm -rf kcov-master && -# for file in target/debug/azul-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done && -# bash <(curl -s https://codecov.io/bash) && -# echo "Uploaded code coverage" \ No newline at end of file +# We can't test OpenGL 3.2 on Travis, the shader compilation fails +# because glium does a check first if it has a OGL 3.2 context +script: + - cargo build --verbose --examples + - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" + +after_success: | + sudo apt-get install libcurl4-openssl-dev libelf-dev libdw-dev && + wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && + tar xzf master.tar.gz && + mkdir kcov-master/build && + cd kcov-master/build && + cmake .. && + make && + sudo make install && + cd ../.. && + kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/azul-*; \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 4a46bc5db..c7d2c5667 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,7 +25,8 @@ environment: install: - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - rustup-init -yv --default-toolchain %channel% --default-host %target% - - set PATH=%PATH%;%USERPROFILE%\.cargo\bin;C:\tools\mingw64\bin;C:\MinGW\bin + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin;C:\tools\mingw64\bin;C:\MinGW\bin; + - set PATH=%PATH:C:\Program Files\Git\usr\bin;=% # ignore other MinGW shells, use C:\MinGW - gcc -v - rustc -vV - cargo -vV @@ -39,4 +40,4 @@ build: false # TODO modify this phase as you see fit test_script: - cargo build --verbose --examples - - cargo test --verbose \ No newline at end of file + - cargo test --verbose --features "doc-test no-opengl-tests" \ No newline at end of file From f6d55747200f176cabad45f2e940dad9c8d99af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 04:12:01 +0200 Subject: [PATCH 038/868] Updated coveralls & travis --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index f81da36d5..e7e82b268 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,8 @@ os: rust: - stable - - beta - - nightly matrix: - allow_failures: - - rust: nightly fast_finish: true # We can't test OpenGL 3.2 on Travis, the shader compilation fails @@ -34,4 +30,5 @@ after_success: | make && sudo make install && cd ../.. && - kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/azul-*; \ No newline at end of file + kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/azul-* && + echo "uploaded coverage to coveralls"; \ No newline at end of file From 7d0b5953995e8619ceb255a2204b8f11382a1c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 04:40:49 +0200 Subject: [PATCH 039/868] Updated apt packages in travis --- .travis.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e7e82b268..67a137b5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,19 @@ rust: matrix: fast_finish: true +# Required for kcov +addons: + apt: + packages: + - libcurl4-openssl-dev + - libelf-dev + - libdw-dev + - cmake + - gcc + - binutils-dev + - libiberty-dev + - zlib1g-dev + # We can't test OpenGL 3.2 on Travis, the shader compilation fails # because glium does a check first if it has a OGL 3.2 context script: @@ -21,14 +34,16 @@ script: - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" after_success: | - sudo apt-get install libcurl4-openssl-dev libelf-dev libdw-dev && wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && tar xzf master.tar.gz && - mkdir kcov-master/build && - cd kcov-master/build && + cd kcov-master && + mkdir build && + cd build && cmake .. && make && sudo make install && cd ../.. && - kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/azul-* && - echo "uploaded coverage to coveralls"; \ No newline at end of file + rm -rf kcov-master && + kcov --exclude-pattern=/.cargo,/usr/lib --verify target/cov target/debug/azul-* && + bash <(curl -s https://codecov.io/bash) && + echo "Uploaded code coverage" \ No newline at end of file From c5cb65ab589d8fa6d6b95196c079253fbd40dd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 08:35:34 +0200 Subject: [PATCH 040/868] Update travis & appveyor to use coveralls, remove windows-gnu --- .travis.yml | 2 +- appveyor.yml | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 67a137b5b..8f80515ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,6 +44,6 @@ after_success: | sudo make install && cd ../.. && rm -rf kcov-master && - kcov --exclude-pattern=/.cargo,/usr/lib --verify target/cov target/debug/azul-* && + kcov –-coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo,/usr/lib --verify target/cov target/debug/azul-* && bash <(curl -s https://codecov.io/bash) && echo "Uploaded code coverage" \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index c7d2c5667..0aeb05802 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,10 @@ environment: PROJECT_NAME: azul # TODO feel free to delete targets/channels you don't need matrix: - - TARGET: i686-pc-windows-gnu - CHANNEL: stable + # Disable windows-gnu because appveyor has problems installing CMAKE correctly + # (CMAKE_C_COMPILER not set) + # - TARGET: i686-pc-windows-gnu + # CHANNEL: stable - TARGET: i686-pc-windows-msvc CHANNEL: stable # This target is commented out because for some reason appveyor only ships @@ -15,8 +17,8 @@ environment: # compiler anymore in the real world). Just use a 64-bit compiler and # everything will work fine. # - - TARGET: x86_64-pc-windows-gnu - CHANNEL: stable + # - TARGET: x86_64-pc-windows-gnu + # CHANNEL: stable - TARGET: x86_64-pc-windows-msvc CHANNEL: stable From 31c0b3c495d1516c1796d426f4308acf0a733f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 08:35:56 +0200 Subject: [PATCH 041/868] Use forked branch of glium (preparation for menus) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2e6a7da17..2121396ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,10 @@ webrender = { git = "https://github.com/fschutt/webrender", branch = "fix_dpi" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" -glium = "0.20.0" +glium = { git = "https://github.com/fschutt/glium", branch = "mapedit-winit-windows-ext" } gleam = "0.4.20" euclid = "0.17" -image = "0.18.0" +image = "0.19.0" rusttype = "0.5.2" app_units = "0.6" unicode-normalization = "0.1.5" From 3629ac8944dd1db1f0d517671c32dd6345c721b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 08:36:21 +0200 Subject: [PATCH 042/868] Add builder functions for DOM, reexport image crate --- src/dom.rs | 21 +++++++++++++++++++++ src/lib.rs | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/dom.rs b/src/dom.rs index 630ce36a8..8a239312c 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -427,6 +427,27 @@ impl Dom { } } + /// Same as `id`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_id>(mut self, id: S) -> Self { + self.id(id); + self + } + + /// Same as `id`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_class>(mut self, class: S) -> Self { + self.class(class); + self + } + + /// Same as `event`, but easier to use for method chaining in a builder-style pattern + #[inline] + pub fn with_event>(mut self, on: On, callback: Callback) -> Self { + self.event(on, callback); + self + } + #[inline] pub fn id>(&mut self, id: S) { self.arena.borrow_mut()[self.last].data.id = Some(id.into()); diff --git a/src/lib.rs b/src/lib.rs index 3b75c4911..69d3d36d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ extern crate glium; extern crate gleam; extern crate euclid; extern crate simplecss; -extern crate image; +pub extern crate image; extern crate rusttype; extern crate app_units; extern crate unicode_normalization; From 206ac1bcc083051d09ae281e9344e5c46d473276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 08:36:35 +0200 Subject: [PATCH 043/868] Update README --- README.md | 491 ++++++++++++++++++++++++++++++++---- doc/azul_callback_model.png | Bin 0 -> 12189 bytes 2 files changed, 437 insertions(+), 54 deletions(-) create mode 100644 doc/azul_callback_model.png diff --git a/README.md b/README.md index df22a4580..8535de9ab 100644 --- a/README.md +++ b/README.md @@ -3,61 +3,444 @@ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) -[![Coverage Status](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) -[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.23%20stable-blue.svg)]() +[![coveralls](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) +[![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) +[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.26%20stable-blue.svg)]() -azul is a stylable GUI framework using `webrender` and `limn-layout` for rendering +azul is a cross-platform, stylable GUI framework using Mozillas `webrender` engine for rendering +and a CSS / DOM model for layout and rendering + +[Crates.io](https://crates.io/crates/azul) | [Library documentation](https://docs.rs/azul) | [User guide](http://azul.rs/) ## Design -azul is a library, that, in difference to pretty much all other GUI libraries -uses a functional, data-driven design. `azul` requires your application data to -serialize itself into a user interface. Due to CSS stylesheets, your application can -be styled however you want. - -That said, `azul` is probably not the most efficient UI library. - -![azul design diagram](https://i.imgur.com/M5NGnBk.png) - -## Goals - -This library is not done yet. Once it is done, it should support the following: - -- Basic elements - - Label - - List Box - - Checkbox - - Radio - - Three-state checkbox - - Dropdown - - Button - - Menu - - Either / Or checkbox - - GlImage - - Ordered list (1. 2. 3.) - - Unordered list - -- OpenGL helpers - - Rectangle - - Rectangle with borders - - Circle - - Dashed / dotted circles - -- Layout (parent) - - direction (horizontal, vertical, horizontal-reverse, vertical-reverse) - - wrap (nowrap, wrap, wrap-reverse) - - justify-content: start, end, center, space-between, space-around, space-evenly - - align-items: start, end, center, stretch - - align-content: start, end, center, stretch, space-between, space-around - -- Layout (child) - - order: `number` - -- Media rules - - query window width & height - -## Use-cases - -The goal is to be used in desktop applications that require special rendering -(ex. image / vector editors) as well as games. Currently the backend is tied to -OpenGL. +azul is a library designed from the experience gathered during working with other +GUI toolkits. azul is very different from (QT / GTK / FLTK / etc.) in the following regards: + +- GUIs are seen as a "view" into your applications data, they are not "objects" like + in any other toolkit. There is no `button.setActive(true)` for example, as that would + introduce stateful design. +- Widgets types are simply enums that "serialize" themselves into a DOM tree. +- The DOM is immutable and gets re-generated every frame. This makes testing + and debugging very easy, since if you give the `get_dom()` function a + specific data model, you always get the same DOM back (`get_dom()` is a pure function). + This comes at a slight performance cost, however in practice the cost isn't too + high and it makes the seperation of application data and GUI data very clean. +- The layout model closely follows the CSS flexbox model. The default for CSS is + `display:flex` instead of `display:static` (keep that in mind). Some semantics + of CSS are not the same, especially the `image` and `vertical-align` properties. + However, most attributes work in azul, same as they do in CSS, i.e. `color`, + `linear-gradient`, etc. +- azul trades a slight bit of performance for better usability. azul is not meant + for game UIs, it is currently too slow for that (currently using 2 - 4 ms per frame) +- azul does not have any asyncronous callbacks - you can implement them manually by + using threads, but we will wait until the Rust compiler stabilizes async / await + later this year (should be stabilized until Dec 2018). +- azul links everything statically, including freetype and + +## Data model / Reactive programming + +To understand how to efficiently program with azul, you have to understand its +programming / memory model. One image says more than 1000 words: + +![Azul callback model](https://raw.githubusercontent.com/maps4print/azul/master/doc/azul_callback_model.png) + +This creates a very simple programming flow: + +```rust +// Your data model +struct DataModel { + /* store anything you want here that is relevant to the application */ +} + +// Data model -> DOM +impl LayoutScreen for DataModel { + fn get_dom(&self, _window_id: WindowId) -> Dom { + /* DataModel is read-only here, "serialize" from the data model into a UI */ + Dom::new(NodeType::Button { text: hello, .. }) + .with_event(On::MouseDown, Callback::Sync(my_button_was_clicked)) + } +} + +// Callback updates data model, when the button is clicked +fn my_button_was_clicked(_app_state: &mut AppState) -> UpdateScreen { + println!("Button clicked!"); + // performance optimization, tell azul that this function doesn't change the UI + // azul will still redraw when the window is resized / CSS events changed + // but by default, azul only redraws when it's absolutely necessary. + UpdateScreen::DontRedraw +} + +fn main() { + let mut app = App::new(DataModel { }); + app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); + app.run(); +} +``` + +This makes it easy to compose the UI from a set of functions, where each function +creates a sub-DOM that can be composed into a larger UI: + +```rust +impl LayoutScreen for DataModel { + fn get_dom(&self, _window_id: WindowId) -> Dom { + let mut dom = Dom::new(); + if !self.is_email_sent { + dom.add_child(email_recipients_list(&self.names)); + dom.add_child(email_send_button()); + } else { + dom.add_child(no_email_label()); + } + dom + } +} + +fn email_recipients_list(names: &[String]) -> Dom { + let mut names_list = Dom::new(NodeType::Div); + for name in names { + names_list.add_child(Dom::new(NodeType::Label { + text: name, + })); + } + names_list +} + +fn email_send_button() -> Dom { + Dom::new(NodeType::Button { text: hello, .. }) + .with_id("email-send-button") + .with_event(On::MouseDown, Callback::Sync(my_button_was_clicked)) +} + +fn no_email_label() { + Dom::new(NodeType::Button { text: "No email to send!", .. }) + .with_id("email-done-label") +} + +fn send_email(app_state: &mut AppState, _window_id: WindowId) -> UpdateScreen { + app_state.data.is_email_sent = false; + // trigger a redraw, so the list gets removed from the screen + // and the "you're done" message is displayed + UpdateScreen::Redraw +} +``` + +And this is why azul doesn't really have a large API like other frameworks - +that's really all there is to it! Didn't I say it was simple to learn? + +The benefit of this is that it's very simple to test: + +```rust +#[test] +fn test_it_should_send_the_email() { + let mut initial_state = AppState::new(DataModel { is_email_sent: false }); + send_email(&mut initial_state, WindowId::new(0)); + assert_eq!(initial_state.is_email_sent, true); +} +``` + +However, this model gets a bit tricky when you want to know about additional +information in the callback (such as determining which email recipient of the +list was clicked on). + +// TODO: explain how to send hit test IDs in the callback + +## Updating window properties + +You may have noticed that the callback takes in a `AppState`, not +the `DataModel` directly. This is because you can change the window settings, for +example the title of the window: + +```rust +fn callback(app_state: &mut AppState, window_id: WindowId) -> UpdateScreen { + app_state.windows[window_id].window.title = "Hello"; + app_state.windows[window_id].window.menu += "&Application > &Quit\tAlt+F4"; +} +``` + +Note how there isn't any `.get_title()` or `.set_title()`. Simply setting the title +is enough to invoke the (stateful) Win32 / X11 / Wayland / Cocoa functions for setting +the window title. + +## Working with blocking IO + +Bloking IO is when you have to wait for something to complete, a website returning +HTMl / JSON. Azul has to continouusly poll if the execution has finished. + +For this, azul has a mechanism called a "Task" (similar to C#). A Task starts a +background thread and azul registers it and looks if the thread has finished yet. +Usually you lock the data model when the task is done, i.e. when you've finished loading +the file / website, etc. + +```rust + +struct DataModel { + website_data: Option, +} + +impl LayoutScreen { + /// Note: `get_dom` is called in a thread-safe way by azul. + fn get_dom(&self, _window_id: WindowId) -> Dom { + let mut dom = Dom::new(); + match self.website_data { + Some(data) => dom.append_child(Dom::new(NodeType::Label { text: data.clone() })), + None => dom.append_child( + Dom::new(NodeType::Button { text: "Download the website", .. }) + .with_event(On::Click, Callback::Async(start_download))), + } + dom + } +} + +// Note: push_background_task are only implemented on Arc>, not T itself. +fn start_download(app_state: &mut Arc>>, _window_id: WindowId) -> UpdateScreen { + // background_fns creates a background thread that clones the app_state Arc, + // waits until the thread has completed and then calls `get_dom()` on completion, to update the UI + app_state.push_background_task(Task(download_website)); + UpdateScreen::DontRedraw +} + +// Note: The `_drop` is necessary so that azul can tell that the thread has finished executing. +fn download_website(app_state: Arc>>, _drop: Arc<()>) { + // simulate slow, blocking IO + ::std::thread::sleep(::std::time::Duration::from_secs(5)); + // only lock the Arc when done with the work + let app_state = app_state.lock().unwrap(); + app_state.data.website_data = Some("

Hello

".into()); +} +``` + +Note that there is no "wait". If you call `app_state.lock()` in the `download_website` function, +it will block the main thread, so only call it once you are done with the blocking IO. + +These concepts currently start full, OS-level threads. However, generally in +desktop applications, you don't start 10k tasks at once, maybe 4 - 5 max. This concept +will be replaced by async / await syntax, until then it uses the OS threads. + +## Deamons + +Sometimes you want to run functions independent of the user interacting with the application. +Example: you want to update a progress bar to how what percentage of a file has loaded. Or +you want to start a timer. For this, azul has "deamons" or "polling functions", that run +continouusly in the background, until they stop. + +```rust +use std::time::Duration; + +struct DataModel { + // technically you'd only need to store the Instant of the start, + // but this is just to demonstrate how deamons work + stopwatch: Option<(Instant, Duration)>, +} + +impl LayoutScreen { + // pseudocode, you can imagine what display_stop_watch, create_stop_timer_btn + // and create_start_timer_button do + fn get_dom(&self, _window_id: WindowId) -> Dom { + let mut dom = Dom::new(); + match stopwatch { + Some(_, current_duration) => { + dom.append_child(display_stop_watch(current_duration)); + dom.append_child( + create_stop_timer_btn() + .with_callback(On::MouseDown, Callback::Sync(stop_timer)) + ); + }, + None => { + dom.append_child( + create_start_timer_button() + .with_callback(On::MouseDown, Callback::Sync(start_timer)) + ) + } + } + dom + } + + fn start_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { + app_state.stopwatch = Some(Instant::now(), Duration::from_secs(0)); + // Deamons are identified by ID, to allow to run ex. multiple timers at once + app_state.push_deamon("timer_1", Callback::Sync(update_timer)); + UpdateScreen::Redraw + } + + fn update_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { + app_state.data.last_time.1 = Instant::now() - app_state.data.last_time.0; + // Trigger a redraw on every frame + UpdateScreen::Redraw + } + + fn stop_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { + app_state.pop_deamon("timer_1"); + UpdateScreen::Redraw + } +} +``` + +Polling functions / deamons are useful when implementing actions that should run +independently if the user interacts with the application or not. + +## Styling + +azul comes with default styles that mimick the operating-system native style. +However, you can overwrite parts (or everything) with your custom CSS styles: + +```rust +let default_css = Css::native(); +let my_css = Css::new_from_string(include_str!("my_custom.css")); +// Use the default CSS as a fallback, but overwrite only the styles in my_custom.css +let custom_css = default_css + my_css; +``` + +The default styles are implemented using CSS classes, with the special name +`.__azul-native-`, i.e. `__azul-native-button` for styling buttons, +`.__azul-native-scrollbar` for styling scrollbars, etc. + +You can add and remove CSS styles dynamically using `my_style.push_css_rule(CssRule::new("color", "#fff"));`. +However, this will trigger a re-build of the CSS rules, relayout and re-style, and is +generally not recommended. It is recommended that you don't over-use this feature and rather switch out +CSS blocks in the `get_dom()` method, rather than changing CSS properties: + +```css +.btn-active { color: blue; } +.btn-danger { color: red; } +``` +```rust +if self.button[i].is_danger { + dom.class("btn-danger"); +} else { + dom.class("btn-active"); +} +``` + +instead of: + +```rust +if self.button[i].is_danger { + app_data.windows[window_id].css.push_rule(".btn-active { color: red; }"); +} else { + // warning: pushing CSS is stateful and won't be re-generated every frame + // DONT DO THIS, VERY BAD PERFORMANCE + app_data.windows[window_id].css.push_rule(".btn-active { color: blue; }"); +} +``` + +## SVG / Canvas API + +**NOTE: This README was written for the future, not implemented yet** + +For drawing custom graphics, azul has a high-performance 2D vector & raster API. +The core of the custom-drawing API is based on an OpenGL texture. A `NodeType::SvgComponent` +consists of "layers", like in Photoshop, which are OpenGL textures composited on top +of each other. To make it easier to display vector graphics, you can directly initialize +a component from a SVG file (uses the `resvg` parser, `usvg` minifaction, `lyon` triangulation +and `glium` drawing libraries): + +```rust +// Don't parse and SVG in the `get_dom()` function, store the parsed SVG in +// the data model, to cache it +let svg_parsed = Svg::new_from_string(include_str!("hello.svg")); + +dom.add_child + Dom::new(NodeType::DrawComponent( + DrawComponent::Svg { + layers: vec![ + ("layer-01", svg_parsed.into(), SvgCallbacks::None) + ], + } + )) + .with_id("my-svg") +); +``` + +If you want callbacks on any item **inside** the SVG, i.e. when someone clicks or hovers on / over a shape, +you can register a callback for that, using the `SvgCallbacks`. + +// TODO: explain how to register custom events + +In order to draw your own vector graphics, without putting the data through an SVG parser +first, you can build the layers yourself (ex. from the SVG data). + +Since azul needs the image library anyway (for decoding), it is re-exported, to improve build times +and reduce duplication (so you don't have to do `extern crate image`, just do `use azul::image;`) + +## Other features + +### Current + +- Supported CSS attributes (syntax is the same as CSS, expect when marked otherwise): + - `background-color` + - `background`: **Note**: `image()` takes an ID instead of a URL, see below. + - `color` + - `border-radius` + - `font-size` + - `font-family`: **Note**: same as with the `background` property, you need to register fonts first, see below. + - `text-align`: **Note**: block-text is not supported. + - `width` + - `height` + - `min-width` + - `min-height` + - `flex-direction`: **Note**: not implemented yet + - `flex-wrap`: **Note**: not implemented yet + - `justify-content`: **Note**: not implemented yet + - `align-items`: **Note**: not implemented yet + - `align-content`: **Note**: not implemented yet + +Remarks: + +1. Any measurements can be given in `px` or `em`. `px` does not respect high-DPI scaling, while `em` does. + The default is `1em` = `16px * high_dpi_scale` +2. Images and fonts are external resources that have to be cached. Use `app.add_image("id", my_image_data)` + or `app_state.add_image()`, then you can use the `"id"` that you gave the image in the CSS. + If an image is not present on a displayed div (i.e. you added it to the CSS, but forgot to add the image), + the following happens: + - In debug mode, the app crashes with a message (to notify you of the failure) + - In release mode, the app doesn't display the image (how could it?) and silently fails + The same goes for fonts (azul does currently not load any default font, but that is subject to change) +3. CSS rules are (within a block), parsed from top to bottom, so: + ```css + #my_div { + background: image("Cat01"); + background: linear-gradient(""); + } + ``` + ... will give you the linear gradient, not display the image. +4. Maybe the most important thing, cascading is currently extremely buggy, + the best result is done if you do everything via classes / ids and not mix them: + ```css + /* Will work */ + .general .specific .very-specific { color: black; } + /* Won't work */ + .general #specific .very-specific { color: black; } + ``` + The CSS parser currently only supports CSS 2.1, not CSS 3 attributes (esp. animations) + Animations are a feature to be implemented. + +### Planned + +- Animations (should be implemented in CSS, not in Rust, no breaking change necessary) +- WEBM Video playback (using libvp9 / rust-media, will be exposed the same way as images, using IDs, no breaking change) +- Asynchronous callbacks (waiting on rustc to stabilize async / await) +- Looping / polling functions (important to drive futures Executors / update the app state continouusly) + +## CPU & Memory usage + +azul checks for all the displays in an infinite loop. Windows run by default at +60 FPS, but you can limit / unlimit this in the `WindowCreateOptions`. With these +default settings, azul uses ~ 0 - 0.5% CPU and ~ 39MB RAM. However, if you add images +and fonts, the data for these has to be kept in memory (with the uncompressed RGBA +values), so the memory usage can spike to 60MB or more once images are involved. +The redraw time (when using hardware acceleration) lies between 2 and 4 milliseconds, +i.e. 400 - 200 FPS. However, azul will only redraw the screen when absolutely necessary, +so the real FPS is usually much lower. This is usually fast enough for most desktop +applications, but not for games. However, if you use the SVG API (which skips the layout step and +uses absolute positioning), this library may be fast enough for drawing the UI in your game (~ 1 ms). + +The startup time depends on how many fonts / images you add on startup, the default +time is between 100 and 200 ms for an app with no images and a single font. + +## License + +This library is MIT-licensed. It was developed by [Maps4Print](http://maps4print.com/), +for quickly prototyping and producing desktop GUI cross-platform applications, +such as vector or photo editors. + +For licensing questions, please contact opensource@maps4print.com \ No newline at end of file diff --git a/doc/azul_callback_model.png b/doc/azul_callback_model.png new file mode 100644 index 0000000000000000000000000000000000000000..08f927b7454bdb61ea3b9ecc7708689b51f455a9 GIT binary patch literal 12189 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 bdsoczIdxa#&&YtXdpxk3o@$ly^LPIXb>j~H literal 0 HcmV?d00001 From 9e6b4d5a634197d4f3888ebcada54e0cf58cbf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 08:56:02 +0200 Subject: [PATCH 044/868] Updated glium, fix winit Closed to CloseRequested --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 55d97fbb6..4a3680095 100644 --- a/src/app.rs +++ b/src/app.rs @@ -396,7 +396,7 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { WindowEvent::HiDPIFactorChanged(dpi) => { frame_event_info.new_dpi_factor = Some(dpi); }, - WindowEvent::Closed => { + WindowEvent::CloseRequested => { return true; } _ => { }, From 3cce406da7a24365a55425c96ff20e968fd050a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 21 May 2018 13:58:05 +0200 Subject: [PATCH 045/868] Added initial compositor, deamon, task, window state and menu system --- .travis.yml | 1 + README.md | 12 ++- src/app_state.rs | 23 ++++++ src/compositor.rs | 27 ++++++ src/deamon.rs | 29 +++++++ src/lib.rs | 22 ++++- src/menu.rs | 51 ++++++++++++ src/platform_ext/mod.rs | 2 + src/platform_ext/win32.rs | 138 +++++++++++++++++++++++++++++++ src/task.rs | 42 ++++++++++ src/window_state.rs | 170 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 509 insertions(+), 8 deletions(-) create mode 100644 src/compositor.rs create mode 100644 src/deamon.rs create mode 100644 src/menu.rs create mode 100644 src/platform_ext/mod.rs create mode 100644 src/platform_ext/win32.rs create mode 100644 src/task.rs create mode 100644 src/window_state.rs diff --git a/.travis.yml b/.travis.yml index 8f80515ab..26e94b69a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ matrix: # Required for kcov addons: apt: + update: true packages: - libcurl4-openssl-dev - libelf-dev diff --git a/README.md b/README.md index 8535de9ab..70be829fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # azul +# WARNING: This README has been written for the future (so I have a "spec" and don't need to update it for the 0.1 release). +# The features advertised won't work yet, they will work when the 0.1 version releases on crates.io. +# See the /examples folder for an example of what's currently possible. + [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) @@ -95,7 +99,7 @@ impl LayoutScreen for DataModel { } } -fn email_recipients_list(names: &[String]) -> Dom { +fn email_recipients_list(names: &[String]) -> Dom { let mut names_list = Dom::new(NodeType::Div); for name in names { names_list.add_child(Dom::new(NodeType::Label { @@ -105,13 +109,13 @@ fn email_recipients_list(names: &[String]) -> Dom { names_list } -fn email_send_button() -> Dom { +fn email_send_button() -> Dom { Dom::new(NodeType::Button { text: hello, .. }) .with_id("email-send-button") .with_event(On::MouseDown, Callback::Sync(my_button_was_clicked)) } -fn no_email_label() { +fn no_email_label() -> Dom { Dom::new(NodeType::Button { text: "No email to send!", .. }) .with_id("email-done-label") } @@ -119,7 +123,7 @@ fn no_email_label() { fn send_email(app_state: &mut AppState, _window_id: WindowId) -> UpdateScreen { app_state.data.is_email_sent = false; // trigger a redraw, so the list gets removed from the screen - // and the "you're done" message is displayed + // and the "No email to send!" message is displayed instead UpdateScreen::Redraw } ``` diff --git a/src/app_state.rs b/src/app_state.rs index 7ace53032..63f161b1b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,6 +4,9 @@ use std::io::Read; use images::ImageType; use image::ImageError; use font::FontError; +use std::collections::hash_map::Entry::*; +use FastHashMap; +use deamon::DeamonCallback; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application @@ -13,6 +16,8 @@ pub struct AppState<'a, T: LayoutScreen> { pub data: T, /// Fonts and images that are currently loaded into the app pub(crate) resources: AppResources<'a>, + /// Currently running deamons (polling functions) + pub(crate) deamons: FastHashMap>, } impl<'a, T: LayoutScreen> AppState<'a, T> { @@ -22,6 +27,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { Self { data: initial_data, resources: AppResources::default(), + deamons: FastHashMap::default(), } } @@ -147,4 +153,21 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { { self.resources.delete_font(id) } + + /// Create a deamon. Does nothing if a deamon with the same ID already exists. + /// + /// If the deamon was inserted, returns true, otherwise false + pub fn add_deamon>(&mut self, id: S, deamon: DeamonCallback) -> bool { + let id_string = id.into(); + match self.deamons.entry(id_string) { + Occupied(_) => false, + Vacant(v) => { v.insert(deamon); true }, + } + } + + /// Remove a currently running deamon from running. Does nothing if there is + /// already a deamon with the same ID + pub fn delete_deamon>(&mut self, id: S) -> bool { + self.deamons.remove(id.as_ref()).is_some() + } } diff --git a/src/compositor.rs b/src/compositor.rs new file mode 100644 index 000000000..4fa8db914 --- /dev/null +++ b/src/compositor.rs @@ -0,0 +1,27 @@ +//! The compositor takes all the textures for a frame and draws them on top of each other. +//! This makes it possible to use OpenGL images in the background and compose SVG elements +//! into the UI. + +use glium::texture::compressed_srgb_texture2d::CompressedSrgbTexture2d; + +#[derive(Default, Debug)] +pub struct Compositor { + textures: Vec +} + +impl Compositor { + pub fn new() -> Self { + Self::default() + } + + pub fn push_texture(&mut self, texture: CompressedSrgbTexture2d) { + self.textures.push(texture); + } + +/* + /// Draw all textures onto a final texture, which can then be displayed on the screen + pub fn compose(self) -> CompressedSrgbTexture2d { + + } +*/ +} \ No newline at end of file diff --git a/src/deamon.rs b/src/deamon.rs new file mode 100644 index 000000000..17f1ffd93 --- /dev/null +++ b/src/deamon.rs @@ -0,0 +1,29 @@ +use app_state::AppState; +use std::sync::{Arc, Mutex}; +use traits::LayoutScreen; +use window::WindowId; +use dom::UpdateScreen; + +pub struct DeamonCallback { + callback: fn(&mut T) -> UpdateScreen, +} + +impl Clone for DeamonCallback +{ + fn clone(&self) -> Self { + Self { callback: self.callback.clone() } + } +} + +/// Run all currently registered deamons on an `Arc>` +pub(crate) fn run_all_deamons(app_state: &mut AppState) -> UpdateScreen { + let mut should_update_screen = UpdateScreen::DontRedraw; + for deamon in app_state.deamons.values().cloned() { + let should_update = (deamon.callback)(&mut app_state.data); + if should_update == UpdateScreen::Redraw && + should_update_screen == UpdateScreen::DontRedraw { + should_update_screen = UpdateScreen::Redraw; + } + } + should_update_screen +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 69d3d36d3..eef3fa28a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,14 +24,15 @@ #![allow(dead_code)] #![allow(unused_imports)] +pub extern crate glium; +pub extern crate gleam; +pub extern crate euclid; +pub extern crate image; + extern crate webrender; extern crate cassowary; extern crate twox_hash; -extern crate glium; -extern crate gleam; -extern crate euclid; extern crate simplecss; -pub extern crate image; extern crate rusttype; extern crate app_units; extern crate unicode_normalization; @@ -74,6 +75,19 @@ mod cache; mod images; /// Font handling mod font; +/// Window state handling, event filtering +mod window_state; +/// Application / context menu handling. Currently Win32 only. Also has parsing functions +mod menu; +/// Deamon / polling function implementation +mod deamon; +/// The compositor takes all textures (user-defined + the UI texture(s)) and draws them on +/// top of each other +mod compositor; +/// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) +mod platform_ext; +/// Async IO / task system +mod task; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 000000000..4e52ae1bd --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,51 @@ +//! Note: Application menus currently only works on Windows. +//! +//! Linux has a very complicated and especially undocumented API on how to create menus via DBus, +//! and even then, window managers can just "ignore" the DBus menu if they feel like it, +//! so you have to provide a fallback via borderless windows anyway. +//! +//! So there's no guarantee that the "native" menu actually shows up and I really don't have +//! the time to debug for some random guy on the internet why his custom Gentoo installation +//! with a riced xorg.conf doesn't work correcly... if you feel strongly about this, +//! then please contribute the code yourself, I'm happy to accept any hints on how to +//! correctly implement window menus. +//! +//! For the time being, application menus on Linux will be drawn using borderless windows. +//! Yes, it's a shitty solution, but it's better than nothing. +//! +//! I don't have a Mac, so that's why there are currently no menus for Macs, but I've seen +//! crates providing application menus for Cocoa, so I would be happy to use native menus +//! (the ones where the menu is in the top bar, like in any app) +//! +//! Even for Win32 menus, there is a flaw - you can't currently modify them in any way. +//! The reason being that winit (which I use for window creation and management) only + +//! This is attributed to Win32: Each menu item is a command, and you can, in the end +//! switch on the command type when you want to see which menu item was clicked. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CommandId(pub u16); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ApplicationMenu { + pub(crate) items: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ContextMenu { + pub(crate) items: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MenuItem { + /// Item, such as "New File" + ClickableItem { id: CommandId, text: String }, + /// Seperator item + Seperator, + /// Submenu + SubMenu { text: String, menu: Box }, +} + +pub mod command_ids { + // "Test" menu + pub const CMD_TEST: u16 = 9001; +} \ No newline at end of file diff --git a/src/platform_ext/mod.rs b/src/platform_ext/mod.rs new file mode 100644 index 000000000..42c33ab64 --- /dev/null +++ b/src/platform_ext/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "windows")] +pub mod win32; \ No newline at end of file diff --git a/src/platform_ext/win32.rs b/src/platform_ext/win32.rs new file mode 100644 index 000000000..dc2d7b918 --- /dev/null +++ b/src/platform_ext/win32.rs @@ -0,0 +1,138 @@ +//! Extension module to add a Win32 menu a winit window + +use menu::{MenuItem, ApplicationMenu}; +use menu::command_ids; + +use glium::glutin::winapi::{ + shared::windef::{HMENU, HWND}, + um::winuser::{ ShowWindow, IsIconic, keybd_event, SetMenu, CreateMenu, AppendMenuW, + GetForegroundWindow, SetForegroundWindow, MF_STRING, MF_SEPARATOR, MF_POPUP, + KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, SW_RESTORE + }, +}; + +/// On Windows, if you open a window from the console, it won't bring the new window to +/// the front if any other window (like the code editor) is currently focused. +#[cfg(debug_assertions)] +fn win32_bring_window_to_top(hwnd: HWND) { + // Not checked for errors since it isn't really important if it succeeds or not + // + // NOTE: SetForegroundWindow does not work if the user has focused a different window + // + // While the reason is understandable (not to steal user focus), sadly Windows + // doesn't make it configurable whether you as the application developer wants to + // respect this or not, it will always assume that you don't want to de-focus the + // current window. Hence this workaround. + + // Check if the window already has focus + if hwnd as usize == unsafe { GetForegroundWindow() } as usize { + return; + } + + // If window is minimized + if unsafe { IsIconic(hwnd) } != 0 { + unsafe { ShowWindow(hwnd, SW_RESTORE) }; + } + + // Simulate an ALT key press & release to trick Windows + // into bringing the window into the foreground + unsafe { keybd_event(0xA4, 0x45, KEYEVENTF_EXTENDEDKEY | 0, 0) }; + unsafe { keybd_event(0xA4, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0) }; + + unsafe { SetForegroundWindow(hwnd) }; +} + +// Encode a Rust `&str` as a Vec compatible with the Win32 API +fn str_to_wide_vec_u16(input: &str) -> Vec { + use std::ffi::OsString; + use std::os::windows::ffi::OsStrExt; + let mut s: Vec = OsString::from(input).as_os_str().encode_wide().into_iter().collect(); + s.push(0); + s +} + +impl ApplicationMenu { + + /// NOTE: If the returned HMENU is a top-level menu, call `DestroyMenu` on it to clean up the resources. + fn build(&self) -> HMENU { + + use std::ptr; + use menu::MenuItem::*; + + // TODO: check for errors + let current_menu = unsafe { CreateMenu() }; + + for item in &self.items { + match item { + ClickableItem { id, text } => { + let text_u16 = str_to_wide_vec_u16(&text); + unsafe { AppendMenuW(current_menu, MF_STRING, id.0 as usize, text_u16.as_ptr()) }; + }, + Seperator => { + unsafe { AppendMenuW(current_menu, MF_SEPARATOR, 0, ptr::null_mut()) }; + }, + SubMenu { text, menu } => { + let text_u16 = str_to_wide_vec_u16(&text); + let menu_ptr = menu.build(); + unsafe { AppendMenuW(current_menu, MF_POPUP, menu_ptr as usize, text_u16.as_ptr()) }; + // NOTE: do not call DestroyMenu here. + // + // For some reason, Windows changes the **style of the menu** to the Windows 95 style if you do this. + // A resource leak does not happen, since destroying a pointer to a top-level-menu + // (as you should do on the result of this function) via `DestroyMenu` also recursively destroys all + // sub-menus. + // + // see: https://stackoverflow.com/questions/12392677/creating-modern-style-dynamic-menu-in-windows + } + } + } + + current_menu + } +} +/* +fn win32_create_menu(hwnd: HWND) { + + use self::command_ids::*; + + // Init the menu bar + macro_rules! menu_item { + ($id:expr, $text:expr) => (MenuItem::ClickableItem { id: $id, text: $text.into() }) + } + macro_rules! seperator { + () => (MenuItem::Seperator) + } + + let menu = ApplicationMenu { + items: vec![ + MenuItem::SubMenu { + text: "&Test".into(), + menu: Box::new(ApplicationMenu { + items: vec![ + menu_item!(CMD_TEST, "&Hello\tCtrl+Shift+O"), + ] + }) + }, + ] + }; + + let menu_ptr = menu.build(); + unsafe { SetMenu(hwnd, menu_ptr) }; + + // NOTE: DestroyMenu changes the style of the app, see above + // Not sure if this actually leaks the memory of the menu... this seems to be a + // Windows problem. Since the menu is only created once on startup, it probably + // doesn't matter much. + // + // unsafe { DestroyMenu(menu_ptr) }; +} + +// When calling Win32 functions, especially at startup, they have to be +// called from the same thread as the Win32 message loop, otherwise Windows +// will lock up the application. +pub fn win32_create_callback(hwnd: HWND) { + // for release builds, respect the user focus + win32_bring_window_to_top(hwnd); + win32_create_menu(hwnd); +} +*/ \ No newline at end of file diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 000000000..727df98a4 --- /dev/null +++ b/src/task.rs @@ -0,0 +1,42 @@ +//! Preliminary async IO / Task system + +use app_state::AppState; +use traits::LayoutScreen; +use std::sync::{Arc, Mutex, Weak}; +use std::thread::{spawn, JoinHandle}; + +pub struct Task { + // Task is in progress + join_handle: Option>, + dropcheck: Weak<()>, +} + +impl Task { + pub fn new(app_state: Arc>, callback: fn(Arc>, Arc<()>) -> ()) -> Self { + + let thread_check = Arc::new(()); + let thread_weak = Arc::downgrade(&thread_check); + let app_state_clone = app_state.clone(); + + let thread_handle = spawn(move || { + callback(app_state_clone, thread_check) + }); + + Self { + join_handle: Some(thread_handle), + dropcheck: thread_weak, + } + } + + pub fn is_ready(&self) -> bool { + self.dropcheck.upgrade().is_none() + } +} + +impl Drop for Task { + fn drop(&mut self) { + if let Some(thread_handle) = self.join_handle.take() { + let _ = thread_handle.join().unwrap(); + } + } +} \ No newline at end of file diff --git a/src/window_state.rs b/src/window_state.rs new file mode 100644 index 000000000..cfad7673b --- /dev/null +++ b/src/window_state.rs @@ -0,0 +1,170 @@ +//! Contains methods related to event filtering (i.e. detecting whether a +//! click was a mouseover, mouseout, and so on and calling the correct callbacks) + +use glium::glutin::{ + Window, Event, WindowEvent, KeyboardInput, ElementState, + MouseCursor, VirtualKeyCode, MouseButton, MouseScrollDelta, TouchPhase +}; + +use dom::On; +use menu::{ApplicationMenu, ContextMenu}; + +/// Determines which keys are pressed currently (modifiers, etc.) +#[derive(Debug, Default, Clone)] +pub struct KeyboardState +{ + /// Modifier keys that are currently actively pressed during this frame + pub modifiers: Vec, + /// Hidden keys, such as the "n" in CTRL + n. Always lowercase + pub hidden_keys: Vec, + /// Actual keys pressed during this frame (i.e. regular text input) + pub keys: Vec, +} + +/// Mouse position on the screen +#[derive(Debug, Copy, Clone)] +pub struct MouseState +{ + /// Current mouse cursor type + pub mouse_cursor_type: MouseCursor, + //// Where is the mouse cursor? Set to `None` if the window is not focused + pub mouse_cursor: Option<(i32, i32)>, + //// Is the left MB down? + pub left_down: bool, + //// Is the right MB down? + pub right_down: bool, + //// Is the middle MB down? + pub middle_down: bool, +} + +impl Default for MouseState { + /// Creates a new mouse state + fn default() -> Self { + Self { + mouse_cursor_type: MouseCursor::Default, + mouse_cursor: Some((0, 0)), + left_down: false, + right_down: false, + middle_down: false, + } + } +} + +/// State, size, etc of the window, for comparing to the last frame +#[derive(Debug, Clone)] +pub struct WindowState +{ + /// Current title of the window + pub title: String, + /// The state of the keyboard for this frame + pub keyboard_state: KeyboardState, + /// The "global" application menu of this window (one window usually only has one menu) + pub application_menu: Option, + /// The current context menu for this window + pub context_menu: Option, + /// The x and y positon, (0, 0) by default + pub position: WindowPosition, + /// The state of the mouse + pub mouse_state: MouseState, + /// Size of the window + max width / max height: 800 x 600 by default + pub size: WindowSize, + /// Is the window currently maximized + pub maximized: bool, + /// Is the window currently fullscreened? + pub fullscreen: bool, + /// Does the window have decorations (close, minimize, maximize, title bar)? + pub decorations: bool, + /// Is the window currently visible? + pub visible: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct WindowPosition { + /// X position from the left side of the screen + pub x: u32, + /// Y position from the top of the screen + pub y: u32, +} + +impl Default for WindowPosition { + fn default() -> Self { + Self { + x: 0, + y: 0, + } + } +} + + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct WindowSize { + /// Width of the window + pub width: u32, + /// Height of the window + pub height: u32, + /// Minimum dimensions of the window + pub min_dimensions: Option<(u32, u32)>, + /// Maximum dimensions of the window + pub max_dimensions: Option<(u32, u32)>, + +} + +impl Default for WindowSize { + fn default() -> Self { + Self { + width: 800, + height: 600, + min_dimensions: None, + max_dimensions: None, + } + } +} + +impl WindowState +{ + /// Creates a new window state + pub(crate) fn new>(width: u32, height: u32, title: S ) -> Self { + Self { + title: title.into(), + keyboard_state: KeyboardState::default(), + mouse_state: MouseState::default(), + application_menu: None, + context_menu: None, + position: WindowPosition::default(), + size: WindowSize { width, height, .. Default::default() }, + maximized: false, + fullscreen: false, + decorations: true, + visible: true, + } + } + + // Determine which event / which callback(s) should be called and in which order + // + // This function also updates / mutates the current window state, + // so that we are ready for the next frame + pub(crate) fn determine_callback(&mut self, event: &Event) -> Vec { +/* + pub enum On { + MouseOver, + MouseDown, + MouseUp, + MouseEnter, + MouseLeave, + } +*/ + // TODO + Vec::new() + } +} + +fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { + if *old != *new { + window.set_cursor(*new); + } +} + +fn virtual_key_code_to_char(code: VirtualKeyCode) -> Option { + // TODO + Some('a') +} \ No newline at end of file From b4c03c291add7cc9c10b7500044f0d660d053280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 22 May 2018 04:11:42 +0200 Subject: [PATCH 046/868] Add NonZero hack (remove once NonZero is stabilized) This hack saves quite a bit of memory. It uses the fact that Rust optimizes an Option<&T> to be the same size as a &T (8 bytes), since Rust knows internally that pointers can never be 0x0 / NULL. The `NonZeroUsizeHack` provides a safe interface to this by adding 1 to the index on creation and removing 1 on retrieval - the IDs are still the same, but now the `NodeId` is only 8 bytes large instead of 16 bytes. --- src/cache.rs | 6 ++-- src/id_tree.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index de7ee35dd..4181ef3ce 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -107,6 +107,8 @@ impl DomTreeCache { fn update_tree_inner_2(previous_arena: &Arena, next_arena: &Arena) -> DomChangeSet { + use id_tree::NonZeroUsizeHack; + let mut previous_iter = previous_arena.nodes.iter(); let mut next_iter = next_arena.nodes.iter().enumerate(); let mut changeset = DomChangeSet::empty(); @@ -114,11 +116,11 @@ impl DomTreeCache { while let Some((next_idx, next_hash)) = next_iter.next() { if let Some(old_hash) = previous_iter.next() { if old_hash.data != next_hash.data { - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); + changeset.added_nodes.insert(NodeId { index: NonZeroUsizeHack::new(next_idx) }, next_hash.data); } } else { // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); + changeset.added_nodes.insert(NodeId { index: NonZeroUsizeHack::new(next_idx) }, next_hash.data); } } /* diff --git a/src/id_tree.rs b/src/id_tree.rs index 7cd79b64c..9d9e14cb0 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -5,11 +5,83 @@ use std::ops::{Index, IndexMut}; use std::fmt; use std::hash::{Hasher, Hash}; use std::collections::BTreeMap; +use std::ops::Deref; + +/// See: https://github.com/rust-lang/rust/issues/27730#issuecomment-311919692 +/// +/// This hack allows us to save some memory. Credit to @nox for inventing this. +/// Currently, rust optimizes an `Option<&T>` to be 8 bytes large instead of 16, +/// because Rust knows that pointers in Rust can never be 0 / NULL. +/// +/// The `NonZeroUsizeHack` adds 1 to a usize, then casts it to a pointer. +/// On retrieval, it casts it back to a usize and subtracts 1, to get the original value. +/// So in the end, `Option` is only 8 bytes large instead of 16, which gives +/// possibly better cache access and less memory usage at the cost of 1 or 2 extra +/// assembly instructions. +/// +/// Note that the Rust spec says that the pointer may never be null, even though it is +/// never dereferenced. +/// +/// NEVER MAKE THE INTERNAL FIELD PUBLIC, ALWAYS USE `::new()` and `.get()`! +#[derive(Copy, Clone)] +pub struct NonZeroUsizeHack(&'static ()); + +impl NonZeroUsizeHack { + #[inline] + pub fn new(value: usize) -> Self { + // Add 1 on insertion + let value = value + 1; + unsafe { NonZeroUsizeHack(&*(value as *const ())) } + } + + #[inline] + pub fn get(self) -> usize { + // Remove 1 on retrieval + let value = self.0 as *const () as usize; + assert!(value != 0); // can never happen, since we add 1 it in the new() fn + value - 1 + } +} + +use std::cmp::Ordering; + +impl PartialOrd for NonZeroUsizeHack { + fn partial_cmp(&self, other: &NonZeroUsizeHack) -> Option { + Some(self.get().cmp(&other.get())) + } +} + +impl Ord for NonZeroUsizeHack { + fn cmp(&self, other: &NonZeroUsizeHack) -> Ordering { + self.get().cmp(&other.get()) + } +} + +impl PartialEq for NonZeroUsizeHack { + fn eq(&self, other: &NonZeroUsizeHack) -> bool { + self.get() == other.get() + } +} + +impl Eq for NonZeroUsizeHack { } + +impl fmt::Debug for NonZeroUsizeHack { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + +impl Hash for NonZeroUsizeHack { + fn hash(&self, state: &mut H) { + self.get().hash(state); + } +} /// A node identifier within a particular `Arena`. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug, Hash)] pub struct NodeId { - pub(crate) index: usize, // FIXME: use NonZero to optimize the size of Option + // FIXME: Change this to NonZero once NonZero is stabilized + pub(crate) index: NonZeroUsizeHack, } #[derive(Clone, PartialEq)] @@ -108,7 +180,7 @@ impl Arena { data: data, }); NodeId { - index: next_index, + index: NonZeroUsizeHack::new(next_index), } } @@ -128,7 +200,7 @@ impl Arena { pub fn get_all_node_ids(&self) -> BTreeMap { use std::iter::FromIterator; BTreeMap::from_iter(self.nodes.iter().enumerate().map(|(i, node)| - (NodeId { index: i }, node.data) + (NodeId { index: NonZeroUsizeHack::new(i) }, node.data) )) } } @@ -136,8 +208,7 @@ impl Arena { trait GetPairMut { /// Get mutable references to two distinct nodes /// - /// Panic - /// ----- + /// ## Panic /// /// Panics if the two given IDs are the same. fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) @@ -161,13 +232,13 @@ impl Index for Arena { type Output = Node; fn index(&self, node: NodeId) -> &Node { - &self.nodes[node.index] + &self.nodes[node.index.get()] } } impl IndexMut for Arena { fn index_mut(&mut self, node: NodeId) -> &mut Node { - &mut self.nodes[node.index] + &mut self.nodes[node.index.get()] } } @@ -289,7 +360,7 @@ impl NodeId { let last_child_opt; { let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not append a node to itself"); + self.index.get(), new_child.index.get(), "Can not append a node to itself"); new_child_borrow.parent = Some(self); last_child_opt = mem::replace(&mut self_borrow.last_child, Some(new_child)); if let Some(last_child) = last_child_opt { @@ -311,7 +382,7 @@ impl NodeId { let first_child_opt; { let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index, new_child.index, "Can not prepend a node to itself"); + self.index.get(), new_child.index.get(), "Can not prepend a node to itself"); new_child_borrow.parent = Some(self); first_child_opt = mem::replace(&mut self_borrow.first_child, Some(new_child)); if let Some(first_child) = first_child_opt { @@ -334,7 +405,7 @@ impl NodeId { let parent_opt; { let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node after itself"); + self.index.get(), new_sibling.index.get(), "Can not insert a node after itself"); parent_opt = self_borrow.parent; new_sibling_borrow.parent = parent_opt; new_sibling_borrow.previous_sibling = Some(self); @@ -359,7 +430,7 @@ impl NodeId { let parent_opt; { let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index, new_sibling.index, "Can not insert a node before itself"); + self.index.get(), new_sibling.index.get(), "Can not insert a node before itself"); parent_opt = self_borrow.parent; new_sibling_borrow.parent = parent_opt; new_sibling_borrow.next_sibling = Some(self); From 7f093c341d86730d3004b730393af734b457d0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 22 May 2018 07:25:28 +0200 Subject: [PATCH 047/868] Added svg module & added compositing functions / shaders to compositor --- examples/debug.rs | 25 ++--- src/compositor.rs | 226 ++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 3 + src/svg.rs | 33 +++++++ src/window.rs | 44 ++++++--- 5 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 src/svg.rs diff --git a/examples/debug.rs b/examples/debug.rs index 08c4e4dd7..ef829cc24 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -17,29 +17,16 @@ impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { let mut dom = Dom::new(NodeType::Label { - text: String::from(/*"\ - Lorem ipsum dolor sit amet, \ - consetetur sadipscing elitr, sed diam \ - nonumy eirmod tempor invidunt ut \ - labore et dolore magna aliquyam \ - erat, sed diam voluptua. At vero eos \ - et accusam et justo duo dolores et ea \ - rebum. Stet clita kasd gubergren, no \ - sea takimata sanctus est Lorem ipsum \ - dolor sit amet. Lorem ipsum dolor sit \ - amet, consetetur sadipscing elitr, sed diam \ - nonumy eirmod tempor invidunt \ - ut labore et dolore magna aliquyam \ - \ - erat, sed diam voluptua. At vero eos \ - et accusam et justo duo dolores et ea rebum. \ - Stet clita kasd gubergren, no sea takimata \ - sanctus est Lorem ipsum dolor sit amet."*/ "Azul"), + text: String::from("Azul"), }); dom.class("__azul-native-button"); dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - // dom.add_sibling(Dom::new(NodeType::)); + for i in 0..1000 { + dom.add_sibling(Dom::new(NodeType::Label { + text: format!("{}", i), + }).with_class("__azul-native-button")) + } dom } diff --git a/src/compositor.rs b/src/compositor.rs index 4fa8db914..875dee2fa 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -2,26 +2,236 @@ //! This makes it possible to use OpenGL images in the background and compose SVG elements //! into the UI. -use glium::texture::compressed_srgb_texture2d::CompressedSrgbTexture2d; +use std::sync::{Arc, Mutex}; + +use glium::{ + Program, VertexBuffer, Display, + index::{NoIndices, PrimitiveType::TriangleStrip}, + texture::texture2d::Texture2d, + backend::Facade, +}; #[derive(Default, Debug)] pub struct Compositor { - textures: Vec + textures: Vec, } +// I'd wrap this in a `Arc>`, but this is only available on nightly +// So, for now, this is completely thread-unsafe +// +// However, this should be fine, as we initialize the program only from the main thread +// and never de-initialize it +static mut SHADER_FULL_SCREEN: Option = None; + +pub const INDICES_NO_INDICES_TRIANGLE_STRIP: NoIndices = NoIndices(TriangleStrip); + +const SIMPLE_FRAGMENT_SHADER: &'static str = "\ + #version 130 + + in vec4 v_color; + + void main() { + gl_FragColor = v_color; + } +"; + +/// Simple fragment shader that combines two textures `tex1` and `tex2`, +/// drawing `tex2` over `tex1` +/// +/// ## Inputs +/// +/// - `vec2 v_tex_coords`: The texture coordinates of the vertices +/// (see `VERTEX_SHADER_FULL_SCREEN`) +/// - `uniform sampler2d tex1`: The lower texture to be drawn (RGBA) +/// - `uniform sampler2d tex2`: The texture to draw on top (RGBA) +/// +/// ## Outputs +/// +/// - `vec4 gl_FragColor`: The color on the screen / to a different texture +const TWO_TEXTURES_FRAGMENT_SHADER: &'static str = "\ + #version 130 + + in vec2 v_tex_coords; + uniform sampler2D tex1; + uniform sampler2D tex2; + + void main() { + vec4 tex1_color = texture(tex1, v_tex_coords); + vec4 tex2_color = texture(tex2, v_tex_coords); + gl_FragColor = mix(tex1_color, tex2_color, tex2_color.a); + } +"; + +/// This is a vertex shader that should be called with a glDrawArrays(3) and no data +/// What it does is to generate a triangle that stretches over the whole screen: +/// +/// ```no_run,ignore +/// + +/// | - +/// | - +/// | - +/// +-----------+ +/// | | - +/// | screen | - +/// | | - +/// +-----------+-----------+ +/// ``` +/// +/// It also sets up the texture coordinates. So if you pair it with the +/// `TWO_TEXTURES_FRAGMENT_SHADER`, you can draw two textures on top of each other +const VERTEX_SHADER_FULL_SCREEN: &str = " + #version 140 + out vec2 v_tex_coords; + void main() { + float x = -1.0 + float((gl_VertexID & 1) << 2); + float y = -1.0 + float((gl_VertexID & 2) << 1); + v_tex_coords = vec2((x+1.0)*0.5, (y+1.0)*0.5); + gl_Position = vec4(x, y, 0, 1); + } +"; + +#[derive(Debug, Copy, Clone)] +pub struct SimpleGpuVertex { + pub coordinate: [f32; 2], + pub tex_coords: [f32; 2], +} + +implement_vertex!(SimpleGpuVertex, coordinate, tex_coords); + +pub const VERTEXBUFFER_FOR_FULL_SCREEN_QUAD: [SimpleGpuVertex;3] = [ + // top left + SimpleGpuVertex { + coordinate: [-1.0, 1.0], + tex_coords: [-1.0, 1.0], + }, + // bottom left + SimpleGpuVertex { + coordinate: [-1.0, -1.0], + tex_coords: [-1.0, -1.0], + }, + // top right + SimpleGpuVertex { + coordinate: [1.0, 1.0], + tex_coords: [1.0, 1.0], + }, +]; + impl Compositor { pub fn new() -> Self { Self::default() } - pub fn push_texture(&mut self, texture: CompressedSrgbTexture2d) { + pub fn push_texture(&mut self, texture: Texture2d) { self.textures.push(texture); } -/* - /// Draw all textures onto a final texture, which can then be displayed on the screen - pub fn compose(self) -> CompressedSrgbTexture2d { + /// Combine all texture together. Returns None if there are no textures in `self.textures`. + pub fn combine_all_textures(&self, display: &T, sample_behaviour: SampleBehaviour) + -> Option + { + // lazily initialize shader + if unsafe { SHADER_FULL_SCREEN.is_none() } { + unsafe { SHADER_FULL_SCREEN = Some(CombineTwoTexturesProgram::new(display)) }; + } + + let shader = unsafe { SHADER_FULL_SCREEN.as_ref().unwrap() }; + let mut iter = self.textures.iter().skip(1); + + let mut initial_tex: Texture2d = match self.textures.get(0) { + Some(tex) => { + // TODO: this could be optimized + let (w, h) = (tex.width(), tex.height()); + Texture2d::empty(display, w, h).unwrap() + }, + None => return None, + }; + + while let Some(tex2) = iter.next() { + let combined = shader.draw(display, &initial_tex, tex2, sample_behaviour); + initial_tex = combined; + } + Some(initial_tex) } -*/ -} \ No newline at end of file +} + +#[derive(Debug)] +pub struct CombineTwoTexturesProgram { + program: Program, + vertex_buffer: VertexBuffer, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum SampleBehaviour { + GlNearest, + GlLinear, +} + +impl CombineTwoTexturesProgram { + + /// Uses `VERTEX_SHADER_FULL_SCREEN`, `TWO_TEXTURES_FRAGMENT_SHADER` + /// and `VERTEXBUFFER_FOR_FULL_SCREEN_QUAD`. + pub fn new(display: &T) -> Self { + let program = Program::from_source(display, VERTEX_SHADER_FULL_SCREEN, TWO_TEXTURES_FRAGMENT_SHADER, None).unwrap(); + let vertex_buf = VertexBuffer::new(display, &VERTEXBUFFER_FOR_FULL_SCREEN_QUAD).unwrap(); + Self { + program: program, + vertex_buffer: vertex_buf, + } + } + + /// Draw tex2 over tex1, returns a new texture with the combined result + /// + /// NOTE: `sample_behaviour`: specify if using `GL_NEAREST` or `GL_LINEAR` + /// for blending + pub fn draw( + &self, + display: &T, + tex1: &Texture2d, + tex2: &Texture2d, + sample_behaviour: SampleBehaviour) + -> Texture2d + { + use self::SampleBehaviour::*; + use glium::{ + Surface, + uniforms::{MagnifySamplerFilter, MinifySamplerFilter}, + texture::{CompressedSrgbFormat, CompressedMipmapsOption}, + }; + + let max_width = tex1.width().max(tex2.width()); + let max_height = tex1.height().max(tex2.height()); + + let (tex1, tex2) = match sample_behaviour { + GlNearest => { + (tex1.sampled() + .magnify_filter(MagnifySamplerFilter::Nearest) + .minify_filter(MinifySamplerFilter::LinearMipmapNearest), + tex2.sampled() + .magnify_filter(MagnifySamplerFilter::Nearest) + .minify_filter(MinifySamplerFilter::LinearMipmapNearest)) + }, + GlLinear => { + (tex1.sampled(), + tex2.sampled()) + } + }; + + let uniforms = uniform! { + tex1: tex1, + tex2: tex2, + }; + + let target = Texture2d::empty(display, max_width, max_height).unwrap(); + + target.as_surface().draw( + &self.vertex_buffer, + &INDICES_NO_INDICES_TRIANGLE_STRIP, + &self.program, + &uniforms, + &Default::default()).unwrap(); + + target + } +} + diff --git a/src/lib.rs b/src/lib.rs index eef3fa28a..f3edfad76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ #![allow(dead_code)] #![allow(unused_imports)] +#[macro_use] pub extern crate glium; pub extern crate gleam; pub extern crate euclid; @@ -88,6 +89,8 @@ mod compositor; mod platform_ext; /// Async IO / task system mod task; +/// SVG / path flattering module (lyon) +mod svg; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; diff --git a/src/svg.rs b/src/svg.rs new file mode 100644 index 000000000..a7cb03fd5 --- /dev/null +++ b/src/svg.rs @@ -0,0 +1,33 @@ +use dom::Callback; +use image::Rgb; +use traits::LayoutScreen; + +pub struct Svg { + pub layers: Vec>, +} + +pub struct SvgLayer { + pub id: String, + pub data: Vec, + pub callbacks: SvgCallbacks, + pub style: SvgStyle, +} + +pub enum SvgCallbacks { + // No callbacks for this layer + None, + /// Call the callback on any of the items + Any(Callback), + /// Call the callback when the SvgLayer item at index [x] is + /// hovered over / interacted with + Some(Vec<(usize, Callback)>), +} + +pub struct SvgStyle { + outline: Option>, + fill: Option>, +} + +pub enum SvgShape { + Polygon(Vec<(f32, f32)>), +} \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 05652b75f..8e4140760 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,24 +1,32 @@ -use webrender::api::*; -use webrender::{Renderer, RendererOptions, RendererKind}; -// use webrender::renderer::RendererError; -use glium::{IncompatibleOpenGl, Display}; -use glium::debug::DebugCallbackBehavior; -use glium::glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, - MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}; +//! Window creation module + +use std::{time::Duration, fmt}; + +use webrender::{ + api::*, + Renderer, RendererOptions, RendererKind, + // renderer::RendererError; -- not currently public in WebRender +}; +use glium::{ + IncompatibleOpenGl, Display, + debug::DebugCallbackBehavior, + glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, + MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, + backend::glutin::DisplayCreationError, +}; use gleam::gl; -use glium::backend::glutin::DisplayCreationError; use euclid::TypedScale; -use cassowary::{Variable, Solver}; -use cassowary::strength::*; +use cassowary::{ + Variable, Solver, + strength::*, +}; use display_list::SolvedLayout; use traits::LayoutScreen; use css::Css; use cache::{EditVariableCache, DomTreeCache}; use id_tree::NodeId; - -use std::time::Duration; -use std::fmt; +use compositor::Compositor; const DEFAULT_TITLE: &str = "Azul App"; const DEFAULT_WIDTH: u32 = 800; @@ -366,10 +374,17 @@ impl Default for WindowMonitorTarget { pub struct Window { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, + // TODO: Migrate to the window_state for state diffing pub(crate) options: WindowCreateOptions, + /// The webrender renderer pub(crate) renderer: Option, + /// The display, i.e. the window pub(crate) display: Display, + /// The `WindowInternal` allows us to solve some borrowing issues pub(crate) internal: WindowInternal, + /// The compositor caches and stores OpenGL textures, so that we can + /// render custom elements behind the UI if needed. + pub(crate) compositor: Compositor, /// The solver for the UI, for caching the results of the computations pub(crate) solver: UiSolver, // The background thread that is running for this window. @@ -588,11 +603,14 @@ impl Window { solver.suggest_value(window_dim.width_var, window_dim.width() as f64).unwrap(); solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); + let compositor = Compositor::new(); + let window = Window { events_loop: events_loop, options: options, renderer: Some(renderer), display: display, + compositor: compositor, css: css, internal: WindowInternal { layout_size: layout_size, From 0d242e14a204b837d29954f3acf3582741789bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 22 May 2018 10:36:08 +0200 Subject: [PATCH 048/868] Fixed point / pixel confusion + rusttype scaling bug, upgraded webrender --- Cargo.toml | 5 +-- examples/debug.rs | 6 ---- examples/test_content.css | 3 +- src/app.rs | 5 +-- src/compositor.rs | 19 ++++++++++++ src/css_parser.rs | 4 +-- src/display_list.rs | 65 +++++++++++++++------------------------ src/text_layout.rs | 46 ++++++++++++++------------- src/window.rs | 2 +- 9 files changed, 77 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2121396ef..243044a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,13 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/fschutt/webrender", branch = "fix_dpi" } +# webrender = { git = "https://github.com/fschutt/webrender", branch = "fix_dpi" } +webrender = { git = "https://github.com/servo/webrender", rev = "bb354abbf84602d3d8357c63c4f0b1139ec4deb1" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" glium = { git = "https://github.com/fschutt/glium", branch = "mapedit-winit-windows-ext" } -gleam = "0.4.20" +gleam = "0.5" euclid = "0.17" image = "0.19.0" rusttype = "0.5.2" diff --git a/examples/debug.rs b/examples/debug.rs index ef829cc24..6370e08c9 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -22,12 +22,6 @@ impl LayoutScreen for MyAppData { dom.class("__azul-native-button"); dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - for i in 0..1000 { - dom.add_sibling(Dom::new(NodeType::Label { - text: format!("{}", i), - }).with_class("__azul-native-button")) - } - dom } } diff --git a/examples/test_content.css b/examples/test_content.css index 3ab9e4b38..7b5721950 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -6,7 +6,6 @@ box-shadow: 0px 0px 3px #c5c5c5ad; /*background: image("Cat01");*/ background: linear-gradient(#fcfcfc, #efefef); - text-align: right; width: 200px; height: 200px; min-height: 400px; @@ -19,7 +18,7 @@ } * { - font-size: 10px; + font-size: 16px; font-family: "Webly Sleeky UI", sans-serif; color: black; } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 4a3680095..a7db01600 100644 --- a/src/app.rs +++ b/src/app.rs @@ -383,9 +383,7 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { frame_event_info.should_hittest = true; frame_event_info.cur_cursor_pos = position; - let _ = window_id; - let _ = device_id; - let _ = modifiers; + let (_, _, _) = (window_id, device_id, modifiers); }, WindowEvent::Resized(w, h) => { frame_event_info.new_window_size = Some((w, h)); @@ -438,7 +436,6 @@ fn render( let resources = ResourceUpdates::new(); let mut txn = Transaction::new(); - // TODO: something is wrong, the redraw times increase, even if the same display list is redrawn txn.set_display_list( window.internal.epoch, None, diff --git a/src/compositor.rs b/src/compositor.rs index 875dee2fa..1a580bf3a 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -10,6 +10,7 @@ use glium::{ texture::texture2d::Texture2d, backend::Facade, }; +use webrender::ExternalImage; #[derive(Default, Debug)] pub struct Compositor { @@ -235,3 +236,21 @@ impl CombineTwoTexturesProgram { } } +/* +impl ExternalImageHandler for Compositor { + // Do not perform any actual locking since rendering happens on the main thread + fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> webrender::ExternalImage { + let descriptor = resources::resources().image_loader.texture_descriptors[&key.0]; + ExternalImage { + uv: TexelRect { + uv0: TypedPoint2D::zero(), + uv1: TypedPoint2D::::new(descriptor.width as f32, descriptor.height as f32), + }, + source: webrender::ExternalImageSource::NativeTexture(key.0 as _), + } + } + + fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { + } +} +*/ \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index 6ef17da65..bc61f1035 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -3,7 +3,7 @@ use webrender::api::{ColorU, BorderRadius, LayoutVector2D, LayoutPoint, ColorF, BoxShadowClipMode, LayoutSize, BorderStyle, BorderDetails, BorderSide, NormalBorder, BorderWidths, - ExtendMode, LayoutRect, LayerPixel}; + ExtendMode, LayoutRect, LayoutPixel}; use std::num::{ParseIntError, ParseFloatError}; use euclid::{TypedRotation2D, Angle, TypedPoint2D}; @@ -786,7 +786,7 @@ impl DirectionCorner { } } - pub fn to_point(&self, rect: &LayoutRect) -> TypedPoint2D + pub fn to_point(&self, rect: &LayoutRect) -> TypedPoint2D { use self::DirectionCorner::*; match *self { diff --git a/src/display_list.rs b/src/display_list.rs index 3a824716d..b43f85aa2 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -279,17 +279,27 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { tag: rect.tag.and_then(|tag| Some((tag, 0))), }; + let clip_region_id = rect.style.border_radius.and_then(|border_radius| { + let region = ComplexClipRegion { + rect: bounds, + radii: border_radius, + mode: ClipMode::Clip, + }; + Some(builder.define_clip(bounds, vec![region], None)) + }); + // TODO: expose 3D-transform in CSS // TODO: expose blend-modes in CSS // TODO: expose filters (blur, hue, etc.) in CSS builder.push_stacking_context( &info, - ScrollPolicy::Scrollable, + clip_region_id, None, - TransformStyle::Preserve3D, + TransformStyle::Flat, None, MixBlendMode::Normal, - Vec::new() + Vec::new(), + GlyphRasterSpace::Screen, ); // Push the "outset" box shadow, before the clip is active @@ -300,15 +310,6 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { &full_screen_rect, BoxShadowClipMode::Outset); - let clip_region_id = rect.style.border_radius.and_then(|border_radius| { - let region = ComplexClipRegion { - rect: bounds, - radii: border_radius, - mode: ClipMode::Clip, - }; - Some(builder.define_clip(bounds, vec![region], None)) - }); - // Push clip if let Some(id) = clip_region_id { builder.push_clip_id(id); @@ -364,7 +365,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { } #[inline] -fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { +fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { match style.background_color { Some(bg) => builder.push_rect(&info, bg.into()), None => builder.push_clear_rect(&info), @@ -373,14 +374,14 @@ fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, #[inline] fn push_text( - info: &PrimitiveInfo, + info: &PrimitiveInfo, display_list: &DisplayList, rect_idx: NodeId, builder: &mut DisplayListBuilder, style: &RectStyle, app_resources: &mut AppResources, render_api: &RenderApi, - bounds: &TypedRect, + bounds: &TypedRect, resource_updates: &mut ResourceUpdates) { use dom::NodeType::*; @@ -414,46 +415,30 @@ fn push_text( None => return, }; - // TODO: border let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size = Length::new(font_size.0.to_pixels()); let font_size_app_units = (font_size.0 as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); - let font_size_app_units = Au(font_size_app_units as i32); - let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); + let font_size_app_units = Au(font_size_app_units as i32); // * text_layout::WEBRENDER_DPI_HACK) as i32 + let font_result = push_font(font_id, font_size_app_units, + resource_updates, app_resources, render_api); let font_instance_key = match font_result { Some(f) => f, None => return, }; - // The font_size_adjustment_hack is a hack to make horizontal spacing work correctly - // For some reason, rusttype doesn't return the correct horizontal spacing for characters - let font_size_adjustment_hack = { - let font = &app_resources.font_data[font_id].0; - let v_metrics = font.v_metrics_unscaled(); - let v_scale_factor = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap) / - font.units_per_em() as f32; - 1.0 + ((v_scale_factor - 1.0) * 2.0) - }; - let font = &app_resources.font_data[font_id].0; let alignment = style.text_align.unwrap_or(TextAlignment::default()); let overflow_behaviour = style.text_overflow.unwrap_or(TextOverflowBehaviour::default()); let positioned_glyphs = text_layout::put_text_in_bounds( - text, font, font_size * font_size_adjustment_hack, alignment, overflow_behaviour, bounds); - - // TODO: webrender doesn't respect the DPI of the monitor its on: - // - // See: https://github.com/servo/webrender/pull/2597 - // and: https://github.com/servo/webrender/issues/2596 + text, font, font_size, alignment, overflow_behaviour, bounds); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); let flags = FontInstanceFlags::SUBPIXEL_BGR; let options = GlyphOptions { render_mode: FontRenderMode::Subpixel, flags: flags, - dpi: Some(96), }; builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); } @@ -467,8 +452,8 @@ fn push_text( fn push_box_shadow( builder: &mut DisplayListBuilder, style: &RectStyle, - bounds: &TypedRect, - full_screen_rect: &TypedRect, + bounds: &TypedRect, + full_screen_rect: &TypedRect, shadow_type: BoxShadowClipMode) { let pre_shadow = match style.box_shadow { @@ -512,8 +497,8 @@ fn push_box_shadow( #[inline] fn push_background( - info: &PrimitiveInfo, - bounds: &TypedRect, + info: &PrimitiveInfo, + bounds: &TypedRect, builder: &mut DisplayListBuilder, style: &RectStyle, app_resources: &AppResources) @@ -567,7 +552,7 @@ fn push_background( #[inline] fn push_border( - info: &PrimitiveInfo, + info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { diff --git a/src/text_layout.rs b/src/text_layout.rs index aacf7a2dc..f8960b5ef 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -5,18 +5,25 @@ use euclid::{Length, TypedRect, TypedPoint2D}; use rusttype::{Font, Scale}; use css_parser::{TextAlignment, TextOverflowBehaviour}; +/// Webrender measures in points, not in pixels! +pub const PT_TO_PX: f32 = 96.0 / 72.0; + +pub const PX_TO_PT: f32 = 72.0 / 96.0; + +/// Rusttype has a certain sizing hack, I have no idea where this number comes from +/// Without this adjustment, we won't have the correct horizontal spacing +const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; + /// Lines is responsible for layouting the lines of the rectangle to struct Lines<'a> { align: TextAlignment, max_lines_before_overflow: usize, - line_height: Length, - max_horizontal_width: Length, + line_height: Length, + max_horizontal_width: Length, font: &'a Font<'a>, font_size: Scale, - origin: TypedPoint2D, + origin: TypedPoint2D, current_line: usize, - v_advance: f32, - v_scale_factor: f32, } #[derive(Debug)] @@ -43,17 +50,14 @@ struct HarfbuzzAdjustment(pub f32); impl<'a> Lines<'a> { pub(crate) fn from_bounds( - bounds: &TypedRect, + bounds: &TypedRect, alignment: TextAlignment, font: &'a Font<'a>, - font_size: Length) + font_size: Length) -> Self { let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; let max_horizontal_width = Length::new(bounds.size.width); - let v_metrics = font.v_metrics_unscaled(); - let v_advance = v_metrics.ascent - v_metrics.descent + v_metrics.line_gap; - let v_scale_factor = v_advance / font.units_per_em() as f32; Self { align: alignment, @@ -64,8 +68,6 @@ impl<'a> Lines<'a> { max_horizontal_width: max_horizontal_width, font_size: Scale::uniform(font_size.0), current_line: 0, - v_scale_factor: v_scale_factor, - v_advance: v_advance, } } @@ -89,7 +91,7 @@ impl<'a> Lines<'a> { let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text); // (3) Split the text into words - let words = split_text_into_words(&text, font, self.font_size); + let words = split_text_into_words(&text, font, font_size); // (4) Align text to the left let (mut positioned_glyphs, line_break_offsets) = words_to_left_aligned_glyphs(words, font, font_size, max_horizontal_width, max_lines_before_overflow); @@ -125,7 +127,7 @@ fn normalize_unicode_characters(text: &str) -> String { // (should it be done after split_text_into_words?) // TODO: THis is an expensive operation! use unicode_normalization::UnicodeNormalization; - text.nfc().collect::() + text.nfc().filter(|c| !c.is_control()).collect::() } #[inline] @@ -184,11 +186,16 @@ fn split_text_into_words<'a>(text: &'a str, font: &Font<'a>, font_size: Scale) - let mut last_glyph = None; for c in word.chars() { + use rusttype::Point; let g = font.glyph(c).scaled(font_size); let id = g.id(); + if c.is_control() { + continue; + } + if let Some(last) = last_glyph { caret += font.pair_kerning(font_size, last, g.id()); } @@ -238,10 +245,7 @@ fn words_to_left_aligned_glyphs<'a>( let v_metrics_scaled = font.v_metrics(font_size); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - // TODO: This is one hack because webrender locks fonts at 76 DPI - // and doesn't scale them correctly - // HORRIBLE WEBRENDER HACK! - let offset_top = font_size.y * 3.0 / 4.0; + let offset_top = v_metrics_scaled.ascent; // In order to space between words, we need to let space_width = font.glyph(' ').scaled(font_size).h_metrics().advance_width; @@ -359,13 +363,13 @@ fn align_text(alignment: TextAlignment, glyphs: &mut Vec, line_br pub(crate) fn put_text_in_bounds<'a>( text: &str, font: &Font<'a>, - font_size: Length, + font_size: Length, alignment: TextAlignment, overflow_behaviour: TextOverflowBehaviour, - bounds: &TypedRect) + bounds: &TypedRect) -> Vec { - let mut lines = Lines::from_bounds(bounds, alignment, font, font_size); + let mut lines = Lines::from_bounds(bounds, alignment, font, font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT); let (glyphs, overflow) = lines.get_glyphs(text, overflow_behaviour); glyphs } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 8e4140760..69037bc66 100644 --- a/src/window.rs +++ b/src/window.rs @@ -328,7 +328,7 @@ impl RenderNotifier for Notifier { self.events_loop_proxy.wakeup().unwrap_or_else(|_| { }); } - fn new_document_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool) { + fn new_frame_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool) { self.wake_up(); } } From 1a24d74b5a93611ba253b64e9d1325599e41028a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 23 May 2018 11:06:00 +0200 Subject: [PATCH 049/868] Initial work on getting the Svg component into a NodeType We can't directly put the Svg component into a NodeType, because we need to compare / diff the nodes. Comparing large amounts of data takes too much time, so instead we can simply make a "registry" system where users of the library can provide IDs for BLOB data - essentially implementing the Svg like the images handling. --- Cargo.toml | 3 +- src/dom.rs | 63 ++++++++++++++++++++++----------- src/lib.rs | 4 +++ src/svg.rs | 87 ++++++++++++++++++++++++++++++++++++++++++++-- src/text_layout.rs | 19 +++------- 5 files changed, 137 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 243044a01..6a1e80519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -# webrender = { git = "https://github.com/fschutt/webrender", branch = "fix_dpi" } webrender = { git = "https://github.com/servo/webrender", rev = "bb354abbf84602d3d8357c63c4f0b1139ec4deb1" } cassowary = "0.3.0" simplecss = "0.1.0" @@ -17,6 +16,8 @@ rusttype = "0.5.2" app_units = "0.6" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" +resvg = "0.2.0" +lyon = { version = "0.10.0", features = ["extra"] } [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/dom.rs b/src/dom.rs index 8a239312c..f491a82eb 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -8,6 +8,8 @@ use std::rc::Rc; use std::cell::RefCell; use std::hash::{Hash, Hasher}; use webrender::api::ColorU; +use glium::Texture2d; +use svg::Svg; /// This is only accessed from the main thread, so it's safe to use pub(crate) static mut NODE_ID: u64 = 0; @@ -101,7 +103,7 @@ impl Copy for Callback { } /// wrapper around a repeated (`Div` + `Label`) clone where the first /// `Div` is shaped like a circle (for `Ul`). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum NodeType { +pub enum NodeType /**/ { /// Regular div Div, /// Image: The actual contents of the image are determined by the CSS @@ -162,7 +164,16 @@ pub enum NodeType { placeholder: Option, use_dots: bool, }, + // Custom drawing component + //CustomDrawComponent(DrawComponent), } +/* +/// State of a checkbox (disabled, checked, etc.) +#[derive(Debug, Clone, Hash)] +pub enum DrawComponent { + Svg(Svg), +} +*/ /// State of a checkbox (disabled, checked, etc.) #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -183,29 +194,31 @@ pub enum CheckboxState { Unchecked } -impl NodeType { +impl /**/ NodeType /**/ { /// Get the CSS / HTML identifier "p", "ul", "li", etc. /// /// Full list of the types you can use in CSS: /// /// ```ignore - /// Div => "div" - /// Image => "img" - /// Button => "button" - /// Ul => "ul" - /// Ol => "ol" - /// Li => "li" - /// Label => "label" - /// Form => "form" - /// TextInput => "text-input" - /// TextEdit => "text-edit" - /// Tab => "tab" - /// Checkbox => "checkbox" - /// Color => "color" - /// Drowdown => "dropdown" - /// ToolTip => "tooltip" - /// Password => "password" + /// Div => "div" + /// Image => "img" + /// Button => "button" + /// Ul => "ul" + /// Ol => "ol" + /// Li => "li" + /// Label => "label" + /// Form => "form" + /// TextInput => "text-input" + /// TextEdit => "text-edit" + /// Tab => "tab" + /// Checkbox => "checkbox" + /// Color => "color" + /// Drowdown => "dropdown" + /// ToolTip => "tooltip" + /// Password => "password" + /// CustomDrawComponent::Svg => "svg" + /// CustomDrawComponent::GlTexture => "gltexture" /// ``` pub fn get_css_identifier(&self) -> &'static str { use self::NodeType::*; @@ -225,6 +238,14 @@ impl NodeType { Dropdown { .. } => "dropdown", ToolTip { .. } => "tooltip", Password { .. } => "password", + /* + CustomDrawComponent(c) => { + match c { + DrawComponent::Svg(_) => "svg", + DrawComponent::GlTexture(_) => "gltexture", + } + } + */ } } } @@ -248,7 +269,7 @@ pub enum On { #[derive(PartialEq, Eq)] pub(crate) struct NodeData { /// `div` - pub node_type: NodeType, + pub node_type: NodeType/**/, /// `#main` pub id: Option, /// `.myclass .otherclass` @@ -317,7 +338,7 @@ impl CallbackList { impl NodeData { /// Creates a new NodeData - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType/**/) -> Self { Self { node_type: node_type, id: None, @@ -395,7 +416,7 @@ impl Dom { /// Creates an empty DOM #[inline] - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType/**/) -> Self { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new(node_type)); Self { diff --git a/src/lib.rs b/src/lib.rs index f3edfad76..6f0a89f5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,12 +24,16 @@ #![allow(dead_code)] #![allow(unused_imports)] +#![windows_subsystem = "windows"] + #[macro_use] pub extern crate glium; pub extern crate gleam; pub extern crate euclid; pub extern crate image; +extern crate resvg; +extern crate lyon; extern crate webrender; extern crate cassowary; extern crate twox_hash; diff --git a/src/svg.rs b/src/svg.rs index a7cb03fd5..a016ac498 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,11 +1,19 @@ use dom::Callback; -use image::Rgb; +use image::Rgba; use traits::LayoutScreen; +use std::sync::atomic::AtomicUsize; +/// In order to store / compare SVG files, we have to +pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); + + + +#[derive(Debug, Clone)] pub struct Svg { pub layers: Vec>, } +#[derive(Debug, Clone)] pub struct SvgLayer { pub id: String, pub data: Vec, @@ -13,6 +21,7 @@ pub struct SvgLayer { pub style: SvgStyle, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SvgCallbacks { // No callbacks for this layer None, @@ -23,11 +32,83 @@ pub enum SvgCallbacks { Some(Vec<(usize, Callback)>), } +#[derive(Debug, Copy, Clone, PartialEq)] pub struct SvgStyle { - outline: Option>, - fill: Option>, + outline: Option>, + fill: Option>, } +#[derive(Debug, Clone, PartialEq)] pub enum SvgShape { Polygon(Vec<(f32, f32)>), +} + + +impl Svg { + pub fn default_testing() -> Self { + Self { + layers: vec![SvgLayer { + id: String::from("svg-layer-01"), + // simple triangle for testing + data: vec![SvgShape::Polygon(vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)])], + callbacks: SvgCallbacks::None, + style: SvgStyle { + outline: Some(Rgba { data: [0.0, 0.0, 0.0, 1.0] }), + fill: Some(Rgba { data: [1.0, 0.0, 0.0, 1.0] }), + } + }] + } + } +} + +mod resvg_to_lyon { + + use resvg::tree::{self, Color, Paint, Stroke, PathSegment}; + + use lyon::{ + path::PathEvent, + tessellation::{self, StrokeOptions}, + math::Point, + }; + + fn point(x: f64, y: f64) -> Point { + Point::new(x as f32, y as f32) + } + + pub const FALLBACK_COLOR: Color = Color { red: 0, green: 0, blue: 0 }; + + pub(super) fn as_event(ps: &PathSegment) -> PathEvent { + match *ps { + PathSegment::MoveTo { x, y } => PathEvent::MoveTo(point(x, y)), + PathSegment::LineTo { x, y } => PathEvent::LineTo(point(x, y)), + PathSegment::CurveTo { x1, y1, x2, y2, x, y, } => { + PathEvent::CubicTo(point(x1, y1), point(x2, y2), point(x, y)) + } + PathSegment::ClosePath => PathEvent::Close, + } + } + + pub(super) fn convert_stroke(s: &Stroke) -> (Color, StrokeOptions) { + let color = match s.paint { + Paint::Color(c) => c, + _ => FALLBACK_COLOR, + }; + let linecap = match s.linecap { + tree::LineCap::Butt => tessellation::LineCap::Butt, + tree::LineCap::Square => tessellation::LineCap::Square, + tree::LineCap::Round => tessellation::LineCap::Round, + }; + let linejoin = match s.linejoin { + tree::LineJoin::Miter => tessellation::LineJoin::Miter, + tree::LineJoin::Bevel => tessellation::LineJoin::Bevel, + tree::LineJoin::Round => tessellation::LineJoin::Round, + }; + + let opt = StrokeOptions::tolerance(0.01) + .with_line_width(s.width as f32) + .with_line_cap(linecap) + .with_line_join(linejoin); + + (color, opt) + } } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index f8960b5ef..9feead521 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -88,7 +88,7 @@ impl<'a> Lines<'a> { let text = normalize_unicode_characters(text); // (2) Harfbuzz pass, for getting glyph-individual character shaping offsets - let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text); + let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); // (3) Split the text into words let words = split_text_into_words(&text, font, font_size); @@ -131,7 +131,7 @@ fn normalize_unicode_characters(text: &str) -> String { } #[inline] -fn calculate_harfbuzz_adjustments(text: &str) -> Vec { +fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) -> Vec { use harfbuzz_rs::*; use harfbuzz_rs::rusttype::SetRustTypeFuncs; @@ -141,24 +141,15 @@ fn calculate_harfbuzz_adjustments(text: &str) -> Vec { let face = Face::from_file(path, index).unwrap(); let mut font = Font::new(face); - // Use RustType as provider for font information that harfbuzz needs. - // You can also use a custom font implementation. For more information look - // at the documentation for `FontFuncs`. font.set_rusttype_funcs(); + let output = UnicodeBuffer::new().add_str(text).shape(&font, &[]); let positions = output.get_glyph_positions(); let infos = output.get_glyph_infos(); - // iterate over the shaped glyphs for (position, info) in positions.iter().zip(infos) { - let gid = info.codepoint; - let cluster = info.cluster; - let x_advance = position.x_advance; - let x_offset = position.x_offset; - let y_offset = position.y_offset; - - // Here you would usually draw the glyphs. - println!("gid{:?}={:?}@{:?},{:?}+{:?}", gid, cluster, x_advance, x_offset, y_offset); + println!("gid: {:?}, cluster: {:?}, x_advance: {:?}, x_offset: {:?}, y_offset: {:?}", + info.codepoint, info.cluster, position.x_advance, position.x_offset, position.y_offset); } */ Vec::new() // TODO From d9517014281850ba70304f7b42b5a0095a22a77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 23 May 2018 11:17:49 +0200 Subject: [PATCH 050/868] Added layout tracing function for width / height constraints --- src/display_list.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/display_list.rs b/src/display_list.rs index b43f85aa2..aefe5df68 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -711,4 +711,84 @@ fn css_constraints_to_cassowary_constraints(rect: &DisplayRect, css: &Vec Arena> { + + /// Recursive algorithm for getting the dimensions of a rectangle + /// + /// This function can be used on any rectangle to get the maximum allowed width + /// (for inserting the width / height constraint into the layout solver). + /// It simply traverses upwards through the nodes, until it finds a matching min-width / width + /// constraint, if it finds none, it will return the width of the root node. + fn get_wh_for_rectangle(&self, id: NodeId, field: WidthOrHeight) -> Option { + + use self::WidthOrHeight::*; + + let node = &self[id]; + + macro_rules! get_wh { + ($field_name:ident, $min_field:ident) => ({ + let mut $field_name: Option = None; + + match node.data.layout.$min_field { + Some(m_w) => { + let m_w_px = m_w.0.to_pixels(); + match node.data.layout.$field_name { + Some(w) => { + // width + min_width + let w_px = w.0.to_pixels(); + $field_name = Some(m_w_px.max(w_px)); + }, + None => { + // min_width + $field_name = Some(m_w_px); + } + } + }, + None => { + match node.data.layout.$field_name { + Some(w) => { + // width + let w_px = w.0.to_pixels(); + $field_name = Some(w_px); + }, + None => { + // neither width nor min_width + } + } + } + }; + + if $field_name.is_none() { + match node.parent() { + Some(p) => $field_name = self.get_wh_for_rectangle(p, field), + None => { }, + } + } + + $field_name + }) + } + + match field { + Width => { + let w = get_wh!(width, min_width); + w + }, + Height => { + let h = get_wh!(height, min_height); + h + } + } + } } \ No newline at end of file From 896c8a42684fa74629cf11630bfefdc4d02c3890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 24 May 2018 10:47:01 +0200 Subject: [PATCH 051/868] Refactor the DisplayList.rectangles so that we have the NodeId / DOM information in the layout step Now the `get_wh_for_rectangle()` actually works! It outputs 400, which is correct - the rectangle has a width of 200, but a min-width of 400, so the min-width takes precedence. For traversing the DOM, we need information about parent / ancestor, etc. in the layout step. Added LinearIterator for iterating the Arena linearly (without DOM information, just iterate the inner Vec). --- examples/test_content.css | 1 + src/cache.rs | 4 +-- src/display_list.rs | 54 ++++++++++++++++++----------- src/dom.rs | 6 +++- src/id_tree.rs | 72 +++++++++++++++++++++++++++++++-------- src/traits.rs | 13 +++---- src/ui_description.rs | 31 +++++++++-------- 7 files changed, 123 insertions(+), 58 deletions(-) diff --git a/examples/test_content.css b/examples/test_content.css index 7b5721950..44d0fea4c 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -9,6 +9,7 @@ width: 200px; height: 200px; min-height: 400px; + text-align: center; min-width: 400px; flex-direction: row; flex-wrap: nowrap; diff --git a/src/cache.rs b/src/cache.rs index 4181ef3ce..c48e955c8 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -89,7 +89,7 @@ impl DomTreeCache { if let Some(previous_root) = self.previous_layout.root { // let mut changeset = DomChangeSet::empty(); - let new_tree = new_nodes_arena.transform(|data| data.calculate_node_data_hash()); + let new_tree = new_nodes_arena.transform(|data, _| data.calculate_node_data_hash()); // Self::update_tree_inner(previous_root, &self.previous_layout.arena, new_root, &new_nodes_arena, &mut changeset); let changeset = Self::update_tree_inner_2(&self.previous_layout.arena, &new_tree); self.previous_layout.arena = new_tree; @@ -97,7 +97,7 @@ impl DomTreeCache { } else { // initialize arena use std::iter::FromIterator; - self.previous_layout.arena = new_nodes_arena.transform(|data| data.calculate_node_data_hash()); + self.previous_layout.arena = new_nodes_arena.transform(|data, _| data.calculate_node_data_hash()); self.previous_layout.root = Some(new_root); DomChangeSet { added_nodes: self.previous_layout.arena.get_all_node_ids(), diff --git a/src/display_list.rs b/src/display_list.rs index aefe5df68..8d0ef3f3f 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -25,7 +25,7 @@ const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("san pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) ui_descr: &'a UiDescription, - pub(crate) rectangles: BTreeMap> + pub(crate) rectangles: Arena> } #[derive(Debug)] @@ -80,20 +80,25 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { /// layout. The layout is done only in the `into_display_list_builder` step. pub fn new_from_ui_description(ui_description: &'a UiDescription) -> Self { - let arena = &ui_description.ui_descr_arena; - - let mut rect_btree = BTreeMap::new(); - - for node in &ui_description.styled_nodes { - let mut rect = DisplayRectangle::new(arena.borrow()[node.id].data.tag, &node); + let arena = ui_description.ui_descr_arena.borrow(); + let display_rect_arena = arena.transform(|node, node_id| { + let style = ui_description.styled_nodes.get(&node_id).unwrap_or(&ui_description.default_style_of_node); + let mut rect = DisplayRectangle::new(node.tag, style); + parse_css_style_properties(&mut rect); + parse_css_layout_properties(&mut rect); + rect + }); +/* + for node in ui_description.styled_nodes { + let mut rect = DisplayRectangle::new(arena[node.id].data.tag, &node); parse_css_style_properties(&mut rect); parse_css_layout_properties(&mut rect); rect_btree.insert(node.id, rect); } - +*/ Self { ui_descr: ui_description, - rectangles: rect_btree, + rectangles: display_rect_arena, } } @@ -221,11 +226,12 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { if css.needs_relayout { // constraints were added or removed during the last frame - for (rect_idx, rect) in self.rectangles.iter() { + for rect_idx in self.rectangles.linear_iter() { + let rect = &self.rectangles[rect_idx].data; let arena = &*self.ui_descr.ui_descr_arena.borrow(); - let dom_hash = &ui_solver.dom_tree_cache.previous_layout.arena[*rect_idx]; + let dom_hash = &ui_solver.dom_tree_cache.previous_layout.arena[rect_idx]; let display_rect = ui_solver.edit_variable_cache.map[&dom_hash.data]; - let layout_contraints = create_layout_constraints(rect, *rect_idx, arena, &ui_solver.window_dimensions); + let layout_contraints = create_layout_constraints(rect, rect_idx, &self.rectangles, &ui_solver.window_dimensions); let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, &layout_contraints); ui_solver.solver.add_constraints(&cassowary_constraints).unwrap(); } @@ -257,14 +263,15 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { css.needs_relayout = false; let layout_size = ui_solver.window_dimensions.layout_size; - let mut builder = DisplayListBuilder::with_capacity(pipeline_id, layout_size, self.rectangles.len()); + let mut builder = DisplayListBuilder::with_capacity(pipeline_id, layout_size, self.rectangles.nodes_len()); let mut resource_updates = ResourceUpdates::new(); let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; // Upload image and font resources Self::update_resources(render_api, app_resources, &mut resource_updates); - for (rect_idx, rect) in self.rectangles.iter() { + for rect_idx in self.rectangles.linear_iter() { + let rect = &self.rectangles[rect_idx].data; // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); @@ -342,7 +349,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { push_text( &info, &self, - *rect_idx, + rect_idx, &mut builder, &rect.style, app_resources, @@ -676,19 +683,21 @@ fn parse_css_layout_properties(rect: &mut DisplayRectangle) } // Returns the constraints for one rectangle -fn create_layout_constraints( +fn create_layout_constraints<'a>( rect: &DisplayRectangle, rect_id: NodeId, - arena: &Arena>, + arena: &Arena>, window_dimensions: &WindowDimensions) -> Vec -where T: LayoutScreen { use css_parser; use cassowary::strength::*; use constraints::{SizeConstraint, Strength}; let mut layout_constraints = Vec::::new(); + let max_width = arena.get_wh_for_rectangle(rect_id, WidthOrHeight::Width) + .unwrap_or(window_dimensions.layout_size.width); + println!("max width for rectangle with the ID {} is: {}", rect_id, max_width); layout_constraints.push(CssConstraint::Size((SizeConstraint::Width(200.0), Strength(STRONG)))); layout_constraints.push(CssConstraint::Size((SizeConstraint::Height(200.0), Strength(STRONG)))); @@ -729,7 +738,14 @@ impl<'a> Arena> { /// This function can be used on any rectangle to get the maximum allowed width /// (for inserting the width / height constraint into the layout solver). /// It simply traverses upwards through the nodes, until it finds a matching min-width / width - /// constraint, if it finds none, it will return the width of the root node. + /// constraint, returns None, if the root node is reached (with no constraints) + /// + /// Usually, you'd use it like: + /// + /// ```no_run,ignore + /// let max_width = arena.get_wh_for_rectangle(id, WidthOrHeight::Width) + /// .unwrap_or(window_dimensions.width); + /// ``` fn get_wh_for_rectangle(&self, id: NodeId, field: WidthOrHeight) -> Option { use self::WidthOrHeight::*; diff --git a/src/dom.rs b/src/dom.rs index f491a82eb..661a90f5b 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -489,7 +489,11 @@ impl Dom { impl Dom { - pub(crate) fn collect_callbacks(&self, callback_list: &mut BTreeMap>, nodes_to_callback_id_list: &mut BTreeMap>) { + pub(crate) fn collect_callbacks( + &self, + callback_list: &mut BTreeMap>, + nodes_to_callback_id_list: &mut BTreeMap>) + { for item in self.root.traverse(&*self.arena.borrow()) { let mut cb_id_list = BTreeMap::::new(); let item = &self.arena.borrow()[item.inner_value()]; diff --git a/src/id_tree.rs b/src/id_tree.rs index 9d9e14cb0..1ca3e4ad7 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -1,11 +1,13 @@ //! ID-based node tree -use std::mem; -use std::ops::{Index, IndexMut}; -use std::fmt; -use std::hash::{Hasher, Hash}; -use std::collections::BTreeMap; -use std::ops::Deref; +use std::{ + mem, + fmt, + ops::{Index, IndexMut, Deref}, + hash::{Hasher, Hash}, + collections::BTreeMap, + cmp::Ordering, +}; /// See: https://github.com/rust-lang/rust/issues/27730#issuecomment-311919692 /// @@ -43,8 +45,6 @@ impl NonZeroUsizeHack { } } -use std::cmp::Ordering; - impl PartialOrd for NonZeroUsizeHack { fn partial_cmp(&self, other: &NonZeroUsizeHack) -> Option { Some(self.get().cmp(&other.get())) @@ -78,12 +78,26 @@ impl Hash for NonZeroUsizeHack { } /// A node identifier within a particular `Arena`. -#[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Debug, Hash)] +#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub struct NodeId { // FIXME: Change this to NonZero once NonZero is stabilized pub(crate) index: NonZeroUsizeHack, } +impl NodeId { + pub(crate) fn new(value: usize) -> Self { + Self { + index: NonZeroUsizeHack::new(value), + } + } +} + +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.index.get()) + } +} + #[derive(Clone, PartialEq)] pub struct Node { // Keep these private (with read-only accessors) so that we can keep them consistent. @@ -143,15 +157,15 @@ impl Arena { /// Transform keeps the relative order of parents / children /// but transforms an Arena into an Arena, by running the closure on each of the /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. - pub fn transform(&self, closure: F) -> Arena where F: Fn(&T) -> U { + pub(crate) fn transform(&self, closure: F) -> Arena where F: Fn(&T, NodeId) -> U { Arena { - nodes: self.nodes.iter().map(|node| Node { + nodes: self.nodes.iter().enumerate().map(|(node_id, node)| Node { parent: node.parent, previous_sibling: node.previous_sibling, next_sibling: node.next_sibling, first_child: node.first_child, last_child: node.last_child, - data: closure(&node.data) + data: closure(&node.data, NodeId::new(node_id)) }).collect() } } @@ -168,8 +182,16 @@ impl Arena { } } + /// Return an iterator over the indices in the internal arenas Vec + pub fn linear_iter(&self) -> LinearIterator { + LinearIterator { + arena: &self, + position: 0, + } + } + /// Create a new node from its associated data. - pub fn new_node(&mut self, data: T) -> NodeId { + pub(crate) fn new_node(&mut self, data: T) -> NodeId { let next_index = self.nodes.len(); self.nodes.push(Node { parent: None, @@ -184,8 +206,7 @@ impl Arena { } } - // Useful for debugging - returns how many - // nodes there are in the arena + // Returns how many nodes there are in the arena pub fn nodes_len(&self) -> usize { self.nodes.len() } @@ -468,6 +489,27 @@ macro_rules! impl_node_iterator { } } +/// An linear iterator, does not respec the DOM in any way, +/// it just iterates over the nodes like a Vec +pub struct LinearIterator<'a, T: 'a> { + arena: &'a Arena, + position: usize, +} + +impl<'a, T> Iterator for LinearIterator<'a, T> { + type Item = NodeId; + + fn next(&mut self) -> Option { + if self.position > (self.arena.nodes.len() - 1) { + None + } else { + let new_id = Some(NodeId::new(self.position)); + self.position += 1; + new_id + } + } +} + /// An iterator of references to the ancestors a given node. pub struct Ancestors<'a, T: 'a> { arena: &'a Arena, diff --git a/src/traits.rs b/src/traits.rs index b747dea35..f696aeef1 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use dom::{NodeData, Dom}; use ui_description::{StyledNode, CssConstraintList, UiDescription}; use css::{Css, CssRule}; @@ -96,13 +97,13 @@ impl<'a> ParsedCss<'a> { fn match_dom_css_selectors(root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss, css: &Css, parent_z_level: u32) -> UiDescription { - let mut root_constraints = CssConstraintList::empty(); + let mut root_constraints = CssConstraintList::default(); for global_rule in &parsed_css.pure_global_rules { push_rule(&mut root_constraints, global_rule); } let arena_borrow = &*(*arena).borrow(); - let mut styled_nodes = Vec::::new(); + let mut styled_nodes = BTreeMap::::new(); let sibling_iterator = root.following_siblings(arena_borrow); // skip the root node itself, see documentation for `following_siblings` in id_tree.rs // sibling_iterator.next().unwrap(); @@ -117,19 +118,19 @@ fn match_dom_css_selectors(root: NodeId, arena: &Rc(root: NodeId, arena: &Arena>, parsed_css: &ParsedCss, css: &Css, parent_constraints: &CssConstraintList, parent_z_level: u32) --> Vec +-> BTreeMap { - let mut styled_nodes = Vec::::new(); + let mut styled_nodes = BTreeMap::::new(); let mut current_constraints = parent_constraints.clone(); cascade_constraints(&arena[root].data, &mut current_constraints, parsed_css, css); let current_node = StyledNode { - id: root, z_level: parent_z_level, css_constraints: current_constraints, }; @@ -139,7 +140,7 @@ fn match_dom_css_selectors_inner(root: NodeId, arena: &Arena { pub(crate) ui_descr_arena: Rc>>>, + /// ID of the root node of the arena (usually NodeId(0)) pub(crate) ui_descr_root: Option, - pub(crate) styled_nodes: Vec, + /// This field is created from the Css parser + pub(crate) styled_nodes: BTreeMap, + /// In the display list, we take references to the `UiDescription.styled_nodes` + /// + /// However, if there is no style, we want to have a default style applied + /// and the reference to that style has to live as least as long as the `self.styled_nodes` + /// This is why we need this field here + pub(crate) default_style_of_node: StyledNode, } impl Clone for UiDescription { @@ -19,6 +28,7 @@ impl Clone for UiDescription { ui_descr_arena: self.ui_descr_arena.clone(), ui_descr_root: self.ui_descr_root.clone(), styled_nodes: self.styled_nodes.clone(), + default_style_of_node: self.default_style_of_node.clone(), } } } @@ -28,7 +38,8 @@ impl Default for UiDescription { Self { ui_descr_arena: Rc::new(RefCell::new(Arena::new())), ui_descr_root: None, - styled_nodes: Vec::new(), + styled_nodes: BTreeMap::new(), + default_style_of_node: StyledNode::default(), } } } @@ -40,25 +51,15 @@ impl UiDescription { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct StyledNode { - /// The current node we are processing (the current HTML element) - pub id: NodeId, - /// The z-index level that we are currently on + /// The z-index level that we are currently on, 0 by default pub z_level: u32, /// The CSS constraints, after the cascading step pub css_constraints: CssConstraintList } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct CssConstraintList { pub list: FastHashMap -} - -impl CssConstraintList { - pub fn empty() -> Self { - Self { - list: FastHashMap::default(), - } - } } \ No newline at end of file From 54ece5f0ab901525350c058f502cd594efc47de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 24 May 2018 17:12:08 +0200 Subject: [PATCH 052/868] Migrated from late-parsing CSS values to early-parsing - Removed dynamic pushing of CSS rules - The CSS string has to live long than the Window it is attached to - All CSS errors derive Debug, Clone and PartialEq - Does currently not compile --- examples/debug.rs | 2 +- examples/test_content.css | 2 +- src/app.rs | 4 +- src/css.rs | 187 ++++++++++++++++++++++-------- src/css_parser.rs | 237 +++++++++++++++++++++++++++++++++----- src/display_list.rs | 73 ++++++------ src/id_tree.rs | 1 + src/text_layout.rs | 7 +- src/traits.rs | 8 +- src/window.rs | 10 +- 10 files changed, 401 insertions(+), 130 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 6370e08c9..7ae374331 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -17,7 +17,7 @@ impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { let mut dom = Dom::new(NodeType::Label { - text: String::from("Azul"), + text: format!("{}", self.my_data), }); dom.class("__azul-native-button"); dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); diff --git a/examples/test_content.css b/examples/test_content.css index 44d0fea4c..766c450b3 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -9,8 +9,8 @@ width: 200px; height: 200px; min-height: 400px; + min-width: 500px; text-align: center; - min-width: 400px; flex-direction: row; flex-wrap: nowrap; justify-content: space-around; diff --git a/src/app.rs b/src/app.rs index a7db01600..a2583ee19 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,7 @@ use webrender::api::RenderApi; /// Graphical application that maintains some kind of application state pub struct App<'a, T: LayoutScreen> { /// The graphical windows, indexed by ID - windows: Vec>, + windows: Vec>, /// The global application state pub app_state: Arc>>, } @@ -58,7 +58,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Spawn a new window on the screen. If an application has no windows, /// the [`run`](#method.run) function will exit immediately. - pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { + pub fn create_window(&mut self, options: WindowCreateOptions, css: Css<'a>) -> Result<(), WindowCreateError> { self.windows.push(Window::new(options, css)?); Ok(()) } diff --git a/src/css.rs b/src/css.rs index efedba697..16d007dbe 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,5 +1,6 @@ //! CSS parsing and styling use std::ops::Add; +use css_parser::{ParsedCssProperty, CssParsingError}; #[cfg(target_os="windows")] const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); @@ -9,21 +10,23 @@ const NATIVE_CSS_LINUX: &str = include_str!("../assets/native_linux.css"); const NATIVE_CSS_MACOS: &str = include_str!("../assets/native_macos.css"); /// All the keys that, when changed, can trigger a re-layout -const RELAYOUT_RULES: [&str;11] = [ - "border", "width", "height", "min-width", "min-height", +const RELAYOUT_RULES: [&str; 13] = [ + "border", "width", "height", "min-width", "min-height", "max-width", "max-height", "direction", "wrap", "justify-content", "align-items", "align-content", "order" ]; -/// Wrapper for a `Vec`. Fields are private, because the `Css` -/// struct does caching - each time you add / subtract a `Css`, it is checked -/// if the added / removed CSS rules change the actual layout. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Css { +/// Wrapper for a `Vec` - the CSS is immutable at runtime, it can only be +/// created once. Animations / conditional styling is implemented using dynamic fields (see ``) +#[derive(Debug, Clone, PartialEq)] +pub struct Css<'a> { // NOTE: Each time the rules are modified, the `dirty` flag // has to be set accordingly for the CSS to update! - pub(crate) rules: Vec, - /// + pub(crate) rules: Vec>, + /* + /// The dynamic properties that have to be set for this frame + rules_to_change: FastHashMap, + */ pub(crate) is_dirty: bool, /// Has the CSS changed in a way where it needs a re-layout? /// @@ -33,14 +36,33 @@ pub struct Css { } /// Error that can happen during the parsing of a CSS value -#[derive(Debug, Clone)] -pub enum CssParseError { +#[derive(Debug, Clone, PartialEq)] +pub enum CssParseError<'a> { /// A hard error in the CSS syntax ParseError(::simplecss::Error), /// Braces are not balanced properly UnclosedBlock, /// Invalid syntax, such as `#div { #div: "my-value" }` MalformedCss, + /// Error parsing dynamic CSS property, such as + /// `#div { width: {{ my_id }} /* no default case */ }` + DynamicCssParseError(DynamicCssParseError<'a>), + /// Error during parsing the value of a field + /// (Css is parsed eagerly, directly converted to strongly typed values + /// as soon as possible) + UnexpectedValue(CssParsingError<'a>), +} + +impl<'a> From> for CssParseError<'a> { + fn from(e: CssParsingError<'a>) -> Self { + CssParseError::UnexpectedValue(e) + } +} + +impl<'a> From> for CssParseError<'a> { + fn from(e: DynamicCssParseError<'a>) -> Self { + CssParseError::DynamicCssParseError(e) + } } /// Rule that applies to some "path" in the CSS, i.e. @@ -48,8 +70,8 @@ pub enum CssParseError { /// /// The CSS rule is currently not cascaded, use `Css::new_from_string()` /// to do the cascading. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct CssRule { +#[derive(Debug, Clone, PartialEq)] +pub struct CssRule<'a> { /// `div` (`*` by default) pub html_type: String, /// `#myid` (`None` by default) @@ -57,16 +79,46 @@ pub struct CssRule { /// `.myclass .myotherclass` (vec![] by default) pub classes: Vec, /// `("justify-content", "center")` - pub declaration: (String, String), + pub declaration: (String, CssDeclaration<'a>), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum CssDeclaration<'a> { + Static(ParsedCssProperty<'a>), + Dynamic(DynamicCssProperty<'a>), } -impl CssRule { +/// A `CssProperty` is a type of CSS Rule, +/// but the contents of the rule is dynamic. +/// +/// Azul has "dynamic properties", i.e.: +/// +/// ```no_run,ignore +/// #my_div { +/// padding: {{ my_dynamic_property_id | 400px }}; +/// } +/// ``` +/// +/// At runtime the CSS is immutable (which is a performance optimization - if we +/// can assume that the CSS never changes at runtime), we can do some optimizations on it. +/// Also it leads to cleaner code, since both animations and conditional CSS styling +/// now use the same API. +/// +#[derive(Debug, Clone, PartialEq)] +pub struct DynamicCssProperty<'a> { + default: ParsedCssProperty<'a>, + dynamic_id: String, +} + +impl<'a> CssRule<'a> { pub fn needs_relayout(&self) -> bool { - RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) + // RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) + // TODO + true } } -impl Css { +impl<'a> Css<'a> { /// Creates an empty set of CSS rules pub fn empty() -> Self { @@ -78,7 +130,7 @@ impl Css { } /// Parses a CSS string (single-threaded) and returns the parsed rules - pub fn new_from_string(css_string: &str) -> Result { + pub fn new_from_string(css_string: &'a str) -> Result { use simplecss::{Tokenizer, Token}; use std::collections::HashSet; @@ -110,6 +162,7 @@ impl Css { block_nesting += 1; }, Token::BlockEnd => { + println!("blockend!"); block_nesting -= 1; parser_in_block = false; current_type = "*"; @@ -135,14 +188,22 @@ impl Css { current_classes.insert(class); } Token::Declaration(key, val) => { + println!("declaration: key - {}\t\t| val - {}", key, val); + if !parser_in_block { return Err(CssParseError::MalformedCss); } + + // see if the Declaration is static or dynamic + // + // css_val = "center" | "{{ my_dynamic_id | center }}" + let css_decl = determine_static_or_dynamic_css_property(key, val)?; + let mut css_rule = CssRule { html_type: current_type.to_string(), id: current_id.clone(), classes: current_classes.iter().map(|e| e.to_string()).collect::>(), - declaration: (key.to_string(), val.to_string()), + declaration: (key.to_string(), css_decl), }; // IMPORTANT! css_rule.classes.sort(); @@ -171,22 +232,6 @@ impl Css { }) } - /// Adds a CSS rule - pub fn add_rule(&mut self, css_rule: CssRule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.push(css_rule); - self.is_dirty = true; - } - - /// Removes a rule from the current stylesheet - pub fn remove_rule(&mut self, css_rule: &CssRule) { - if let Some(pos) = self.rules.iter().position(|x| *x == *css_rule) { - self.needs_relayout = css_rule.needs_relayout(); - self.rules.remove(pos); - self.is_dirty = true; - } - } - /// Returns the native style for the OS #[cfg(target_os="windows")] pub fn native() -> Self { @@ -206,21 +251,63 @@ impl Css { } } -impl Add for Css { - type Output = Css; - - fn add(mut self, mut other: Css) -> Css { - let needs_relayout = if !other.needs_relayout { - other.rules.iter().any(|r| r.needs_relayout()) - } else { - other.needs_relayout - }; +#[derive(Debug, Clone, PartialEq)] +pub enum DynamicCssParseError<'a> { + UnclosedBraces, + /// There is a valid dynamic css property, but no default case + NoDefaultCase, + /// The ID may not start with a number or be a CSS property itself + InvalidId, + /// The "default" ID has to be the second ID, not the first one. + DefaultCaseNotSecond, + /// Unexpected value when parsing the string + UnexpectedValue(CssParsingError<'a>), +} - self.rules.append(&mut other.rules); - Css { - rules: self.rules, - is_dirty: true, - needs_relayout: needs_relayout, - } +impl<'a> From> for DynamicCssParseError<'a> { + fn from(e: CssParsingError<'a>) -> Self { + DynamicCssParseError::UnexpectedValue(e) } +} + +/// Determine if a Css property is static (immutable) or if it can change +/// during the runtime of the program +fn determine_static_or_dynamic_css_property<'a>(key: &'a str, value: &'a str) +-> Result, DynamicCssParseError<'a>> +{ + // TODO: dynamic css declarations + Ok(CssDeclaration::Static(ParsedCssProperty::from_kv(key, value)?)) +} + +#[test] +fn test_detect_static_or_dynamic_property() { + use css_parser::TextAlignment; + assert_eq!( + determine_static_or_dynamic_css_property("text-align", " center "), + Ok(CssDeclaration::Static(ParsedCssProperty::TextAlign(TextAlignment::Center))) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "{{ 400px }}"), + Err(DynamicCssParseError::NoDefaultCase) + ); + + assert_eq!(determine_static_or_dynamic_css_property("text-align", "{{ 400px"), + Err(DynamicCssParseError::UnclosedBraces) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "{{ 400px | 500px }}"), + Err(DynamicCssParseError::InvalidId) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "{{ hello | 500px }}"), + Err(DynamicCssParseError::InvalidId) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "{{ 500px | hello }}"), + Err(DynamicCssParseError::InvalidId) + ); } \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index bc61f1035..4fd6ab082 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -8,6 +8,8 @@ use std::num::{ParseIntError, ParseFloatError}; use euclid::{TypedRotation2D, Angle, TypedPoint2D}; pub(crate) const EM_HEIGHT: f32 = 16.0; +/// Webrender measures in points, not in pixels! +pub(crate) const PT_TO_PX: f32 = 96.0 / 72.0; // In case no font size is specified for a node, this will be subsituted as the // default font size @@ -16,8 +18,9 @@ pub(crate) const DEFAULT_FONT_SIZE: FontSize = FontSize(PixelValue { number: 10.0, }); +/// Implements `From` for `$a`, mapping it to the `$b::$enum_type` variant macro_rules! impl_from { - ($a:ident, $b:ident, $enum_type:ident) => ( + ($a:ident, $b:ident::$enum_type:ident) => ( impl<'a> From<$a<'a>> for $b<'a> { fn from(e: $a<'a>) -> Self { $b::$enum_type(e) @@ -26,6 +29,17 @@ macro_rules! impl_from { ) } +/// Same as `impl_from`, but without lifetime annotations for `$a` +macro_rules! impl_from_no_lifetimes { + ($a:ident, $b:ident::$enum_type:ident) => ( + impl<'a> From<$a> for $b<'a> { + fn from(e: $a) -> Self { + $b::$enum_type(e) + } + } + ) +} + /// A parser that can accept a list of items and mappings macro_rules! multi_type_parser { ($fn:ident, $return:ident, $([$identifier_string:expr, $enum_type:ident]),+) => ( @@ -52,6 +66,151 @@ macro_rules! typed_pixel_value_parser { ) } +/// A successfully parsed CSS property +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum ParsedCssProperty<'a> { + BorderRadius(BorderRadius), + BackgroundColor(BackgroundColor), + TextColor(TextColor), + Border(BorderWidths, BorderDetails), + Background(Background<'a>), + FontSize(FontSize), + FontFamily(FontFamily), + TextOverflow(TextOverflowBehaviour), + TextAlign(TextAlignment), + BoxShadow(Option), + + Width(LayoutWidth), + Height(LayoutHeight), + MinWidth(LayoutMinWidth), + MinHeight(LayoutMinHeight), + MaxWidth(LayoutMaxWidth), + MaxHeight(LayoutMaxHeight), + + FlexWrap(LayoutWrap), + FlexDirection(LayoutDirection), + JustifyContent(LayoutJustifyContent), + AlignItems(LayoutAlignItems), + AlignContent(LayoutAlignContent), +} + +impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); +impl_from!(Background, ParsedCssProperty::Background); +impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); +impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); +impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); +impl_from_no_lifetimes!(TextAlignment, ParsedCssProperty::TextAlign); + +impl_from_no_lifetimes!(LayoutWidth, ParsedCssProperty::Width); +impl_from_no_lifetimes!(LayoutHeight, ParsedCssProperty::Height); +impl_from_no_lifetimes!(LayoutMinWidth, ParsedCssProperty::MinWidth); +impl_from_no_lifetimes!(LayoutMinHeight, ParsedCssProperty::MinHeight); +impl_from_no_lifetimes!(LayoutMaxWidth, ParsedCssProperty::MaxWidth); +impl_from_no_lifetimes!(LayoutMaxHeight, ParsedCssProperty::MaxHeight); + +impl_from_no_lifetimes!(LayoutWrap, ParsedCssProperty::FlexWrap); +impl_from_no_lifetimes!(LayoutDirection, ParsedCssProperty::FlexDirection); +impl_from_no_lifetimes!(LayoutJustifyContent, ParsedCssProperty::JustifyContent); +impl_from_no_lifetimes!(LayoutAlignItems, ParsedCssProperty::AlignItems); +impl_from_no_lifetimes!(LayoutAlignContent, ParsedCssProperty::AlignContent); + +impl_from_no_lifetimes!(BackgroundColor, ParsedCssProperty::BackgroundColor); +impl_from_no_lifetimes!(TextColor, ParsedCssProperty::TextColor); + +impl<'a> From<(BorderWidths, BorderDetails)> for ParsedCssProperty<'a> { + fn from((widths, details): (BorderWidths, BorderDetails)) -> Self { + ParsedCssProperty::Border(widths, details) + } +} + +impl<'a> From> for ParsedCssProperty<'a> { + fn from(box_shadow: Option) -> Self { + ParsedCssProperty::BoxShadow(box_shadow) + } +} + +impl<'a> ParsedCssProperty<'a> { + /// Main parsing function, takes a stringified key / value pair and either + /// returns the parsed value or an error + pub fn from_kv(key: &'a str, value: &'a str) -> Result> { + match key { + "border-radius" => Ok(parse_css_border_radius(value) .map_err(|e| e.into())?.into()), + "background-color" => Ok(parse_css_background_color(value) .map_err(|e| e.into())?.into()), + "color" => Ok(parse_css_text_color(value) .map_err(|e| e.into())?.into()), + "border" => Ok(parse_css_border(value) .map_err(|e| e.into())?.into()), + "background" => Ok(parse_css_background(value) .map_err(|e| e.into())?.into()), + "font-size" => Ok(parse_css_font_size(value) .map_err(|e| e.into())?.into()), + "font-family" => Ok(parse_css_font_family(value) .map_err(|e| e.into())?.into()), + "box-shadow" => Ok(parse_css_box_shadow(value) .map_err(|e| e.into())?.into()), + + "width" => Ok(parse_layout_width(value) .map_err(|e| e.into())?.into()), + "height" => Ok(parse_layout_height(value) .map_err(|e| e.into())?.into()), + "min-width" => Ok(parse_layout_min_width(value) .map_err(|e| e.into())?.into()), + "min-height" => Ok(parse_layout_min_height(value) .map_err(|e| e.into())?.into()), + "max-width" => Ok(parse_layout_max_width(value) .map_err(|e| e.into())?.into()), + "max-height" => Ok(parse_layout_max_height(value) .map_err(|e| e.into())?.into()), + + "flex-wrap" => Ok(parse_layout_wrap(value) .map_err(|e| e.into())?.into()), + "flex-direction" => Ok(parse_layout_direction(value) .map_err(|e| e.into())?.into()), + "justify-content" => Ok(parse_layout_justify_content(value) .map_err(|e| e.into())?.into()), + "align-items" => Ok(parse_layout_align_items(value) .map_err(|e| e.into())?.into()), + "align-content" => Ok(parse_layout_align_content(value) .map_err(|e| e.into())?.into()), + "overflow" => Ok(parse_layout_text_overflow(value) .map_err(|e| e.into())?.into()), + "text-align" => Ok(parse_layout_text_align(value) .map_err(|e| e.into())?.into()), + + _ => Err((key, value).into()) + } + } +} + +/// Error containing all sub-errors that could happen during CSS parsing +/// +/// Usually we want to crash on the first error, to notify the user of the problem. +#[derive(Debug, Clone, PartialEq)] +pub enum CssParsingError<'a> { + CssBorderParseError(CssBorderParseError<'a>), + CssColorParseError(CssColorParseError<'a>), + PixelParseError(PixelParseError<'a>), + CssImageParseError(CssImageParseError<'a>), + CssBorderRadiusParseError(CssBorderRadiusParseError<'a>), + /// Key is not supported, i.e. `#div { aldfjasdflk: 400px }` results in an + /// `UnsupportedCssKey("aldfjasdflk", "400px")` error + UnsupportedCssKey(&'a str, &'a str), +} + +impl_from!(CssBorderParseError, CssParsingError::CssBorderParseError); +impl_from!(CssColorParseError, CssParsingError::CssColorParseError); +impl_from!(PixelParseError, CssParsingError::PixelParseError); +impl_from!(CssImageParseError, CssParsingError::CssImageParseError); +impl_from!(CssBorderRadiusParseError, CssParsingError::CssBorderRadiusParseError); + +/* +impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); +impl_from!(Background, ParsedCssProperty::Background); +impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); +impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); +impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); +impl_from_no_lifetimes!(TextAlignment, ParsedCssProperty::TextAlign); + +impl_from_no_lifetimes!(LayoutWidth, ParsedCssProperty::Width); +impl_from_no_lifetimes!(LayoutHeight, ParsedCssProperty::Height); +impl_from_no_lifetimes!(LayoutMinWidth, ParsedCssProperty::MinWidth); +impl_from_no_lifetimes!(LayoutMinHeight, ParsedCssProperty::MinHeight); +impl_from_no_lifetimes!(LayoutMaxWidth, ParsedCssProperty::MaxWidth); +impl_from_no_lifetimes!(LayoutMaxHeight, ParsedCssProperty::MaxHeight); + +impl_from_no_lifetimes!(LayoutWrap, ParsedCssProperty::FlexWrap); +impl_from_no_lifetimes!(LayoutDirection, ParsedCssProperty::FlexDirection); +impl_from_no_lifetimes!(LayoutJustifyContent, ParsedCssProperty::JustifyContent); +impl_from_no_lifetimes!(LayoutAlignItems, ParsedCssProperty::AlignItems); +impl_from_no_lifetimes!(LayoutAlignContent, ParsedCssProperty::AlignContent); +*/ +impl<'a> From<(&'a str, &'a str)> for CssParsingError<'a> { + fn from((a, b): (&'a str, &'a str)) -> Self { + CssParsingError::UnsupportedCssKey(a, b) + } +} + /// Simple "invalid value" error, used for #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct InvalidValueErr<'a>(pub &'a str); @@ -65,6 +224,7 @@ pub struct PixelValue { #[derive(Debug, PartialEq, Clone, Copy)] pub enum CssMetric { Px, + Pt, Em, } @@ -72,32 +232,33 @@ impl PixelValue { pub fn to_pixels(&self) -> f32 { match self.metric { CssMetric::Px => { self.number }, + CssMetric::Pt => { self.number * PT_TO_PX }, CssMetric::Em => { self.number * EM_HEIGHT }, } } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum CssBorderRadiusParseError<'a> { TooManyValues(&'a str), PixelParseError(PixelParseError<'a>), } -impl_from!(PixelParseError, CssBorderRadiusParseError, PixelParseError); +impl_from!(PixelParseError, CssBorderRadiusParseError::PixelParseError); -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssColorParseError<'a> { InvalidColor(&'a str), InvalidColorComponent(u8), ValueParseErr(ParseIntError), } -#[derive(Debug, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum CssImageParseError<'a> { UnclosedQuotes(&'a str), } -#[derive(Debug, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct UnclosedQuotesError<'a>(pub(crate) &'a str); impl<'a> From> for CssImageParseError<'a> { @@ -106,7 +267,7 @@ impl<'a> From> for CssImageParseError<'a> { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssBorderParseError<'a> { InvalidBorderStyle(InvalidValueErr<'a>), InvalidBorderDeclaration(&'a str), @@ -114,7 +275,7 @@ pub enum CssBorderParseError<'a> { ColorParseError(CssColorParseError<'a>), } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssShadowParseError<'a> { InvalidSingleStatement(&'a str), TooManyComponents(&'a str), @@ -122,8 +283,8 @@ pub enum CssShadowParseError<'a> { ColorParseError(CssColorParseError<'a>), } -impl_from!(PixelParseError, CssShadowParseError, ValueParseErr); -impl_from!(CssColorParseError, CssShadowParseError, ColorParseError); +impl_from!(PixelParseError, CssShadowParseError::ValueParseErr); +impl_from!(CssColorParseError, CssShadowParseError::ColorParseError); /// parse the border-radius like "5px 10px" or "5px 10px 6px 10px" pub fn parse_css_border_radius<'a>(input: &'a str) @@ -195,7 +356,7 @@ pub fn parse_css_border_radius<'a>(input: &'a str) } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum PixelParseError<'a> { InvalidComponent(&'a str), ValueParseErr(ParseFloatError), @@ -218,6 +379,7 @@ pub fn parse_pixel_value<'a>(input: &'a str) let unit = match unit { "px" => CssMetric::Px, "em" => CssMetric::Em, + "ept" => CssMetric::Pt, _ => { return Err(PixelParseError::InvalidComponent(&input[(split_pos - 1)..])); } }; @@ -233,7 +395,7 @@ pub fn parse_pixel_value<'a>(input: &'a str) /// /// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) /// "#00FF00" -> ColorF { r: 0, g: 255, b: 0 }) -pub fn parse_css_color<'a>(input: &'a str) +fn parse_css_color<'a>(input: &'a str) -> Result> { if input.starts_with('#') { @@ -243,6 +405,24 @@ pub fn parse_css_color<'a>(input: &'a str) } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct BackgroundColor(pub ColorU); + +pub fn parse_css_background_color<'a>(input: &'a str) +-> Result> +{ + parse_css_color(input).and_then(|ok| Ok(BackgroundColor(ok))) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TextColor(pub ColorU); + +pub fn parse_css_text_color<'a>(input: &'a str) +-> Result> +{ + parse_css_color(input).and_then(|ok| Ok(TextColor(ok))) +} + /// Parse a built-in background color /// /// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) @@ -671,7 +851,7 @@ pub fn parse_css_box_shadow<'a>(input: &'a str) Ok(Some(box_shadow)) } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssBackgroundParseError<'a> { Error(&'a str), InvalidBackground(&'a str), @@ -684,10 +864,10 @@ pub enum CssBackgroundParseError<'a> { ImageParseError(CssImageParseError<'a>), } -impl_from!(CssDirectionParseError, CssBackgroundParseError, DirectionParseError); -impl_from!(CssGradientStopParseError, CssBackgroundParseError, GradientParseError); -impl_from!(CssShapeParseError, CssBackgroundParseError, ShapeParseError); -impl_from!(CssImageParseError, CssBackgroundParseError, ImageParseError); +impl_from!(CssDirectionParseError, CssBackgroundParseError::DirectionParseError); +impl_from!(CssGradientStopParseError, CssBackgroundParseError::GradientParseError); +impl_from!(CssShapeParseError, CssBackgroundParseError::ShapeParseError); +impl_from!(CssImageParseError, CssBackgroundParseError::ImageParseError); #[derive(Debug, Clone, PartialEq)] pub enum Background<'a> { @@ -811,7 +991,6 @@ enum BackgroundType { Image, } - // parses a background, such as "linear-gradient(red, green)" pub fn parse_css_background<'a>(input: &'a str) -> Result> @@ -1043,7 +1222,7 @@ fn strip_quotes<'a>(input: &'a str) -> Result, UnclosedQuotesE } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssGradientStopParseError<'a> { Error(&'a str), ColorParseError(CssColorParseError<'a>), @@ -1083,7 +1262,7 @@ fn parse_percentage(input: &str) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum CssDirectionParseError<'a> { Error(&'a str), InvalidArguments(&'a str), @@ -1165,7 +1344,7 @@ fn parse_direction<'a>(input: &'a str) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum CssDirectionCornerParseError<'a> { InvalidDirection(&'a str), } @@ -1182,7 +1361,7 @@ fn parse_direction_corner<'a>(input: &'a str) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Copy, Clone)] pub enum CssShapeParseError<'a> { ShapeErr(InvalidValueErr<'a>), } @@ -1192,9 +1371,13 @@ pub struct LayoutWidth(pub PixelValue); #[derive(Debug, PartialEq, Copy, Clone)] pub struct LayoutMinWidth(pub PixelValue); #[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutMaxWidth(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] pub struct LayoutHeight(pub PixelValue); #[derive(Debug, PartialEq, Copy, Clone)] pub struct LayoutMinHeight(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutMaxHeight(pub PixelValue); #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutDirection { @@ -1284,7 +1467,7 @@ impl Default for TextAlignment { #[derive(Default, Debug, Clone, PartialEq)] pub(crate) struct RectStyle<'a> { /// Background color of this rectangle - pub(crate) background_color: Option, + pub(crate) background_color: Option, /// Shadow color pub(crate) box_shadow: Option, /// Gradient (location) + stops @@ -1298,7 +1481,7 @@ pub(crate) struct RectStyle<'a> { /// Font name / family pub(crate) font_family: Option, /// Text color - pub(crate) font_color: Option, + pub(crate) font_color: Option, /// Text alignment pub(crate) text_align: Option, /// Text overflow behaviour @@ -1323,6 +1506,8 @@ typed_pixel_value_parser!(parse_layout_width, LayoutWidth); typed_pixel_value_parser!(parse_layout_height, LayoutHeight); typed_pixel_value_parser!(parse_layout_min_height, LayoutMinHeight); typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); +typed_pixel_value_parser!(parse_layout_max_width, LayoutMaxWidth); +typed_pixel_value_parser!(parse_layout_max_height, LayoutMaxHeight); #[derive(Debug, PartialEq, Copy, Clone)] pub struct FontSize(pub PixelValue); @@ -1422,13 +1607,13 @@ multi_type_parser!(parse_shape, Shape, ["circle", Circle], ["ellipse", Ellipse]); -multi_type_parser!(parse_text_overflow, TextOverflowBehaviour, +multi_type_parser!(parse_layout_text_overflow, TextOverflowBehaviour, ["auto", Auto], ["scroll", Scroll], ["visible", Visible], ["hidden", Hidden]); -multi_type_parser!(parse_text_align, TextAlignment, +multi_type_parser!(parse_layout_text_align, TextAlignment, ["center", Center], ["left", Left], ["right", Right]); diff --git a/src/display_list.rs b/src/display_list.rs index 8d0ef3f3f..abd23fa55 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1,26 +1,32 @@ #![allow(unused_variables)] #![allow(unused_macros)] +use std::{ + collections::BTreeMap, + sync::atomic::{Ordering, AtomicUsize}, + fmt::Debug, +}; use webrender::api::*; -use resources::AppResources; -use traits::LayoutScreen; -use constraints::{DisplayRect, CssConstraint}; -use ui_description::{UiDescription, StyledNode}; -use cassowary::{Constraint, Solver, Variable}; -use window::{WindowDimensions, UiSolver}; -use id_tree::{Arena, NodeId}; -use css_parser::*; -use dom::NodeData; -use css::Css; -use std::collections::BTreeMap; -use FastHashMap; -use cache::DomChangeSet; -use std::sync::atomic::{Ordering, AtomicUsize}; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; -use css_parser; +use cassowary::{Constraint, Solver, Variable}; -const DEFAULT_FONT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; +use { + FastHashMap, + resources::AppResources, + traits::LayoutScreen, + constraints::{DisplayRect, CssConstraint}, + ui_description::{UiDescription, StyledNode}, + window::{WindowDimensions, UiSolver}, + id_tree::{Arena, NodeId}, + css_parser::{self, *}, + dom::NodeData, + css::Css, + cache::DomChangeSet, + ui_description::CssConstraintList, +}; + +const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { @@ -28,6 +34,7 @@ pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { pub(crate) rectangles: Arena> } +/// DisplayRectangle is the main type which the layout parsing step gets operated on. #[derive(Debug)] pub(crate) struct DisplayRectangle<'a> { /// `Some(id)` if this rectangle has a callback attached to it @@ -88,14 +95,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { parse_css_layout_properties(&mut rect); rect }); -/* - for node in ui_description.styled_nodes { - let mut rect = DisplayRectangle::new(arena[node.id].data.tag, &node); - parse_css_style_properties(&mut rect); - parse_css_layout_properties(&mut rect); - rect_btree.insert(node.id, rect); - } -*/ + Self { ui_descr: ui_description, rectangles: display_rect_arena, @@ -622,9 +622,6 @@ fn push_font( } } -use ui_description::CssConstraintList; -use std::fmt::Debug; - /// Internal helper function - gets a key from the constraint list and passes it through /// the parse_func - if an error occurs, then the error gets printed fn parse<'a, T, E: Debug>( @@ -638,11 +635,13 @@ fn parse<'a, T, E: Debug>( eprintln!("ERROR - invalid {:?}: {:?}", err, key); } - constraint_list.list.get(key).and_then(|w| parse_func(w).map_err(|e| { - #[cfg(debug_assertions)] - print_error_debug(&e, key); - e - }).ok()) + constraint_list.list.get(key).and_then(|w| + parse_func(w).map_err(|e| { + #[cfg(debug_assertions)] + print_error_debug(&e, key); + e + }).ok() + ) } /// Populate and parse the CSS style properties @@ -651,14 +650,12 @@ fn parse_css_style_properties(rect: &mut DisplayRectangle) let constraint_list = &rect.styled_node.css_constraints; rect.style.border_radius = parse(constraint_list, "border-radius", parse_css_border_radius); - rect.style.background_color = parse(constraint_list, "background-color", parse_css_color); - rect.style.font_color = parse(constraint_list, "color", parse_css_color); + rect.style.background_color = parse(constraint_list, "background-color", parse_css_background_color); + rect.style.font_color = parse(constraint_list, "color", parse_css_text_color); rect.style.border = parse(constraint_list, "border", parse_css_border); rect.style.background = parse(constraint_list, "background", parse_css_background); rect.style.font_size = parse(constraint_list, "font-size", parse_css_font_size); rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); - rect.style.text_overflow = parse(constraint_list, "overflow", parse_text_overflow); - rect.style.text_align = parse(constraint_list, "text-align", parse_text_align); if let Some(box_shadow_opt) = parse(constraint_list, "box-shadow", parse_css_box_shadow) { rect.style.box_shadow = box_shadow_opt; @@ -680,6 +677,9 @@ fn parse_css_layout_properties(rect: &mut DisplayRectangle) rect.layout.justify_content = parse(constraint_list, "justify-content", parse_layout_justify_content); rect.layout.align_items = parse(constraint_list, "align-items", parse_layout_align_items); rect.layout.align_content = parse(constraint_list, "align-content", parse_layout_align_content); + + rect.style.text_overflow = parse(constraint_list, "overflow", parse_layout_text_overflow); + rect.style.text_align = parse(constraint_list, "text-align", parse_layout_text_align); } // Returns the constraints for one rectangle @@ -697,6 +697,7 @@ fn create_layout_constraints<'a>( let mut layout_constraints = Vec::::new(); let max_width = arena.get_wh_for_rectangle(rect_id, WidthOrHeight::Width) .unwrap_or(window_dimensions.layout_size.width); + println!("max width for rectangle with the ID {} is: {}", rect_id, max_width); layout_constraints.push(CssConstraint::Size((SizeConstraint::Width(200.0), Strength(STRONG)))); diff --git a/src/id_tree.rs b/src/id_tree.rs index 1ca3e4ad7..113b924d0 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -158,6 +158,7 @@ impl Arena { /// but transforms an Arena into an Arena, by running the closure on each of the /// items. The `NodeId` for the root is then valid for the newly created `Arena`, too. pub(crate) fn transform(&self, closure: F) -> Arena where F: Fn(&T, NodeId) -> U { + // TODO if T: Send (which is usually the case), then we could use rayon here! Arena { nodes: self.nodes.iter().enumerate().map(|(node_id, node)| Node { parent: node.parent, diff --git a/src/text_layout.rs b/src/text_layout.rs index 9feead521..2d98375e3 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -5,15 +5,12 @@ use euclid::{Length, TypedRect, TypedPoint2D}; use rusttype::{Font, Scale}; use css_parser::{TextAlignment, TextOverflowBehaviour}; -/// Webrender measures in points, not in pixels! -pub const PT_TO_PX: f32 = 96.0 / 72.0; - -pub const PX_TO_PT: f32 = 72.0 / 96.0; - /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; +const PX_TO_PT: f32 = 72.0 / 96.0; + /// Lines is responsible for layouting the lines of the rectangle to struct Lines<'a> { align: TextAlignment, diff --git a/src/traits.rs b/src/traits.rs index f696aeef1..ec56b90ec 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -27,10 +27,10 @@ pub trait LayoutScreen { } pub(crate) struct ParsedCss<'a> { - pub(crate) pure_global_rules: Vec<&'a CssRule>, - pub(crate) pure_div_rules: Vec<&'a CssRule>, - pub(crate) pure_class_rules: Vec<&'a CssRule>, - pub(crate) pure_id_rules: Vec<&'a CssRule>, + pub(crate) pure_global_rules: Vec<&'a CssRule<'a>>, + pub(crate) pure_div_rules: Vec<&'a CssRule<'a>>, + pub(crate) pure_class_rules: Vec<&'a CssRule<'a>>, + pub(crate) pure_id_rules: Vec<&'a CssRule<'a>>, } impl<'a> ParsedCss<'a> { diff --git a/src/window.rs b/src/window.rs index 69037bc66..6d6962dec 100644 --- a/src/window.rs +++ b/src/window.rs @@ -371,7 +371,7 @@ impl Default for WindowMonitorTarget { } /// Represents one graphical window to be rendered -pub struct Window { +pub struct Window<'a, T: LayoutScreen> { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, // TODO: Migrate to the window_state for state diffing @@ -390,7 +390,7 @@ pub struct Window { // The background thread that is running for this window. // pub(crate) background_thread: Option>, /// The css (how the current window is styled) - pub css: Css, + pub css: Css<'a>, } /// Used in the solver, for the root constraint @@ -451,10 +451,10 @@ pub(crate) struct WindowInternal { pub(crate) hidpi_factor: f32, } -impl Window { +impl<'a, T: LayoutScreen> Window<'a, T> { /// Creates a new window - pub fn new(options: WindowCreateOptions, css: Css) -> Result { + pub fn new(options: WindowCreateOptions, css: Css<'a>) -> Result { let events_loop = EventsLoop::new(); @@ -641,7 +641,7 @@ impl Window { } } -impl Drop for Window { +impl<'a, T: LayoutScreen> Drop for Window<'a, T> { fn drop(&mut self) { // self.background_thread.take().unwrap().join(); let renderer = self.renderer.take().unwrap(); From 97c2e17f9caa69ad05b3afddbbf33d163617dfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 24 May 2018 18:20:29 +0200 Subject: [PATCH 053/868] Migrated to early-parsed CSS parsing, fixed display list --- src/app.rs | 4 +- src/css.rs | 6 +-- src/css_parser.rs | 88 ++++++++++++++++++++---------------- src/display_list.rs | 102 +++++++++++++++++------------------------- src/lib.rs | 4 +- src/traits.rs | 33 ++++++++++---- src/ui_description.rs | 29 ++++++------ src/window.rs | 4 +- 8 files changed, 138 insertions(+), 132 deletions(-) diff --git a/src/app.rs b/src/app.rs index a2583ee19..19501d863 100644 --- a/src/app.rs +++ b/src/app.rs @@ -58,8 +58,8 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Spawn a new window on the screen. If an application has no windows, /// the [`run`](#method.run) function will exit immediately. - pub fn create_window(&mut self, options: WindowCreateOptions, css: Css<'a>) -> Result<(), WindowCreateError> { - self.windows.push(Window::new(options, css)?); + pub fn create_window(&mut self, options: WindowCreateOptions, css: &'a mut Css<'a>) -> Result<(), WindowCreateError> { + self.windows.push(Window::new(options, &mut *css)?); Ok(()) } diff --git a/src/css.rs b/src/css.rs index 16d007dbe..fb39b5d61 100644 --- a/src/css.rs +++ b/src/css.rs @@ -71,7 +71,7 @@ impl<'a> From> for CssParseError<'a> { /// The CSS rule is currently not cascaded, use `Css::new_from_string()` /// to do the cascading. #[derive(Debug, Clone, PartialEq)] -pub struct CssRule<'a> { +pub(crate) struct CssRule<'a> { /// `div` (`*` by default) pub html_type: String, /// `#myid` (`None` by default) @@ -83,7 +83,7 @@ pub struct CssRule<'a> { } #[derive(Debug, Clone, PartialEq)] -pub enum CssDeclaration<'a> { +pub(crate) enum CssDeclaration<'a> { Static(ParsedCssProperty<'a>), Dynamic(DynamicCssProperty<'a>), } @@ -105,7 +105,7 @@ pub enum CssDeclaration<'a> { /// now use the same API. /// #[derive(Debug, Clone, PartialEq)] -pub struct DynamicCssProperty<'a> { +pub(crate) struct DynamicCssProperty<'a> { default: ParsedCssProperty<'a>, dynamic_id: String, } diff --git a/src/css_parser.rs b/src/css_parser.rs index 4fd6ab082..9b22b9ba1 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -43,7 +43,7 @@ macro_rules! impl_from_no_lifetimes { /// A parser that can accept a list of items and mappings macro_rules! multi_type_parser { ($fn:ident, $return:ident, $([$identifier_string:expr, $enum_type:ident]),+) => ( - pub fn $fn<'a>(input: &'a str) + fn $fn<'a>(input: &'a str) -> Result<$return, InvalidValueErr<'a>> { match input { @@ -58,7 +58,7 @@ macro_rules! multi_type_parser { macro_rules! typed_pixel_value_parser { ($fn:ident, $return:ident) => ( - pub fn $fn<'a>(input: &'a str) + fn $fn<'a>(input: &'a str) -> Result<$return, PixelParseError<'a>> { parse_pixel_value(input).and_then(|e| Ok($return(e))) @@ -134,29 +134,29 @@ impl<'a> ParsedCssProperty<'a> { /// returns the parsed value or an error pub fn from_kv(key: &'a str, value: &'a str) -> Result> { match key { - "border-radius" => Ok(parse_css_border_radius(value) .map_err(|e| e.into())?.into()), - "background-color" => Ok(parse_css_background_color(value) .map_err(|e| e.into())?.into()), - "color" => Ok(parse_css_text_color(value) .map_err(|e| e.into())?.into()), - "border" => Ok(parse_css_border(value) .map_err(|e| e.into())?.into()), - "background" => Ok(parse_css_background(value) .map_err(|e| e.into())?.into()), - "font-size" => Ok(parse_css_font_size(value) .map_err(|e| e.into())?.into()), - "font-family" => Ok(parse_css_font_family(value) .map_err(|e| e.into())?.into()), - "box-shadow" => Ok(parse_css_box_shadow(value) .map_err(|e| e.into())?.into()), - - "width" => Ok(parse_layout_width(value) .map_err(|e| e.into())?.into()), - "height" => Ok(parse_layout_height(value) .map_err(|e| e.into())?.into()), - "min-width" => Ok(parse_layout_min_width(value) .map_err(|e| e.into())?.into()), - "min-height" => Ok(parse_layout_min_height(value) .map_err(|e| e.into())?.into()), - "max-width" => Ok(parse_layout_max_width(value) .map_err(|e| e.into())?.into()), - "max-height" => Ok(parse_layout_max_height(value) .map_err(|e| e.into())?.into()), - - "flex-wrap" => Ok(parse_layout_wrap(value) .map_err(|e| e.into())?.into()), - "flex-direction" => Ok(parse_layout_direction(value) .map_err(|e| e.into())?.into()), - "justify-content" => Ok(parse_layout_justify_content(value) .map_err(|e| e.into())?.into()), - "align-items" => Ok(parse_layout_align_items(value) .map_err(|e| e.into())?.into()), - "align-content" => Ok(parse_layout_align_content(value) .map_err(|e| e.into())?.into()), - "overflow" => Ok(parse_layout_text_overflow(value) .map_err(|e| e.into())?.into()), - "text-align" => Ok(parse_layout_text_align(value) .map_err(|e| e.into())?.into()), + "border-radius" => Ok(parse_css_border_radius(value)?.into()), + "background-color" => Ok(parse_css_background_color(value)?.into()), + "color" => Ok(parse_css_text_color(value)?.into()), + "border" => Ok(parse_css_border(value)?.into()), + "background" => Ok(parse_css_background(value)?.into()), + "font-size" => Ok(parse_css_font_size(value)?.into()), + "font-family" => Ok(parse_css_font_family(value)?.into()), + "box-shadow" => Ok(parse_css_box_shadow(value)?.into()), + + "width" => Ok(parse_layout_width(value)?.into()), + "height" => Ok(parse_layout_height(value)?.into()), + "min-width" => Ok(parse_layout_min_width(value)?.into()), + "min-height" => Ok(parse_layout_min_height(value)?.into()), + "max-width" => Ok(parse_layout_max_width(value)?.into()), + "max-height" => Ok(parse_layout_max_height(value)?.into()), + + "flex-wrap" => Ok(parse_layout_wrap(value)?.into()), + "flex-direction" => Ok(parse_layout_direction(value)?.into()), + "justify-content" => Ok(parse_layout_justify_content(value)?.into()), + "align-items" => Ok(parse_layout_align_items(value)?.into()), + "align-content" => Ok(parse_layout_align_content(value)?.into()), + "overflow" => Ok(parse_layout_text_overflow(value)?.into()), + "text-align" => Ok(parse_layout_text_align(value)?.into()), _ => Err((key, value).into()) } @@ -169,9 +169,13 @@ impl<'a> ParsedCssProperty<'a> { #[derive(Debug, Clone, PartialEq)] pub enum CssParsingError<'a> { CssBorderParseError(CssBorderParseError<'a>), - CssColorParseError(CssColorParseError<'a>), + CssShadowParseError(CssShadowParseError<'a>), + InvalidValueErr(InvalidValueErr<'a>), PixelParseError(PixelParseError<'a>), CssImageParseError(CssImageParseError<'a>), + CssFontFamilyParseError(CssFontFamilyParseError<'a>), + CssBackgroundParseError(CssBackgroundParseError<'a>), + CssColorParseError(CssColorParseError<'a>), CssBorderRadiusParseError(CssBorderRadiusParseError<'a>), /// Key is not supported, i.e. `#div { aldfjasdflk: 400px }` results in an /// `UnsupportedCssKey("aldfjasdflk", "400px")` error @@ -179,9 +183,13 @@ pub enum CssParsingError<'a> { } impl_from!(CssBorderParseError, CssParsingError::CssBorderParseError); +impl_from!(CssShadowParseError, CssParsingError::CssShadowParseError); impl_from!(CssColorParseError, CssParsingError::CssColorParseError); +impl_from!(InvalidValueErr, CssParsingError::InvalidValueErr); impl_from!(PixelParseError, CssParsingError::PixelParseError); impl_from!(CssImageParseError, CssParsingError::CssImageParseError); +impl_from!(CssFontFamilyParseError, CssParsingError::CssFontFamilyParseError); +impl_from!(CssBackgroundParseError, CssParsingError::CssBackgroundParseError); impl_from!(CssBorderRadiusParseError, CssParsingError::CssBorderRadiusParseError); /* @@ -287,7 +295,7 @@ impl_from!(PixelParseError, CssShadowParseError::ValueParseErr); impl_from!(CssColorParseError, CssShadowParseError::ColorParseError); /// parse the border-radius like "5px 10px" or "5px 10px 6px 10px" -pub fn parse_css_border_radius<'a>(input: &'a str) +fn parse_css_border_radius<'a>(input: &'a str) -> Result> { let mut components = input.split_whitespace(); @@ -363,7 +371,7 @@ pub enum PixelParseError<'a> { } /// parse a single value such as "15px" -pub fn parse_pixel_value<'a>(input: &'a str) +fn parse_pixel_value<'a>(input: &'a str) -> Result> { let mut split_pos = 0; @@ -408,7 +416,7 @@ fn parse_css_color<'a>(input: &'a str) #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct BackgroundColor(pub ColorU); -pub fn parse_css_background_color<'a>(input: &'a str) +fn parse_css_background_color<'a>(input: &'a str) -> Result> { parse_css_color(input).and_then(|ok| Ok(BackgroundColor(ok))) @@ -417,8 +425,8 @@ pub fn parse_css_background_color<'a>(input: &'a str) #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct TextColor(pub ColorU); -pub fn parse_css_text_color<'a>(input: &'a str) --> Result> +fn parse_css_text_color<'a>(input: &'a str) +-> Result> { parse_css_color(input).and_then(|ok| Ok(TextColor(ok))) } @@ -664,7 +672,7 @@ fn parse_color_no_hash<'a>(input: &'a str) /// Parse a CSS border such as /// /// "5px solid red" -pub fn parse_css_border<'a>(input: &'a str) +fn parse_css_border<'a>(input: &'a str) -> Result<(BorderWidths, BorderDetails), CssBorderParseError<'a>> { let mut input_iter = input.split_whitespace(); @@ -740,7 +748,7 @@ pub struct BoxShadowPreDisplayItem { } /// Parses a CSS box-shadow -pub fn parse_css_box_shadow<'a>(input: &'a str) +fn parse_css_box_shadow<'a>(input: &'a str) -> Result, CssShadowParseError<'a>> { let mut input_iter = input.split_whitespace(); @@ -992,7 +1000,7 @@ enum BackgroundType { } // parses a background, such as "linear-gradient(red, green)" -pub fn parse_css_background<'a>(input: &'a str) +fn parse_css_background<'a>(input: &'a str) -> Result> { use self::BackgroundType::*; @@ -1495,6 +1503,8 @@ pub struct RectLayout { pub height: Option, pub min_width: Option, pub min_height: Option, + pub max_width: Option, + pub max_height: Option, pub direction: Option, pub wrap: Option, pub justify_content: Option, @@ -1527,15 +1537,15 @@ pub enum Font { } #[derive(Debug, PartialEq, Copy, Clone)] -pub enum FontFamilyParseError<'a> { +pub enum CssFontFamilyParseError<'a> { InvalidFontFamily(&'a str), UnrecognizedBuiltinFont(&'a str), UnclosedQuotes(&'a str), } -impl<'a> From> for FontFamilyParseError<'a> { +impl<'a> From> for CssFontFamilyParseError<'a> { fn from(err: UnclosedQuotesError<'a>) -> Self { - FontFamilyParseError::UnclosedQuotes(err.0) + CssFontFamilyParseError::UnclosedQuotes(err.0) } } @@ -1544,7 +1554,7 @@ impl<'a> From> for FontFamilyParseError<'a> { // "Webly Sleeky UI", monospace // 'Webly Sleeky Ui', monospace // sans-serif -pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result> { +pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result> { let multiple_fonts = input.split(','); let mut fonts = Vec::with_capacity(1); @@ -1564,7 +1574,7 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result fonts.push(Font::BuiltinFont("serif")), "sans-serif" => fonts.push(Font::BuiltinFont("sans-serif")), "monospace" => fonts.push(Font::BuiltinFont("monospace")), - _ => return Err(FontFamilyParseError::UnrecognizedBuiltinFont(font)), + _ => return Err(CssFontFamilyParseError::UnrecognizedBuiltinFont(font)), } } } diff --git a/src/display_list.rs b/src/display_list.rs index abd23fa55..54ece1a68 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -30,7 +30,7 @@ const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 25 const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { - pub(crate) ui_descr: &'a UiDescription, + pub(crate) ui_descr: &'a UiDescription<'a, T>, pub(crate) rectangles: Arena> } @@ -42,7 +42,7 @@ pub(crate) struct DisplayRectangle<'a> { /// These two are completely seperate numbers! pub tag: Option, /// The original styled node - pub(crate) styled_node: &'a StyledNode, + pub(crate) styled_node: &'a StyledNode<'a>, /// The style properties of the node, parsed pub(crate) style: RectStyle<'a>, /// The layout properties of the node, parsed @@ -91,8 +91,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { let display_rect_arena = arena.transform(|node, node_id| { let style = ui_description.styled_nodes.get(&node_id).unwrap_or(&ui_description.default_style_of_node); let mut rect = DisplayRectangle::new(node.tag, style); - parse_css_style_properties(&mut rect); - parse_css_layout_properties(&mut rect); + populate_css_properties(&mut rect); rect }); @@ -374,7 +373,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { #[inline] fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { match style.background_color { - Some(bg) => builder.push_rect(&info, bg.into()), + Some(bg) => builder.push_rect(&info, bg.0.into()), None => builder.push_clear_rect(&info), } } @@ -441,7 +440,7 @@ fn push_text( let positioned_glyphs = text_layout::put_text_in_bounds( text, font, font_size, alignment, overflow_behaviour, bounds); - let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).into(); + let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); let flags = FontInstanceFlags::SUBPIXEL_BGR; let options = GlyphOptions { render_mode: FontRenderMode::Subpixel, @@ -622,66 +621,47 @@ fn push_font( } } -/// Internal helper function - gets a key from the constraint list and passes it through -/// the parse_func - if an error occurs, then the error gets printed -fn parse<'a, T, E: Debug>( - constraint_list: &'a CssConstraintList, - key: &'static str, - parse_func: fn(&'a str) -> Result) --> Option -{ - #[inline(always)] - fn print_error_debug(err: &E, key: &'static str) { - eprintln!("ERROR - invalid {:?}: {:?}", err, key); - } - - constraint_list.list.get(key).and_then(|w| - parse_func(w).map_err(|e| { - #[cfg(debug_assertions)] - print_error_debug(&e, key); - e - }).ok() - ) -} - /// Populate and parse the CSS style properties -fn parse_css_style_properties(rect: &mut DisplayRectangle) +fn populate_css_properties(rect: &mut DisplayRectangle) { - let constraint_list = &rect.styled_node.css_constraints; - - rect.style.border_radius = parse(constraint_list, "border-radius", parse_css_border_radius); - rect.style.background_color = parse(constraint_list, "background-color", parse_css_background_color); - rect.style.font_color = parse(constraint_list, "color", parse_css_text_color); - rect.style.border = parse(constraint_list, "border", parse_css_border); - rect.style.background = parse(constraint_list, "background", parse_css_background); - rect.style.font_size = parse(constraint_list, "font-size", parse_css_font_size); - rect.style.font_family = parse(constraint_list, "font-family", parse_css_font_family); - - if let Some(box_shadow_opt) = parse(constraint_list, "box-shadow", parse_css_box_shadow) { - rect.style.box_shadow = box_shadow_opt; + for constraint in &rect.styled_node.css_constraints.list { + use css::CssDeclaration::*; + match constraint { + Static(static_property) => { + use css_parser::ParsedCssProperty::*; + match static_property { + BorderRadius(b) => { rect.style.border_radius = Some(*b); }, + BackgroundColor(c) => { rect.style.background_color = Some(*c); }, + TextColor(t) => { rect.style.font_color = Some(*t); }, + Border(widths, details) => { rect.style.border = Some((*widths, *details)); }, + Background(b) => { rect.style.background = Some(b.clone()); }, + FontSize(f) => { rect.style.font_size = Some(*f); }, + FontFamily(f) => { rect.style.font_family = Some(f.clone()); }, + TextOverflow(to) => { rect.style.text_overflow = Some(*to); }, + TextAlign(ta) => { rect.style.text_align = Some(*ta); }, + BoxShadow(opt_box_shadow) => { rect.style.box_shadow = *opt_box_shadow; }, + + Width(w) => { rect.layout.width = Some(*w); }, + Height(h) => { rect.layout.height = Some(*h); }, + MinWidth(mw) => { rect.layout.min_width = Some(*mw); }, + MinHeight(mh) => { rect.layout.min_height = Some(*mh); }, + MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, + MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, + + FlexWrap(w) => { rect.layout.wrap = Some(*w); }, + FlexDirection(d) => { rect.layout.direction = Some(*d); }, + JustifyContent(j) => { rect.layout.justify_content = Some(*j); }, + AlignItems(a) => { rect.layout.align_items = Some(*a); }, + AlignContent(a) => { rect.layout.align_content = Some(*a); }, + } + }, + Dynamic(_) => { + // TODO + } + } } } -/// Populate and parse the CSS layout properties -fn parse_css_layout_properties(rect: &mut DisplayRectangle) -{ - let constraint_list = &rect.styled_node.css_constraints; - - rect.layout.width = parse(constraint_list, "width", parse_layout_width); - rect.layout.height = parse(constraint_list, "height", parse_layout_height); - rect.layout.min_width = parse(constraint_list, "min-width", parse_layout_min_width); - rect.layout.min_height = parse(constraint_list, "min-height", parse_layout_min_height); - - rect.layout.wrap = parse(constraint_list, "flex-wrap", parse_layout_wrap); - rect.layout.direction = parse(constraint_list, "flex-direction", parse_layout_direction); - rect.layout.justify_content = parse(constraint_list, "justify-content", parse_layout_justify_content); - rect.layout.align_items = parse(constraint_list, "align-items", parse_layout_align_items); - rect.layout.align_content = parse(constraint_list, "align-content", parse_layout_align_content); - - rect.style.text_overflow = parse(constraint_list, "overflow", parse_layout_text_overflow); - rect.style.text_align = parse(constraint_list, "text-align", parse_layout_text_align); -} - // Returns the constraints for one rectangle fn create_layout_constraints<'a>( rect: &DisplayRectangle, diff --git a/src/lib.rs b/src/lib.rs index 6f0a89f5b..a7df295ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,11 +102,11 @@ type FastHashSet = ::std::collections::HashSet, css: &mut Css) -> UiDescription where Self: Sized { + fn style_dom<'a>(dom: &Dom, css: &'a mut Css<'a>) -> UiDescription<'a, Self> where Self: Sized { css.is_dirty = false; match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) } @@ -94,8 +94,13 @@ impl<'a> ParsedCss<'a> { } } -fn match_dom_css_selectors(root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss, css: &Css, parent_z_level: u32) --> UiDescription +fn match_dom_css_selectors<'a, T: LayoutScreen>( + root: NodeId, + arena: &Rc>>>, + parsed_css: &ParsedCss<'a>, + css: &Css<'a>, + parent_z_level: u32) +-> UiDescription<'a, T> { let mut root_constraints = CssConstraintList::default(); for global_rule in &parsed_css.pure_global_rules { @@ -122,8 +127,14 @@ fn match_dom_css_selectors(root: NodeId, arena: &Rc(root: NodeId, arena: &Arena>, parsed_css: &ParsedCss, css: &Css, parent_constraints: &CssConstraintList, parent_z_level: u32) --> BTreeMap +fn match_dom_css_selectors_inner<'a, T: LayoutScreen>( + root: NodeId, + arena: &Arena>, + parsed_css: &ParsedCss<'a>, + css: &Css<'a>, + parent_constraints: &CssConstraintList<'a>, + parent_z_level: u32) +-> BTreeMap> { let mut styled_nodes = BTreeMap::::new(); @@ -146,8 +157,12 @@ fn match_dom_css_selectors_inner(root: NodeId, arena: &Arena(node: &NodeData, list: &mut CssConstraintList, parsed_css: &ParsedCss, css: &Css) { - +fn cascade_constraints<'a, T: LayoutScreen>( + node: &NodeData, + list: &mut CssConstraintList<'a>, + parsed_css: &ParsedCss<'a>, + css: &Css<'a>) +{ for div_rule in &parsed_css.pure_div_rules { if *node.node_type.get_css_identifier() == div_rule.html_type { push_rule(list, div_rule); @@ -196,6 +211,6 @@ fn cascade_constraints(node: &NodeData, list: &mut CssConstr } #[inline] -fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { - list.list.insert(rule.declaration.0.clone(), rule.declaration.1.clone()); +fn push_rule<'a>(list: &mut CssConstraintList<'a>, rule: &CssRule<'a>) { + list.list.push(rule.declaration.1.clone()); } \ No newline at end of file diff --git a/src/ui_description.rs b/src/ui_description.rs index 3025a8cc8..3e6741280 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -7,22 +7,23 @@ use dom::NodeData; use std::cell::RefCell; use std::rc::Rc; use std::collections::BTreeMap; +use css::CssDeclaration; -pub struct UiDescription { +pub struct UiDescription<'a, T: LayoutScreen> { pub(crate) ui_descr_arena: Rc>>>, /// ID of the root node of the arena (usually NodeId(0)) pub(crate) ui_descr_root: Option, /// This field is created from the Css parser - pub(crate) styled_nodes: BTreeMap, + pub(crate) styled_nodes: BTreeMap>, /// In the display list, we take references to the `UiDescription.styled_nodes` /// /// However, if there is no style, we want to have a default style applied /// and the reference to that style has to live as least as long as the `self.styled_nodes` /// This is why we need this field here - pub(crate) default_style_of_node: StyledNode, + pub(crate) default_style_of_node: StyledNode<'a>, } -impl Clone for UiDescription { +impl<'a, T: LayoutScreen> Clone for UiDescription<'a, T> { fn clone(&self) -> Self { Self { ui_descr_arena: self.ui_descr_arena.clone(), @@ -33,7 +34,7 @@ impl Clone for UiDescription { } } -impl Default for UiDescription { +impl<'a, T: LayoutScreen> Default for UiDescription<'a, T> { fn default() -> Self { Self { ui_descr_arena: Rc::new(RefCell::new(Arena::new())), @@ -44,22 +45,22 @@ impl Default for UiDescription { } } -impl UiDescription { - pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self +impl<'a, T: LayoutScreen> UiDescription<'a, T> { + pub fn from_ui_state(ui_state: &UiState, style: &'a mut Css<'a>) -> Self { T::style_dom(&ui_state.dom, style) } } -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct StyledNode { +#[derive(Debug, Default, Clone, PartialEq)] +pub(crate) struct StyledNode<'a> { /// The z-index level that we are currently on, 0 by default - pub z_level: u32, + pub(crate) z_level: u32, /// The CSS constraints, after the cascading step - pub css_constraints: CssConstraintList + pub(crate) css_constraints: CssConstraintList<'a> } -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct CssConstraintList { - pub list: FastHashMap +#[derive(Debug, Default, Clone, PartialEq)] +pub(crate) struct CssConstraintList<'a> { + pub(crate) list: Vec> } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 6d6962dec..b5d082055 100644 --- a/src/window.rs +++ b/src/window.rs @@ -390,7 +390,7 @@ pub struct Window<'a, T: LayoutScreen> { // The background thread that is running for this window. // pub(crate) background_thread: Option>, /// The css (how the current window is styled) - pub css: Css<'a>, + pub css: &'a mut Css<'a>, } /// Used in the solver, for the root constraint @@ -454,7 +454,7 @@ pub(crate) struct WindowInternal { impl<'a, T: LayoutScreen> Window<'a, T> { /// Creates a new window - pub fn new(options: WindowCreateOptions, css: Css<'a>) -> Result { + pub fn new(options: WindowCreateOptions, css: &'a mut Css<'a>) -> Result { let events_loop = EventsLoop::new(); From 14235ab4e6eb2fd4287dfa677c0e8f097aae8a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 07:55:10 +0200 Subject: [PATCH 054/868] Updated webrender (ResourceUpdates -> Vec) --- Cargo.toml | 2 +- src/app.rs | 2 -- src/display_list.rs | 44 +++++++++++++++++++++++++------------------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6a1e80519..3a9bc9c62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "bb354abbf84602d3d8357c63c4f0b1139ec4deb1" } +webrender = { git = "https://github.com/servo/webrender", rev = "60f417726a817051108a0124c8ba77fd9c6c52cf" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" diff --git a/src/app.rs b/src/app.rs index 19501d863..7cf2c5fea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -433,7 +433,6 @@ fn render( window.internal.last_display_list_builder = new_builder.finalize().2; } - let resources = ResourceUpdates::new(); let mut txn = Transaction::new(); txn.set_display_list( @@ -446,7 +445,6 @@ fn render( true, ); - txn.update_resources(resources); txn.set_root_pipeline(window.internal.pipeline_id); txn.generate_frame(); window.internal.api.send_transaction(window.internal.document_id, txn); diff --git a/src/display_list.rs b/src/display_list.rs index 54ece1a68..c49fc5b16 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -105,7 +105,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { fn update_resources( api: &RenderApi, app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + resource_updates: &mut Vec) { Self::update_image_resources(api, app_resources, resource_updates); Self::update_font_resources(api, app_resources, resource_updates); @@ -114,7 +114,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { fn update_image_resources( api: &RenderApi, app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + resource_updates: &mut Vec) { use images::{ImageState, ImageInfo}; @@ -137,7 +137,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // Remove any images that should be deleted for (resource_key, image_key) in to_delete_images.into_iter() { if let Some(image_key) = image_key { - resource_updates.delete_image(image_key); + resource_updates.push(ResourceUpdate::DeleteImage(image_key)); } app_resources.images.remove(&resource_key); } @@ -145,8 +145,12 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // Upload all remaining images to the GPU only if the haven't been // uploaded yet for (resource_key, (data, descriptor)) in updated_images.into_iter() { + let key = api.generate_image_key(); - resource_updates.add_image(key, descriptor, data, None); + resource_updates.push(ResourceUpdate::AddImage( + AddImage { key, descriptor, data, tiling: None } + )); + *app_resources.images.get_mut(&resource_key).unwrap() = ImageState::Uploaded(ImageInfo { key: key, @@ -160,7 +164,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { fn update_font_resources( api: &RenderApi, app_resources: &mut AppResources, - resource_updates: &mut ResourceUpdates) + resource_updates: &mut Vec) { use font::FontState; @@ -188,9 +192,9 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { for (resource_key, to_delete_instances) in to_delete_fonts.into_iter() { if let Some((font_key, font_instance_keys)) = to_delete_instances { for instance in font_instance_keys { - resource_updates.delete_font_instance(instance); + resource_updates.push(ResourceUpdate::DeleteFontInstance(instance)); } - resource_updates.delete_font(font_key); + resource_updates.push(ResourceUpdate::DeleteFont(font_key)); app_resources.fonts.remove(&font_key); } app_resources.font_data.remove(&resource_key); @@ -199,7 +203,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // Upload all remaining fonts to the GPU only if the haven't been uploaded yet for (resource_key, data) in updated_fonts.into_iter() { let key = api.generate_font_key(); - resource_updates.add_raw_font(key, data, 0); // TODO: use the index better? + resource_updates.push(ResourceUpdate::AddFont(AddFont::Raw(key, data, 0))); // TODO: use the index better? app_resources.font_data.get_mut(&resource_key).unwrap().1 = FontState::Uploaded(key); } } @@ -263,7 +267,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { let layout_size = ui_solver.window_dimensions.layout_size; let mut builder = DisplayListBuilder::with_capacity(pipeline_id, layout_size, self.rectangles.nodes_len()); - let mut resource_updates = ResourceUpdates::new(); + let mut resource_updates = Vec::::new(); let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; // Upload image and font resources @@ -388,7 +392,7 @@ fn push_text( app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, - resource_updates: &mut ResourceUpdates) + resource_updates: &mut Vec) { use dom::NodeType::*; use euclid::{TypedPoint2D, Length}; @@ -576,7 +580,7 @@ fn push_border( fn push_font( font_id: &css_parser::Font, font_size_app_units: Au, - resource_updates: &mut ResourceUpdates, + resource_updates: &mut Vec, app_resources: &mut AppResources, render_api: &RenderApi) -> Option @@ -600,14 +604,16 @@ fn push_font( let font_instance_key = font_sizes_hashmap.entry(font_size_app_units) .or_insert_with(|| { let f_instance_key = render_api.generate_font_instance_key(); - resource_updates.add_font_instance( - f_instance_key, - font_key, - font_size_app_units, - None, - None, - Vec::new(), - ); + resource_updates.push(ResourceUpdate::AddFontInstance( + AddFontInstance { + key: f_instance_key, + font_key: font_key, + glyph_size: font_size_app_units, + options: None, + platform_options: None, + variations: Vec::new(), + } + )); f_instance_key } ); From e8a8700669d72eb38b3f5dbe3930a2c661a60db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 08:07:48 +0200 Subject: [PATCH 055/868] Added codecov.yml (hopefully fixes codecov) --- codecov.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..e7bb8d289 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,26 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no \ No newline at end of file From c38a1d0177e31892c8f8b63d72c6fb290754eb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 10:06:06 +0200 Subject: [PATCH 056/868] Migrated to owned CSS (CSS owns the string) - Fixed compile errors - Added parsing for dynamic CSS properties The CSS string is now only parsed at startup and there is only one property that requires allocating a String / Vec (the background: property). So it makes sense to decouple the parsed CSS from the original CSS string (since it never changes during the runtime of the application). For creating animations, added parsing (and unit tests) for dynamic CSS IDs, like [[ my_id | 400px ]] - the CSS value can later be changed via .set("my_id", "500px") - but this doesn't require a re-parsing of the CSS values nor any cascading shenanigans. --- examples/debug.rs | 1 + examples/test_content.css | 2 +- src/app.rs | 6 +- src/css.rs | 159 ++++++++++++++++++++++++++++++-------- src/css_parser.rs | 71 +++++++---------- src/display_list.rs | 18 ++--- src/traits.rs | 26 +++---- src/ui_description.rs | 22 +++--- src/window.rs | 10 +-- 9 files changed, 199 insertions(+), 116 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7ae374331..95cb2283d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -33,6 +33,7 @@ fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen fn main() { + // Parse and validate the CSS let css = Css::new_from_string(TEST_CSS).unwrap(); let my_app_data = MyAppData { diff --git a/examples/test_content.css b/examples/test_content.css index 766c450b3..41f1f6af8 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -6,7 +6,7 @@ box-shadow: 0px 0px 3px #c5c5c5ad; /*background: image("Cat01");*/ background: linear-gradient(#fcfcfc, #efefef); - width: 200px; + width: [[ my_id | 200px ]]; height: 200px; min-height: 400px; min-width: 500px; diff --git a/src/app.rs b/src/app.rs index 7cf2c5fea..83472cde1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,7 @@ use webrender::api::RenderApi; /// Graphical application that maintains some kind of application state pub struct App<'a, T: LayoutScreen> { /// The graphical windows, indexed by ID - windows: Vec>, + windows: Vec>, /// The global application state pub app_state: Arc>>, } @@ -58,8 +58,8 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Spawn a new window on the screen. If an application has no windows, /// the [`run`](#method.run) function will exit immediately. - pub fn create_window(&mut self, options: WindowCreateOptions, css: &'a mut Css<'a>) -> Result<(), WindowCreateError> { - self.windows.push(Window::new(options, &mut *css)?); + pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { + self.windows.push(Window::new(options, css)?); Ok(()) } diff --git a/src/css.rs b/src/css.rs index fb39b5d61..014952847 100644 --- a/src/css.rs +++ b/src/css.rs @@ -17,12 +17,10 @@ const RELAYOUT_RULES: [&str; 13] = [ ]; /// Wrapper for a `Vec` - the CSS is immutable at runtime, it can only be -/// created once. Animations / conditional styling is implemented using dynamic fields (see ``) +/// created once. Animations / conditional styling is implemented using dynamic fields #[derive(Debug, Clone, PartialEq)] -pub struct Css<'a> { - // NOTE: Each time the rules are modified, the `dirty` flag - // has to be set accordingly for the CSS to update! - pub(crate) rules: Vec>, +pub struct Css { + pub(crate) rules: Vec, /* /// The dynamic properties that have to be set for this frame rules_to_change: FastHashMap, @@ -71,7 +69,7 @@ impl<'a> From> for CssParseError<'a> { /// The CSS rule is currently not cascaded, use `Css::new_from_string()` /// to do the cascading. #[derive(Debug, Clone, PartialEq)] -pub(crate) struct CssRule<'a> { +pub(crate) struct CssRule { /// `div` (`*` by default) pub html_type: String, /// `#myid` (`None` by default) @@ -79,13 +77,13 @@ pub(crate) struct CssRule<'a> { /// `.myclass .myotherclass` (vec![] by default) pub classes: Vec, /// `("justify-content", "center")` - pub declaration: (String, CssDeclaration<'a>), + pub declaration: (String, CssDeclaration), } #[derive(Debug, Clone, PartialEq)] -pub(crate) enum CssDeclaration<'a> { - Static(ParsedCssProperty<'a>), - Dynamic(DynamicCssProperty<'a>), +pub(crate) enum CssDeclaration { + Static(ParsedCssProperty), + Dynamic(DynamicCssProperty), } /// A `CssProperty` is a type of CSS Rule, @@ -105,12 +103,12 @@ pub(crate) enum CssDeclaration<'a> { /// now use the same API. /// #[derive(Debug, Clone, PartialEq)] -pub(crate) struct DynamicCssProperty<'a> { - default: ParsedCssProperty<'a>, - dynamic_id: String, +pub(crate) struct DynamicCssProperty { + pub(crate) dynamic_id: String, + pub(crate) default: ParsedCssProperty, } -impl<'a> CssRule<'a> { +impl CssRule { pub fn needs_relayout(&self) -> bool { // RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) // TODO @@ -118,7 +116,7 @@ impl<'a> CssRule<'a> { } } -impl<'a> Css<'a> { +impl Css { /// Creates an empty set of CSS rules pub fn empty() -> Self { @@ -130,7 +128,7 @@ impl<'a> Css<'a> { } /// Parses a CSS string (single-threaded) and returns the parsed rules - pub fn new_from_string(css_string: &'a str) -> Result { + pub fn new_from_string<'a>(css_string: &'a str) -> Result> { use simplecss::{Tokenizer, Token}; use std::collections::HashSet; @@ -162,7 +160,6 @@ impl<'a> Css<'a> { block_nesting += 1; }, Token::BlockEnd => { - println!("blockend!"); block_nesting -= 1; parser_in_block = false; current_type = "*"; @@ -188,8 +185,6 @@ impl<'a> Css<'a> { current_classes.insert(class); } Token::Declaration(key, val) => { - println!("declaration: key - {}\t\t| val - {}", key, val); - if !parser_in_block { return Err(CssParseError::MalformedCss); } @@ -198,7 +193,6 @@ impl<'a> Css<'a> { // // css_val = "center" | "{{ my_dynamic_id | center }}" let css_decl = determine_static_or_dynamic_css_property(key, val)?; - let mut css_rule = CssRule { html_type: current_type.to_string(), id: current_id.clone(), @@ -256,10 +250,12 @@ pub enum DynamicCssParseError<'a> { UnclosedBraces, /// There is a valid dynamic css property, but no default case NoDefaultCase, + /// The dynamic CSS property has no ID, i.e. `[[ 400px ]]` + NoId, /// The ID may not start with a number or be a CSS property itself InvalidId, - /// The "default" ID has to be the second ID, not the first one. - DefaultCaseNotSecond, + /// Dynamic css property braces are empty, i.e. `[[ ]]` + EmptyBraces, /// Unexpected value when parsing the string UnexpectedValue(CssParsingError<'a>), } @@ -273,41 +269,138 @@ impl<'a> From> for DynamicCssParseError<'a> { /// Determine if a Css property is static (immutable) or if it can change /// during the runtime of the program fn determine_static_or_dynamic_css_property<'a>(key: &'a str, value: &'a str) --> Result, DynamicCssParseError<'a>> +-> Result> { - // TODO: dynamic css declarations - Ok(CssDeclaration::Static(ParsedCssProperty::from_kv(key, value)?)) + let key = key.trim(); + let value = value.trim(); + + const START_BRACE: &str = "[["; + const END_BRACE: &str = "]]"; + + let is_starting_with_braces = value.starts_with(START_BRACE); + let is_ending_with_braces = value.ends_with(END_BRACE); + + match (is_starting_with_braces, is_ending_with_braces) { + (true, false) | (false, true) => { + Err(DynamicCssParseError::UnclosedBraces) + }, + (true, true) => { + + use std::char; + + // "[[ id | 400px ]]" => "id | 400px" + let value = value.trim_left_matches(START_BRACE); + let value = value.trim_right_matches(END_BRACE); + let value = value.trim(); + + let mut pipe_split = value.splitn(2, "|"); + let dynamic_id = pipe_split.next(); + let default_case = pipe_split.next(); + + // note: dynamic_id will always be Some(), which is why the + let (default_case, dynamic_id) = match (default_case, dynamic_id) { + (Some(default), Some(id)) => (default, id), + (None, Some(id)) => { + if id.trim().is_empty() { + return Err(DynamicCssParseError::EmptyBraces); + } else if ParsedCssProperty::from_kv(key, id).is_ok() { + // if there is an ID, but the ID is a CSS value + return Err(DynamicCssParseError::NoId); + } else { + return Err(DynamicCssParseError::NoDefaultCase); + } + }, + (None, None) | (Some(_), None) => unreachable!(), // iterator would be broken if this happened + }; + + let dynamic_id = dynamic_id.trim(); + let default_case = default_case.trim(); + + match (dynamic_id.is_empty(), default_case.is_empty()) { + (true, true) => return Err(DynamicCssParseError::EmptyBraces), + (true, false) => return Err(DynamicCssParseError::NoId), + (false, true) => return Err(DynamicCssParseError::NoDefaultCase), + (false, false) => { /* everything OK */ } + } + + if dynamic_id.starts_with(char::is_numeric) || + ParsedCssProperty::from_kv(key, dynamic_id).is_ok() { + return Err(DynamicCssParseError::InvalidId); + } + + let default_case_parsed = ParsedCssProperty::from_kv(key, default_case)?; + + Ok(CssDeclaration::Dynamic(DynamicCssProperty { + dynamic_id: dynamic_id.to_string(), + default: default_case_parsed, + })) + }, + (false, false) => { + Ok(CssDeclaration::Static(ParsedCssProperty::from_kv(key, value)?)) + } + } } #[test] fn test_detect_static_or_dynamic_property() { - use css_parser::TextAlignment; + use css_parser::{TextAlignment, PixelParseError, InvalidValueErr}; assert_eq!( determine_static_or_dynamic_css_property("text-align", " center "), Ok(CssDeclaration::Static(ParsedCssProperty::TextAlign(TextAlignment::Center))) ); assert_eq!( - determine_static_or_dynamic_css_property("text-align", "{{ 400px }}"), + determine_static_or_dynamic_css_property("text-align", "[[ 400px ]]"), Err(DynamicCssParseError::NoDefaultCase) ); - assert_eq!(determine_static_or_dynamic_css_property("text-align", "{{ 400px"), + assert_eq!(determine_static_or_dynamic_css_property("text-align", "[[ 400px"), Err(DynamicCssParseError::UnclosedBraces) ); assert_eq!( - determine_static_or_dynamic_css_property("text-align", "{{ 400px | 500px }}"), + determine_static_or_dynamic_css_property("text-align", "[[ 400px | center ]]"), Err(DynamicCssParseError::InvalidId) ); assert_eq!( - determine_static_or_dynamic_css_property("text-align", "{{ hello | 500px }}"), - Err(DynamicCssParseError::InvalidId) + determine_static_or_dynamic_css_property("text-align", "[[ hello | center ]]"), + Ok(CssDeclaration::Dynamic(DynamicCssProperty { + default: ParsedCssProperty::TextAlign(TextAlignment::Center), + dynamic_id: String::from("hello"), + })) ); assert_eq!( - determine_static_or_dynamic_css_property("text-align", "{{ 500px | hello }}"), - Err(DynamicCssParseError::InvalidId) + determine_static_or_dynamic_css_property("text-align", "[[ abc | hello ]]"), + Err(DynamicCssParseError::UnexpectedValue( + CssParsingError::InvalidValueErr(InvalidValueErr("hello")) + )) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "[[ ]]"), + Err(DynamicCssParseError::EmptyBraces) + ); + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "[[]]"), + Err(DynamicCssParseError::EmptyBraces) + ); + + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "[[ center ]]"), + Err(DynamicCssParseError::NoId) + ); + + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "[[ hello | ]]"), + Err(DynamicCssParseError::NoDefaultCase) + ); + + // debatable if this is a suitable error for this case: + assert_eq!( + determine_static_or_dynamic_css_property("text-align", "[[ | ]]"), + Err(DynamicCssParseError::EmptyBraces) ); } \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index 9b22b9ba1..73cc28d48 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -32,7 +32,7 @@ macro_rules! impl_from { /// Same as `impl_from`, but without lifetime annotations for `$a` macro_rules! impl_from_no_lifetimes { ($a:ident, $b:ident::$enum_type:ident) => ( - impl<'a> From<$a> for $b<'a> { + impl From<$a> for $b { fn from(e: $a) -> Self { $b::$enum_type(e) } @@ -68,12 +68,12 @@ macro_rules! typed_pixel_value_parser { /// A successfully parsed CSS property #[derive(Debug, Clone, PartialEq)] -pub(crate) enum ParsedCssProperty<'a> { +pub(crate) enum ParsedCssProperty { BorderRadius(BorderRadius), BackgroundColor(BackgroundColor), TextColor(TextColor), Border(BorderWidths, BorderDetails), - Background(Background<'a>), + Background(Background), FontSize(FontSize), FontFamily(FontFamily), TextOverflow(TextOverflowBehaviour), @@ -95,7 +95,7 @@ pub(crate) enum ParsedCssProperty<'a> { } impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); -impl_from!(Background, ParsedCssProperty::Background); +impl_from_no_lifetimes!(Background, ParsedCssProperty::Background); impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); @@ -117,22 +117,24 @@ impl_from_no_lifetimes!(LayoutAlignContent, ParsedCssProperty::AlignContent); impl_from_no_lifetimes!(BackgroundColor, ParsedCssProperty::BackgroundColor); impl_from_no_lifetimes!(TextColor, ParsedCssProperty::TextColor); -impl<'a> From<(BorderWidths, BorderDetails)> for ParsedCssProperty<'a> { +impl From<(BorderWidths, BorderDetails)> for ParsedCssProperty { fn from((widths, details): (BorderWidths, BorderDetails)) -> Self { ParsedCssProperty::Border(widths, details) } } -impl<'a> From> for ParsedCssProperty<'a> { +impl From> for ParsedCssProperty { fn from(box_shadow: Option) -> Self { ParsedCssProperty::BoxShadow(box_shadow) } } -impl<'a> ParsedCssProperty<'a> { +impl ParsedCssProperty { /// Main parsing function, takes a stringified key / value pair and either /// returns the parsed value or an error - pub fn from_kv(key: &'a str, value: &'a str) -> Result> { + pub fn from_kv<'a>(key: &'a str, value: &'a str) -> Result> { + let key = key.trim(); + let value = value.trim(); match key { "border-radius" => Ok(parse_css_border_radius(value)?.into()), "background-color" => Ok(parse_css_background_color(value)?.into()), @@ -192,27 +194,6 @@ impl_from!(CssFontFamilyParseError, CssParsingError::CssFontFamilyParseError); impl_from!(CssBackgroundParseError, CssParsingError::CssBackgroundParseError); impl_from!(CssBorderRadiusParseError, CssParsingError::CssBorderRadiusParseError); -/* -impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); -impl_from!(Background, ParsedCssProperty::Background); -impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); -impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); -impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); -impl_from_no_lifetimes!(TextAlignment, ParsedCssProperty::TextAlign); - -impl_from_no_lifetimes!(LayoutWidth, ParsedCssProperty::Width); -impl_from_no_lifetimes!(LayoutHeight, ParsedCssProperty::Height); -impl_from_no_lifetimes!(LayoutMinWidth, ParsedCssProperty::MinWidth); -impl_from_no_lifetimes!(LayoutMinHeight, ParsedCssProperty::MinHeight); -impl_from_no_lifetimes!(LayoutMaxWidth, ParsedCssProperty::MaxWidth); -impl_from_no_lifetimes!(LayoutMaxHeight, ParsedCssProperty::MaxHeight); - -impl_from_no_lifetimes!(LayoutWrap, ParsedCssProperty::FlexWrap); -impl_from_no_lifetimes!(LayoutDirection, ParsedCssProperty::FlexDirection); -impl_from_no_lifetimes!(LayoutJustifyContent, ParsedCssProperty::JustifyContent); -impl_from_no_lifetimes!(LayoutAlignItems, ParsedCssProperty::AlignItems); -impl_from_no_lifetimes!(LayoutAlignContent, ParsedCssProperty::AlignContent); -*/ impl<'a> From<(&'a str, &'a str)> for CssParsingError<'a> { fn from((a, b): (&'a str, &'a str)) -> Self { CssParsingError::UnsupportedCssKey(a, b) @@ -878,14 +859,14 @@ impl_from!(CssShapeParseError, CssBackgroundParseError::ShapeParseError); impl_from!(CssImageParseError, CssBackgroundParseError::ImageParseError); #[derive(Debug, Clone, PartialEq)] -pub enum Background<'a> { +pub enum Background { LinearGradient(LinearGradientPreInfo), RadialGradient(RadialGradientPreInfo), - Image(CssImageId<'a>) + Image(CssImageId) } -impl<'a> From> for Background<'a> { - fn from(id: CssImageId<'a>) -> Self { +impl<'a> From for Background { + fn from(id: CssImageId) -> Self { Background::Image(id) } } @@ -1179,19 +1160,27 @@ fn parse_css_background<'a>(input: &'a str) } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct CssImageId<'a>(pub(crate) &'a str); +/// Note: In theory, we could take a String here, +/// but this leads to horrible lifetime issues. Also +/// since we only parse the CSS once (at startup), +/// the performance is absolutely negligible. +/// +/// However, this allows the `Css` struct to be independent +/// of the original source text, i.e. the original CSS string +/// can be deallocated after successfully parsing it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CssImageId(pub(crate) String); -impl<'a> From> for CssImageId<'a> { +impl<'a> From> for CssImageId { fn from(input: QuoteStripped<'a>) -> Self { - CssImageId(input.0) + CssImageId(input.0.to_string()) } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) struct QuoteStripped<'a>(pub(crate) &'a str); -fn parse_image<'a>(input: &'a str) -> Result, CssImageParseError<'a>> { +fn parse_image<'a>(input: &'a str) -> Result> { Ok(strip_quotes(input)?.into()) } @@ -1473,13 +1462,13 @@ impl Default for TextAlignment { } #[derive(Default, Debug, Clone, PartialEq)] -pub(crate) struct RectStyle<'a> { +pub(crate) struct RectStyle { /// Background color of this rectangle pub(crate) background_color: Option, /// Shadow color pub(crate) box_shadow: Option, /// Gradient (location) + stops - pub(crate) background: Option>, + pub(crate) background: Option, /// Border pub(crate) border: Option<(BorderWidths, BorderDetails)>, /// Border radius @@ -2011,7 +2000,7 @@ mod css_tests { #[test] fn test_parse_background_image() { assert_eq!(parse_css_background("image(\"Cat 01\")"), Ok(Background::Image( - CssImageId("Cat 01") + CssImageId(String::from("Cat 01")) ))); } } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index c49fc5b16..64999db64 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -30,7 +30,7 @@ const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 25 const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { - pub(crate) ui_descr: &'a UiDescription<'a, T>, + pub(crate) ui_descr: &'a UiDescription, pub(crate) rectangles: Arena> } @@ -42,9 +42,9 @@ pub(crate) struct DisplayRectangle<'a> { /// These two are completely seperate numbers! pub tag: Option, /// The original styled node - pub(crate) styled_node: &'a StyledNode<'a>, + pub(crate) styled_node: &'a StyledNode, /// The style properties of the node, parsed - pub(crate) style: RectStyle<'a>, + pub(crate) style: RectStyle, /// The layout properties of the node, parsed pub(crate) layout: RectLayout, } @@ -518,8 +518,8 @@ fn push_background( None => return, }; - match *background { - Background::RadialGradient(ref gradient) => { + match background { + Background::RadialGradient(gradient) => { let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), @@ -530,7 +530,7 @@ fn push_background( let gradient = builder.create_radial_gradient(center, radius, stops, gradient.extend_mode); builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, - Background::LinearGradient(ref gradient) => { + Background::LinearGradient(gradient) => { let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), @@ -541,10 +541,10 @@ fn push_background( builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, Background::Image(image_id) => { - if let Some(image_info) = app_resources.images.get(image_id.0) { + if let Some(image_info) = app_resources.images.get(&image_id.0) { use images::ImageState::*; - match *image_info { - Uploaded(ref image_info) => { + match image_info { + Uploaded(image_info) => { builder.push_image( &info, bounds.size, diff --git a/src/traits.rs b/src/traits.rs index 20ab77b64..a6608e5bb 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -20,17 +20,17 @@ pub trait LayoutScreen { /// Applies the CSS styles to the nodes calculated from the `layout_screen` /// function and calculates the final display list that is submitted to the /// renderer. - fn style_dom<'a>(dom: &Dom, css: &'a mut Css<'a>) -> UiDescription<'a, Self> where Self: Sized { + fn style_dom(dom: &Dom, css: &mut Css) -> UiDescription where Self: Sized { css.is_dirty = false; match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) } } pub(crate) struct ParsedCss<'a> { - pub(crate) pure_global_rules: Vec<&'a CssRule<'a>>, - pub(crate) pure_div_rules: Vec<&'a CssRule<'a>>, - pub(crate) pure_class_rules: Vec<&'a CssRule<'a>>, - pub(crate) pure_id_rules: Vec<&'a CssRule<'a>>, + pub(crate) pure_global_rules: Vec<&'a CssRule>, + pub(crate) pure_div_rules: Vec<&'a CssRule>, + pub(crate) pure_class_rules: Vec<&'a CssRule>, + pub(crate) pure_id_rules: Vec<&'a CssRule>, } impl<'a> ParsedCss<'a> { @@ -98,9 +98,9 @@ fn match_dom_css_selectors<'a, T: LayoutScreen>( root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss<'a>, - css: &Css<'a>, + css: &Css, parent_z_level: u32) --> UiDescription<'a, T> +-> UiDescription { let mut root_constraints = CssConstraintList::default(); for global_rule in &parsed_css.pure_global_rules { @@ -131,10 +131,10 @@ fn match_dom_css_selectors_inner<'a, T: LayoutScreen>( root: NodeId, arena: &Arena>, parsed_css: &ParsedCss<'a>, - css: &Css<'a>, - parent_constraints: &CssConstraintList<'a>, + css: &Css, + parent_constraints: &CssConstraintList, parent_z_level: u32) --> BTreeMap> +-> BTreeMap { let mut styled_nodes = BTreeMap::::new(); @@ -159,9 +159,9 @@ fn match_dom_css_selectors_inner<'a, T: LayoutScreen>( #[allow(unused_variables)] fn cascade_constraints<'a, T: LayoutScreen>( node: &NodeData, - list: &mut CssConstraintList<'a>, + list: &mut CssConstraintList, parsed_css: &ParsedCss<'a>, - css: &Css<'a>) + css: &Css) { for div_rule in &parsed_css.pure_div_rules { if *node.node_type.get_css_identifier() == div_rule.html_type { @@ -211,6 +211,6 @@ fn cascade_constraints<'a, T: LayoutScreen>( } #[inline] -fn push_rule<'a>(list: &mut CssConstraintList<'a>, rule: &CssRule<'a>) { +fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { list.list.push(rule.declaration.1.clone()); } \ No newline at end of file diff --git a/src/ui_description.rs b/src/ui_description.rs index 3e6741280..b57f107f2 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -9,21 +9,21 @@ use std::rc::Rc; use std::collections::BTreeMap; use css::CssDeclaration; -pub struct UiDescription<'a, T: LayoutScreen> { +pub struct UiDescription { pub(crate) ui_descr_arena: Rc>>>, /// ID of the root node of the arena (usually NodeId(0)) pub(crate) ui_descr_root: Option, /// This field is created from the Css parser - pub(crate) styled_nodes: BTreeMap>, + pub(crate) styled_nodes: BTreeMap, /// In the display list, we take references to the `UiDescription.styled_nodes` /// /// However, if there is no style, we want to have a default style applied /// and the reference to that style has to live as least as long as the `self.styled_nodes` /// This is why we need this field here - pub(crate) default_style_of_node: StyledNode<'a>, + pub(crate) default_style_of_node: StyledNode, } -impl<'a, T: LayoutScreen> Clone for UiDescription<'a, T> { +impl Clone for UiDescription { fn clone(&self) -> Self { Self { ui_descr_arena: self.ui_descr_arena.clone(), @@ -34,7 +34,7 @@ impl<'a, T: LayoutScreen> Clone for UiDescription<'a, T> { } } -impl<'a, T: LayoutScreen> Default for UiDescription<'a, T> { +impl Default for UiDescription { fn default() -> Self { Self { ui_descr_arena: Rc::new(RefCell::new(Arena::new())), @@ -45,22 +45,22 @@ impl<'a, T: LayoutScreen> Default for UiDescription<'a, T> { } } -impl<'a, T: LayoutScreen> UiDescription<'a, T> { - pub fn from_ui_state(ui_state: &UiState, style: &'a mut Css<'a>) -> Self +impl UiDescription { + pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self { T::style_dom(&ui_state.dom, style) } } #[derive(Debug, Default, Clone, PartialEq)] -pub(crate) struct StyledNode<'a> { +pub(crate) struct StyledNode { /// The z-index level that we are currently on, 0 by default pub(crate) z_level: u32, /// The CSS constraints, after the cascading step - pub(crate) css_constraints: CssConstraintList<'a> + pub(crate) css_constraints: CssConstraintList } #[derive(Debug, Default, Clone, PartialEq)] -pub(crate) struct CssConstraintList<'a> { - pub(crate) list: Vec> +pub(crate) struct CssConstraintList { + pub(crate) list: Vec } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index b5d082055..69037bc66 100644 --- a/src/window.rs +++ b/src/window.rs @@ -371,7 +371,7 @@ impl Default for WindowMonitorTarget { } /// Represents one graphical window to be rendered -pub struct Window<'a, T: LayoutScreen> { +pub struct Window { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, // TODO: Migrate to the window_state for state diffing @@ -390,7 +390,7 @@ pub struct Window<'a, T: LayoutScreen> { // The background thread that is running for this window. // pub(crate) background_thread: Option>, /// The css (how the current window is styled) - pub css: &'a mut Css<'a>, + pub css: Css, } /// Used in the solver, for the root constraint @@ -451,10 +451,10 @@ pub(crate) struct WindowInternal { pub(crate) hidpi_factor: f32, } -impl<'a, T: LayoutScreen> Window<'a, T> { +impl Window { /// Creates a new window - pub fn new(options: WindowCreateOptions, css: &'a mut Css<'a>) -> Result { + pub fn new(options: WindowCreateOptions, css: Css) -> Result { let events_loop = EventsLoop::new(); @@ -641,7 +641,7 @@ impl<'a, T: LayoutScreen> Window<'a, T> { } } -impl<'a, T: LayoutScreen> Drop for Window<'a, T> { +impl Drop for Window { fn drop(&mut self) { // self.background_thread.take().unwrap().join(); let renderer = self.renderer.take().unwrap(); From df871ee709f66fbd3f5f5e3c6447a786b2c2ac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 10:17:52 +0200 Subject: [PATCH 057/868] Fixed .with_event to work correctly --- examples/debug.rs | 14 +++++--------- src/dom.rs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 95cb2283d..8fbe507fc 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -13,22 +13,18 @@ pub struct MyAppData { } impl LayoutScreen for MyAppData { - fn get_dom(&self, _window_id: WindowId) -> Dom { - - let mut dom = Dom::new(NodeType::Label { + Dom::new(NodeType::Label { text: format!("{}", self.my_data), - }); - dom.class("__azul-native-button"); - dom.event(On::MouseUp, Callback::Sync(my_button_click_handler)); - - dom + }) + .with_class("__azul-native-button") + .with_event(On::MouseUp, Callback::Sync(my_button_click_handler)) } } fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { app_state.data.my_data += 1; - UpdateScreen::DontRedraw + UpdateScreen::Redraw } fn main() { diff --git a/src/dom.rs b/src/dom.rs index 661a90f5b..3ad779d3c 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -464,7 +464,7 @@ impl Dom { /// Same as `event`, but easier to use for method chaining in a builder-style pattern #[inline] - pub fn with_event>(mut self, on: On, callback: Callback) -> Self { + pub fn with_event(mut self, on: On, callback: Callback) -> Self { self.event(on, callback); self } From 0f3583a57594605ea95fc1a23b715e2d0f32bfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 11:50:36 +0200 Subject: [PATCH 058/868] Fix codecov --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 26e94b69a..a26956816 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,6 @@ after_success: | sudo make install && cd ../.. && rm -rf kcov-master && - kcov –-coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo,/usr/lib --verify target/cov target/debug/azul-* && + kcov –-coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo,/usr/lib --verify target/debug/azul-* && bash <(curl -s https://codecov.io/bash) && echo "Uploaded code coverage" \ No newline at end of file From ef214c2fd3a3ffc7bde3d8714155ab8d2195f398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 15:49:35 +0200 Subject: [PATCH 059/868] Added Svg type to the NodeType --- examples/debug.rs | 8 +- src/display_list.rs | 13 +-- src/dom.rs | 222 ++++++++++++++++---------------------------- src/lib.rs | 14 +-- src/svg.rs | 138 ++++++++++++++++++++++++--- src/traits.rs | 15 ++- 6 files changed, 234 insertions(+), 176 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 8fbe507fc..8dfb223be 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -14,11 +14,9 @@ pub struct MyAppData { impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label { - text: format!("{}", self.my_data), - }) - .with_class("__azul-native-button") - .with_event(On::MouseUp, Callback::Sync(my_button_click_handler)) + Dom::new(NodeType::Label(format!("{}", self.my_data))) + .with_class("__azul-native-button") + .with_event(On::MouseUp, Callback::Sync(my_button_click_handler)) } } diff --git a/src/display_list.rs b/src/display_list.rs index 64999db64..acb011868 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -403,17 +403,8 @@ fn push_text( let arena = display_list.ui_descr.ui_descr_arena.borrow(); let text = match arena[rect_idx].data.node_type { - Div => return, - Label { ref text } => { - text - }, - _ => { - /// The display list should only ever handle divs and labels. - /// Everything more complex should be handled by a pre-processing step - eprintln!("got a NodeType in a DisplayList that wasn't a div or a label, this is a bug"); - // unreachable!(); - return; - } + Label(ref text) => text, + _ => return, }; if text.is_empty() { diff --git a/src/dom.rs b/src/dom.rs index 3ad779d3c..578584619 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,3 +1,4 @@ +use traits::GetCssId; use app_state::AppState; use traits::LayoutScreen; use std::collections::BTreeMap; @@ -96,84 +97,75 @@ impl Eq for Callback { } impl Copy for Callback { } +use traits::Widget; + /// List of allowed DOM node types that are supported by `azul`. /// /// All node types are purely convenience functions around `Div`, /// `Image` and `Label`. For example a `Ul` is simply a convenience /// wrapper around a repeated (`Div` + `Label`) clone where the first /// `Div` is shaped like a circle (for `Ul`). -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum NodeType /**/ { +pub enum NodeType { /// Regular div Div, - /// Image: The actual contents of the image are determined by the CSS - Image, /// A label that can be (optionally) be selectable with the mouse - Label { - /// Text of the label - text: String, - }, - /// Button - Button { - /// The text on the button - label: String, - }, - /// Unordered list - Ul, - /// Ordered list - Ol, - /// List item. Only valid if the parent is `NodeType::Ol` or `NodeType::Ul`. - Li, - /// This is more or less like a `GroupBox` in Visual Basic, draws a border - Form { - /// The text of the label - text: Option, - }, - /// Single-line text input - TextInput { - content: String, - placeholder: Option - }, - /// Multi line text input - TextEdit { - content: String, - placeholder: Option, - }, - /// A register-like tab - Tab { - label: String, - }, - /// Checkbox - Checkbox { - /// active - state: CheckboxState, - }, - /// Dropdown item - Dropdown { - items: Vec, - }, - /// Small (default yellow) tooltip for help - ToolTip { - title: String, - content: String, - }, - /// Password input, like the TextInput, but the items are rendered as dots - /// (if `use_dots` is active) - Password { - content: String, - placeholder: Option, - use_dots: bool, - }, - // Custom drawing component - //CustomDrawComponent(DrawComponent), -} -/* -/// State of a checkbox (disabled, checked, etc.) -#[derive(Debug, Clone, Hash)] -pub enum DrawComponent { + Label(String), Svg(Svg), + // GlTexture +} + +impl fmt::Debug for NodeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "NodeType<{}>", self.get_css_id()) + } +} + +impl Clone for NodeType { + fn clone(&self) -> Self { + use self::NodeType::*; + match self { + Div => Div, + Label(text) => Label(text.clone()), + Svg(svg) => Svg(svg.clone()), + } + } +} + +impl PartialEq for NodeType { + fn eq(&self, rhs: &Self) -> bool { + use self::NodeType::*; + match (self, rhs) { + (Div, Div) => true, + (Label(a), Label(b)) => a == b, + (Svg(a), Svg(b)) => *a == *b, + _ => false, + } + } +} + +impl Eq for NodeType { } + +impl Hash for NodeType { + fn hash(&self, state: &mut H) { + use self::NodeType::*; + match self { + Div => 0.hash(state), + Label(l) => { 1.hash(state); l.hash(state) }, + Svg(s) => { 2.hash(state); s.hash(state) }, + } + } +} + +impl GetCssId for NodeType { + fn get_css_id(&self) -> &'static str { + use self::NodeType::*; + match *self { + Div => "div", + Label(_) => "p", + Svg(_) => "svg", + } + } } -*/ /// State of a checkbox (disabled, checked, etc.) #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -194,62 +186,6 @@ pub enum CheckboxState { Unchecked } -impl /**/ NodeType /**/ { - - /// Get the CSS / HTML identifier "p", "ul", "li", etc. - /// - /// Full list of the types you can use in CSS: - /// - /// ```ignore - /// Div => "div" - /// Image => "img" - /// Button => "button" - /// Ul => "ul" - /// Ol => "ol" - /// Li => "li" - /// Label => "label" - /// Form => "form" - /// TextInput => "text-input" - /// TextEdit => "text-edit" - /// Tab => "tab" - /// Checkbox => "checkbox" - /// Color => "color" - /// Drowdown => "dropdown" - /// ToolTip => "tooltip" - /// Password => "password" - /// CustomDrawComponent::Svg => "svg" - /// CustomDrawComponent::GlTexture => "gltexture" - /// ``` - pub fn get_css_identifier(&self) -> &'static str { - use self::NodeType::*; - match *self { - Div => "div", - Image => "img", - Label { .. } => "label", - Button { .. } => "button", - Ul => "ul", - Ol => "ol", - Li => "li", - Form { .. } => "form", - TextInput { .. } => "text-input", - TextEdit { .. } => "text-edit", - Tab { .. } => "tab", - Checkbox { .. } => "checkbox", - Dropdown { .. } => "dropdown", - ToolTip { .. } => "tooltip", - Password { .. } => "password", - /* - CustomDrawComponent(c) => { - match c { - DrawComponent::Svg(_) => "svg", - DrawComponent::GlTexture(_) => "gltexture", - } - } - */ - } - } -} - /// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum On { @@ -269,7 +205,7 @@ pub enum On { #[derive(PartialEq, Eq)] pub(crate) struct NodeData { /// `div` - pub node_type: NodeType/**/, + pub node_type: NodeType, /// `#main` pub id: Option, /// `.myclass .otherclass` @@ -317,14 +253,19 @@ impl Clone for NodeData { impl fmt::Debug for NodeData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "NodeData {{ - node_type: {:?}, - id: {:?}, - classes: {:?}, - events: {:?}, - tag: {:?} -}}", - self.node_type, self.id, self.classes, self.events, self.tag) + write!(f, + "NodeData {{ \ + \tnode_type: {:?}, \ + \tid: {:?}, \ + \tclasses: {:?}, \ + \tevents: {:?}, \ + \ttag: {:?} \ + }}", + self.node_type, + self.id, + self.classes, + self.events, + self.tag) } } @@ -338,7 +279,7 @@ impl CallbackList { impl NodeData { /// Creates a new NodeData - pub fn new(node_type: NodeType/**/) -> Self { + pub fn new(node_type: NodeType) -> Self { Self { node_type: node_type, id: None, @@ -372,12 +313,13 @@ pub struct Dom { impl fmt::Debug for Dom { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Dom {{ - arena: {:?}, - root: {:?}, - current_root: {:?}, - last: {:?} -}}", + write!(f, + "Dom {{ \ + \tarena: {:?}, \ + \troot: {:?}, \ + \tcurrent_root: {:?}, \ + \tlast: {:?}, \ + }}", self.arena, self.root, self.current_root, @@ -416,7 +358,7 @@ impl Dom { /// Creates an empty DOM #[inline] - pub fn new(node_type: NodeType/**/) -> Self { + pub fn new(node_type: NodeType) -> Self { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new(node_type)); Self { diff --git a/src/lib.rs b/src/lib.rs index a7df295ac..156fc358a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,9 +29,9 @@ #[macro_use] pub extern crate glium; pub extern crate gleam; -pub extern crate euclid; pub extern crate image; +extern crate euclid; extern crate resvg; extern crate lyon; extern crate webrender; @@ -57,6 +57,12 @@ pub mod dom; pub mod traits; /// Window handling pub mod window; +/// Deamon / polling function implementation +pub mod deamon; +/// Async IO / task system +pub mod task; +/// SVG / path flattering module (lyon) +pub mod svg; /// Font & image resource handling, lookup and caching mod resources; /// Input handling (mostly glium) @@ -84,17 +90,11 @@ mod font; mod window_state; /// Application / context menu handling. Currently Win32 only. Also has parsing functions mod menu; -/// Deamon / polling function implementation -mod deamon; /// The compositor takes all textures (user-defined + the UI texture(s)) and draws them on /// top of each other mod compositor; /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) mod platform_ext; -/// Async IO / task system -mod task; -/// SVG / path flattering module (lyon) -mod svg; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; diff --git a/src/svg.rs b/src/svg.rs index a016ac498..6ac1458be 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,27 +1,99 @@ use dom::Callback; -use image::Rgba; use traits::LayoutScreen; use std::sync::atomic::AtomicUsize; +use FastHashMap; +use std::hash::{Hash, Hasher}; /// In order to store / compare SVG files, we have to pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct SvgId(usize); +pub(crate) struct SvgRegistry { + svg_items: FastHashMap, +} + +impl SvgRegistry { + pub fn add_shape(&mut self, polygon: SvgShape) -> SvgId { + // TODO + SvgId(0) + } + + pub fn delete_shape(&mut self, svg_id: SvgId) { + // TODO + } +} -#[derive(Debug, Clone)] pub struct Svg { pub layers: Vec>, } -#[derive(Debug, Clone)] +impl Clone for Svg { + fn clone(&self) -> Self { + Self { layers: self.layers.clone() } + } +} + +impl Hash for Svg { + fn hash(&self, state: &mut H) { + for layer in &self.layers { + layer.hash(state); + } + } +} + +impl PartialEq for Svg { + fn eq(&self, rhs: &Self) -> bool { + for (a, b) in self.layers.iter().zip(rhs.layers.iter()) { + if *a != *b { + return false + } + } + true + } +} + +impl Eq for Svg { } + pub struct SvgLayer { pub id: String, - pub data: Vec, + pub data: Vec, pub callbacks: SvgCallbacks, pub style: SvgStyle, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +impl Clone for SvgLayer { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + data: self.data.clone(), + callbacks: self.callbacks.clone(), + style: self.style.clone(), + } + } +} + +impl Hash for SvgLayer { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.data.hash(state); + self.callbacks.hash(state); + self.style.hash(state); + } +} + +impl PartialEq for SvgLayer { + fn eq(&self, rhs: &Self) -> bool { + self.id == rhs.id && + self.data == rhs.data && + self.callbacks == rhs.callbacks && + self.style == rhs.style + } +} + +impl Eq for SvgLayer { } + pub enum SvgCallbacks { // No callbacks for this layer None, @@ -29,13 +101,54 @@ pub enum SvgCallbacks { Any(Callback), /// Call the callback when the SvgLayer item at index [x] is /// hovered over / interacted with - Some(Vec<(usize, Callback)>), + Some(Vec<(SvgId, Callback)>), +} + +impl Clone for SvgCallbacks { + fn clone(&self) -> Self { + use self::SvgCallbacks::*; + match self { + None => None, + Any(c) => Any(c.clone()), + Some(v) => Some(v.clone()), + } + } +} + +impl Hash for SvgCallbacks { + fn hash(&self, state: &mut H) { + use self::SvgCallbacks::*; + match self { + None => 0.hash(state), + Any(c) => { Any(*c).hash(state); }, + Some(ref v) => { + 2.hash(state); + for (id, callback) in v { + id.hash(state); + callback.hash(state); + } + }, + } + } +} + +impl PartialEq for SvgCallbacks { + fn eq(&self, rhs: &Self) -> bool { + self == rhs + } +} + +impl Eq for SvgCallbacks { } + +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub struct Rgba { + pub data: [u8;4], } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Hash)] pub struct SvgStyle { - outline: Option>, - fill: Option>, + pub outline: Option, + pub fill: Option, } #[derive(Debug, Clone, PartialEq)] @@ -43,6 +156,7 @@ pub enum SvgShape { Polygon(Vec<(f32, f32)>), } +// SvgShape::Polygon(vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) impl Svg { pub fn default_testing() -> Self { @@ -50,11 +164,11 @@ impl Svg { layers: vec![SvgLayer { id: String::from("svg-layer-01"), // simple triangle for testing - data: vec![SvgShape::Polygon(vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)])], + data: vec![SvgId(0)], callbacks: SvgCallbacks::None, style: SvgStyle { - outline: Some(Rgba { data: [0.0, 0.0, 0.0, 1.0] }), - fill: Some(Rgba { data: [1.0, 0.0, 0.0, 1.0] }), + outline: Some(Rgba { data: [0, 0, 0, 255] }), + fill: Some(Rgba { data: [255, 0, 0, 255] }), } }] } diff --git a/src/traits.rs b/src/traits.rs index a6608e5bb..869de332e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -6,6 +6,7 @@ use window::WindowId; use id_tree::{NodeId, Arena}; use std::rc::Rc; use std::cell::RefCell; +use std::hash::Hash; pub trait LayoutScreen { /// Updates the DOM, must be provided by the final application. @@ -26,6 +27,17 @@ pub trait LayoutScreen { } } +/// Trait for any node type, registers a new top-level CSS id, i.e. +/// `body`, `div`, etc. for custom types +pub trait GetCssId { + /// Returns the top-level CSS identifier for this + fn get_css_id(&self) -> &'static str; +} + +pub trait Widget: GetCssId + Clone + PartialEq + Eq + Hash { } + +impl Widget for T { } + pub(crate) struct ParsedCss<'a> { pub(crate) pure_global_rules: Vec<&'a CssRule>, pub(crate) pure_div_rules: Vec<&'a CssRule>, @@ -164,7 +176,8 @@ fn cascade_constraints<'a, T: LayoutScreen>( css: &Css) { for div_rule in &parsed_css.pure_div_rules { - if *node.node_type.get_css_identifier() == div_rule.html_type { + use traits::GetCssId; + if *node.node_type.get_css_id() == div_rule.html_type { push_rule(list, div_rule); } } From a2ecd0fc65eba096b35d3c94eca4a26b7f4845be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 16:34:18 +0200 Subject: [PATCH 060/868] Migrated from Arc>> to Arc> For implementing the task system we can't send the whole AppState across threads. The AppState contains windows and GlContexts, which aren't thread-safe to access. So in the task system we can only send an Arc> and the rest of the operations happen on the main thread anyway. So it doesn't make sense to use an Arc> when we can't even send the AppState across threads. --- Cargo.toml | 1 + examples/debug.rs | 5 +++-- src/app.rs | 39 ++++++++++++++++----------------------- src/app_state.rs | 7 ++++--- src/deamon.rs | 5 +++-- src/dom.rs | 40 ++++++---------------------------------- src/lib.rs | 2 ++ src/ui_state.rs | 8 +++++++- 8 files changed, 42 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a9bc9c62..90fdd48a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" resvg = "0.2.0" lyon = { version = "0.10.0", features = ["extra"] } +lazy_static = "1.0.0" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/examples/debug.rs b/examples/debug.rs index 8dfb223be..b6d3c5fdf 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -16,12 +16,13 @@ impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { Dom::new(NodeType::Label(format!("{}", self.my_data))) .with_class("__azul-native-button") - .with_event(On::MouseUp, Callback::Sync(my_button_click_handler)) + .with_event(On::MouseUp, Callback(my_button_click_handler)) } } fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { - app_state.data.my_data += 1; + let mut app_state_lock = app_state.data.lock().unwrap(); + app_state_lock.my_data += 1; UpdateScreen::Redraw } diff --git a/src/app.rs b/src/app.rs index 83472cde1..b29f6ca72 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,7 +21,7 @@ pub struct App<'a, T: LayoutScreen> { /// The graphical windows, indexed by ID windows: Vec>, /// The global application state - pub app_state: Arc>>, + pub app_state: AppState<'a, T>, } pub(crate) struct FrameEventInfo { @@ -52,7 +52,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn new(initial_data: T) -> Self { Self { windows: Vec::new(), - app_state: Arc::new(Mutex::new(AppState::new(initial_data))), + app_state: AppState::new(initial_data), } } @@ -74,9 +74,8 @@ impl<'a, T: LayoutScreen> App<'a, T> { // first redraw, initialize cache { - let mut app_state = self.app_state.lock().unwrap(); for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); + ui_state_cache.push(UiState::from_app_state(&self.app_state, WindowId { id: idx })); } // First repaint, otherwise the window would be black on startup @@ -84,7 +83,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); render(window, &WindowId { id: idx, }, &ui_description_cache[idx], - &mut app_state.resources, + &mut self.app_state.resources, true); window.display.swap_buffers().unwrap(); } @@ -139,11 +138,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) // currently, just invoke all actions for callback_id in callback_list.values() { - use dom::Callback::*; - let update = match ui_state_cache[idx].callback_list[callback_id] { - Sync(callback) => { (callback)(&mut *self.app_state.lock().unwrap()) }, - Async(callback) => { (callback)(self.app_state.clone()) }, - }; + let update = (ui_state_cache[idx].callback_list[callback_id].0)(&mut self.app_state); if update == UpdateScreen::Redraw { should_update_screen = UpdateScreen::Redraw; } @@ -156,8 +151,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { } } - let mut app_state = self.app_state.lock().unwrap(); - ui_state_cache[idx] = UiState::from_app_state(&*app_state, WindowId { id: idx }); + ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowId { id: idx }); if window.css.is_dirty { frame_event_info.should_redraw_window = true; @@ -175,7 +169,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { render(window, ¤t_window_id, &ui_description_cache[idx], - &mut app_state.resources, + &mut self.app_state.resources, true); let time_end = ::std::time::Instant::now(); @@ -201,7 +195,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { render(window, ¤t_window_id, &ui_description_cache[idx], - &mut app_state.resources, + &mut self.app_state.resources, frame_event_info.new_window_size.is_some()); let time_end = ::std::time::Instant::now(); @@ -238,7 +232,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { - (*self.app_state.lock().unwrap()).add_image(id, data, image_type) + self.app_state.add_image(id, data, image_type) } /// Removes an image from the internal app resources. @@ -247,14 +241,14 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn delete_image>(&mut self, id: S) -> Option<()> { - (*self.app_state.lock().unwrap()).delete_image(id) + self.app_state.delete_image(id) } /// Checks if an image is currently registered and ready-to-use pub fn has_image>(&mut self, id: S) -> bool { - (*self.app_state.lock().unwrap()).has_image(id) + self.app_state.has_image(id) } /// Add a font (TTF or OTF) as a resource, identified by ID @@ -267,14 +261,14 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, FontError> { - (*self.app_state.lock().unwrap()).add_font(id, data) + self.app_state.add_font(id, data) } /// Checks if a font is currently registered and ready-to-use pub fn has_font>(&mut self, id: S) -> bool { - (*self.app_state.lock().unwrap()).has_font(id) + self.app_state.has_font(id) } /// Deletes a font from the internal app resources. @@ -323,7 +317,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn delete_font>(&mut self, id: S) -> Option<()> { - (*self.app_state.lock().unwrap()).delete_font(id) + self.app_state.delete_font(id) } /// Mock rendering function, for creating a hidden window and rendering one frame @@ -350,17 +344,16 @@ impl<'a, T: LayoutScreen> App<'a, T> { self.create_window(hidden_create_options, Css::native()).unwrap(); let mut ui_state_cache = Vec::with_capacity(self.windows.len()); let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - let mut app_state = self.app_state.lock().unwrap(); for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&*app_state, WindowId { id: idx })); + ui_state_cache.push(UiState::from_app_state(&self.app_state, WindowId { id: idx })); } for (idx, window) in self.windows.iter_mut().enumerate() { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); render(window, &WindowId { id: idx, }, &ui_description_cache[idx], - &mut app_state.resources, + &mut self.app_state.resources, true); window.display.swap_buffers().unwrap(); } diff --git a/src/app_state.rs b/src/app_state.rs index 63f161b1b..2e4ad4e20 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,13 +7,14 @@ use font::FontError; use std::collections::hash_map::Entry::*; use FastHashMap; use deamon::DeamonCallback; +use std::sync::{Arc, Mutex}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application /// should be laid out) pub struct AppState<'a, T: LayoutScreen> { /// Your data (the global struct which all callbacks will have access to) - pub data: T, + pub data: Arc>, /// Fonts and images that are currently loaded into the app pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) @@ -25,7 +26,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// Creates a new `AppState` pub fn new(initial_data: T) -> Self { Self { - data: initial_data, + data: Arc::new(Mutex::new(initial_data)), resources: AppResources::default(), deamons: FastHashMap::default(), } @@ -104,7 +105,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// impl LayoutScreen for MyAppData { /// fn get_dom(&self, _window_id: WindowId) -> Dom { /// let mut dom = Dom::new(NodeType::Div); - /// dom.event(On::MouseEnter, Callback::Sync(my_callback)); + /// dom.event(On::MouseEnter, Callback(my_callback)); /// dom /// } /// } diff --git a/src/deamon.rs b/src/deamon.rs index 17f1ffd93..8b0c544c2 100644 --- a/src/deamon.rs +++ b/src/deamon.rs @@ -5,7 +5,7 @@ use window::WindowId; use dom::UpdateScreen; pub struct DeamonCallback { - callback: fn(&mut T) -> UpdateScreen, + callback: fn(Arc>) -> UpdateScreen, } impl Clone for DeamonCallback @@ -19,7 +19,8 @@ impl Clone for DeamonCallback pub(crate) fn run_all_deamons(app_state: &mut AppState) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; for deamon in app_state.deamons.values().cloned() { - let should_update = (deamon.callback)(&mut app_state.data); + let arc_clone = app_state.data.clone(); + let should_update = (deamon.callback)(arc_clone); if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { should_update_screen = UpdateScreen::Redraw; diff --git a/src/dom.rs b/src/dom.rs index 578584619..d5212cf3c 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -34,35 +34,17 @@ pub enum UpdateScreen { /// The CSS is not affected by this, so if you push to the windows' CSS inside the /// function, the screen will not be automatically redrawn, unless you return an /// `UpdateScreen::Redraw` from the function -pub enum Callback { - /// One-off function (for ex. exporting a file) - /// - /// This is best for actions where you don't care if or when they complete. - /// Because you accept a Mutex, you can create a background thread - /// (azul won't create this for you) - Async(fn(Arc>>) -> UpdateScreen), - /// Same as the `FnOnceNonBlocking`, but it blocks the current - /// thread and does not require the type to be `Send`. - Sync(fn(&mut AppState) -> UpdateScreen), -} +pub struct Callback(pub fn(&mut AppState) -> UpdateScreen); impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::Callback::*; - match *self { - Async(func) => write!(f, "Callback::Async @ {:?}", func as usize), - Sync(func) => write!(f, "Callback::Sync @ {:?}", func as usize), - } + write!(f, "Callback @ {:x?}", self.0 as usize) } } -impl Clone for Callback -{ +impl Clone for Callback { fn clone(&self) -> Self { - match *self { - Callback::Async(ref f) => Callback::Async(f.clone()), - Callback::Sync(ref f) => Callback::Sync(f.clone()), - } + Callback(self.0.clone()) } } @@ -72,24 +54,14 @@ impl Clone for Callback /// than re-creating the whole DOM and serves as a caching mechanism. impl Hash for Callback { fn hash(&self, state: &mut H) where H: Hasher { - use self::Callback::*; - match *self { - Async(f) => { state.write_usize(f as usize); } - Sync(f) => { state.write_usize(f as usize); } - } + state.write_usize(self.0 as usize); } } /// Basically compares the function pointers and types for equality impl PartialEq for Callback { fn eq(&self, rhs: &Self) -> bool { - use self::Callback::*; - if let (Async(self_f), Async(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } else if let (Sync(self_f), Sync(other_f)) = (*self, *rhs) { - if self_f as usize == other_f as usize { return true; } - } - false + self.0 as usize == rhs.0 as usize } } diff --git a/src/lib.rs b/src/lib.rs index 156fc358a..fd7334c52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,8 @@ pub extern crate glium; pub extern crate gleam; pub extern crate image; +#[macro_use] +extern crate lazy_static; extern crate euclid; extern crate resvg; extern crate lyon; diff --git a/src/ui_state.rs b/src/ui_state.rs index 50494ee23..599848025 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -29,9 +29,15 @@ impl UiState { { use dom::{Dom, On}; - let dom: Dom = app_state.data.get_dom(window_id); + // Only shortly lock the data to get the dom out + let dom: Dom = { + let dom_lock = app_state.data.lock().unwrap(); + dom_lock.get_dom(window_id) + }; + unsafe { NODE_ID = 0 }; unsafe { CALLBACK_ID = 0 }; + let mut callback_list = BTreeMap::>::new(); let mut node_ids_to_callbacks_list = BTreeMap::>::new(); dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); From 95ca133548d3afead8735dc4e2202c44f9d0eddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 25 May 2018 18:40:43 +0200 Subject: [PATCH 061/868] Deamons / polling functions work now (+ updated example) Deamons are run after the windows have finished, TODO: force the redraw of all windows after an UpdateScreen::Redraw was received. --- examples/debug.rs | 19 +++++++++++++++++-- src/app.rs | 24 ++++++++++++++++++++++++ src/app_state.rs | 23 ++++++++++++++++++++--- src/deamon.rs | 25 ++++++++----------------- src/lib.rs | 4 ++-- 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index b6d3c5fdf..b504612be 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -21,11 +21,26 @@ impl LayoutScreen for MyAppData { } fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { - let mut app_state_lock = app_state.data.lock().unwrap(); - app_state_lock.my_data += 1; + + let should_start_deamon = { + let mut app_state_lock = app_state.data.lock().unwrap(); + app_state_lock.my_data += 1; + app_state_lock.my_data % 2 == 0 + }; + + if should_start_deamon { + app_state.add_deamon("hello", deamon_test_start); + } else { + app_state.delete_deamon("hello"); + } UpdateScreen::Redraw } +fn deamon_test_start(app_state: &mut MyAppData) -> UpdateScreen { + println!("Hello!"); + UpdateScreen::DontRedraw +} + fn main() { // Parse and validate the CSS diff --git a/src/app.rs b/src/app.rs index b29f6ca72..90afe5afb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,5 @@ +use dom::UpdateScreen; +use deamon::DeamonCallback; use css::Css; use resources::AppResources; use app_state::AppState; @@ -219,6 +221,11 @@ impl<'a, T: LayoutScreen> App<'a, T> { } ::std::thread::sleep(::std::time::Duration::from_millis(16)); } + + // Run deamons and remove them from + if self.app_state.run_all_deamons() == UpdateScreen::Redraw { + // TODO: What to do? + } } } @@ -320,6 +327,23 @@ impl<'a, T: LayoutScreen> App<'a, T> { self.app_state.delete_font(id) } + /// Create a deamon. Does nothing if a deamon with the same ID already exists. + /// + /// If the deamon was inserted, returns true, otherwise false + pub fn add_deamon>(&mut self, id: S, deamon: fn(&mut T) -> UpdateScreen) + -> bool + { + self.app_state.add_deamon(id, deamon) + } + + /// Remove a currently running deamon from running. Does nothing if there is + /// already a deamon with the same ID + pub fn delete_deamon>(&mut self, id: S) + -> bool + { + self.app_state.delete_deamon(id) + } + /// Mock rendering function, for creating a hidden window and rendering one frame /// Used in unit tests. You **have** to enable software rendering, otherwise, /// this function won't work in a headless environment. diff --git a/src/app_state.rs b/src/app_state.rs index 2e4ad4e20..c690a25ec 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use dom::UpdateScreen; use traits::LayoutScreen; use resources::{AppResources}; use std::io::Read; @@ -158,17 +159,33 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// Create a deamon. Does nothing if a deamon with the same ID already exists. /// /// If the deamon was inserted, returns true, otherwise false - pub fn add_deamon>(&mut self, id: S, deamon: DeamonCallback) -> bool { + pub fn add_deamon>(&mut self, id: S, deamon: fn(&mut T) -> UpdateScreen) -> bool { let id_string = id.into(); match self.deamons.entry(id_string) { Occupied(_) => false, - Vacant(v) => { v.insert(deamon); true }, + Vacant(v) => { v.insert(DeamonCallback::new(deamon)); true }, } } /// Remove a currently running deamon from running. Does nothing if there is /// already a deamon with the same ID - pub fn delete_deamon>(&mut self, id: S) -> bool { + pub fn delete_deamon>(&mut self, id: S) -> bool { self.deamons.remove(id.as_ref()).is_some() } + + /// Run all currently registered deamons + pub(crate) fn run_all_deamons(&self) -> UpdateScreen { + let mut should_update_screen = UpdateScreen::DontRedraw; + for deamon in self.deamons.values().cloned() { + let should_update = { + let mut lock = self.data.lock().unwrap(); + (deamon.callback)(&mut lock) + }; + if should_update == UpdateScreen::Redraw && + should_update_screen == UpdateScreen::DontRedraw { + should_update_screen = UpdateScreen::Redraw; + } + } + should_update_screen + } } diff --git a/src/deamon.rs b/src/deamon.rs index 8b0c544c2..c39caef92 100644 --- a/src/deamon.rs +++ b/src/deamon.rs @@ -4,27 +4,18 @@ use traits::LayoutScreen; use window::WindowId; use dom::UpdateScreen; -pub struct DeamonCallback { - callback: fn(Arc>) -> UpdateScreen, +pub(crate) struct DeamonCallback { + pub(crate) callback: fn(&mut T) -> UpdateScreen, } -impl Clone for DeamonCallback -{ - fn clone(&self) -> Self { - Self { callback: self.callback.clone() } +impl DeamonCallback { + pub(crate) fn new(callback: fn(&mut T) -> UpdateScreen) -> Self { + Self { callback } } } -/// Run all currently registered deamons on an `Arc>` -pub(crate) fn run_all_deamons(app_state: &mut AppState) -> UpdateScreen { - let mut should_update_screen = UpdateScreen::DontRedraw; - for deamon in app_state.deamons.values().cloned() { - let arc_clone = app_state.data.clone(); - let should_update = (deamon.callback)(arc_clone); - if should_update == UpdateScreen::Redraw && - should_update_screen == UpdateScreen::DontRedraw { - should_update_screen = UpdateScreen::Redraw; - } +impl Clone for DeamonCallback { + fn clone(&self) -> Self { + Self { callback: self.callback.clone() } } - should_update_screen } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index fd7334c52..7bf714f4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,8 +59,6 @@ pub mod dom; pub mod traits; /// Window handling pub mod window; -/// Deamon / polling function implementation -pub mod deamon; /// Async IO / task system pub mod task; /// SVG / path flattering module (lyon) @@ -97,6 +95,8 @@ mod menu; mod compositor; /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) mod platform_ext; +/// Deamon / polling function implementation +mod deamon; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; From 3ac3c5d40a41e8c331afb9f7b5863509b1822712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 11:57:04 +0200 Subject: [PATCH 062/868] Removed DeamonCallback, just use a function pointer --- src/app.rs | 1 - src/app_state.rs | 11 ++++------- src/deamon.rs | 21 --------------------- src/lib.rs | 2 -- 4 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 src/deamon.rs diff --git a/src/app.rs b/src/app.rs index 90afe5afb..8172483a9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,4 @@ use dom::UpdateScreen; -use deamon::DeamonCallback; use css::Css; use resources::AppResources; use app_state::AppState; diff --git a/src/app_state.rs b/src/app_state.rs index c690a25ec..e34a6c92c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,7 +7,6 @@ use image::ImageError; use font::FontError; use std::collections::hash_map::Entry::*; use FastHashMap; -use deamon::DeamonCallback; use std::sync::{Arc, Mutex}; /// Wrapper for your application data. In order to be layout-able, @@ -19,7 +18,7 @@ pub struct AppState<'a, T: LayoutScreen> { /// Fonts and images that are currently loaded into the app pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) - pub(crate) deamons: FastHashMap>, + pub(crate) deamons: FastHashMap UpdateScreen>, } impl<'a, T: LayoutScreen> AppState<'a, T> { @@ -163,7 +162,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { let id_string = id.into(); match self.deamons.entry(id_string) { Occupied(_) => false, - Vacant(v) => { v.insert(DeamonCallback::new(deamon)); true }, + Vacant(v) => { v.insert(deamon); true }, } } @@ -176,11 +175,9 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// Run all currently registered deamons pub(crate) fn run_all_deamons(&self) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; + let mut lock = self.data.lock().unwrap(); for deamon in self.deamons.values().cloned() { - let should_update = { - let mut lock = self.data.lock().unwrap(); - (deamon.callback)(&mut lock) - }; + let should_update = (deamon)(&mut lock); if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { should_update_screen = UpdateScreen::Redraw; diff --git a/src/deamon.rs b/src/deamon.rs deleted file mode 100644 index c39caef92..000000000 --- a/src/deamon.rs +++ /dev/null @@ -1,21 +0,0 @@ -use app_state::AppState; -use std::sync::{Arc, Mutex}; -use traits::LayoutScreen; -use window::WindowId; -use dom::UpdateScreen; - -pub(crate) struct DeamonCallback { - pub(crate) callback: fn(&mut T) -> UpdateScreen, -} - -impl DeamonCallback { - pub(crate) fn new(callback: fn(&mut T) -> UpdateScreen) -> Self { - Self { callback } - } -} - -impl Clone for DeamonCallback { - fn clone(&self) -> Self { - Self { callback: self.callback.clone() } - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7bf714f4a..8cb1ed1d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,8 +95,6 @@ mod menu; mod compositor; /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) mod platform_ext; -/// Deamon / polling function implementation -mod deamon; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; From 19e441f06fa67f8c9c9439c269e327d3d9a0194f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 12:16:14 +0200 Subject: [PATCH 063/868] Add working Task system --- src/app.rs | 11 +++++++++++ src/app_state.rs | 19 +++++++++++++++++++ src/task.rs | 10 +++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8172483a9..b0ab56148 100644 --- a/src/app.rs +++ b/src/app.rs @@ -225,6 +225,9 @@ impl<'a, T: LayoutScreen> App<'a, T> { if self.app_state.run_all_deamons() == UpdateScreen::Redraw { // TODO: What to do? } + + // Clean up finished tasks + self.app_state.clean_up_finished_tasks(); } } @@ -383,6 +386,14 @@ impl<'a, T: LayoutScreen> App<'a, T> { } } +impl<'a, T: LayoutScreen + Send + 'static> App<'a, T> { + /// Tasks, once started, cannot be stopped, which is why there is no `.delete()` function + pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) + { + self.app_state.add_task(callback); + } +} + fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { use glium::glutin::WindowEvent; match event { diff --git a/src/app_state.rs b/src/app_state.rs index e34a6c92c..e73776d71 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use task::Task; use dom::UpdateScreen; use traits::LayoutScreen; use resources::{AppResources}; @@ -19,6 +20,8 @@ pub struct AppState<'a, T: LayoutScreen> { pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) pub(crate) deamons: FastHashMap UpdateScreen>, + /// Currently running tasks (asynchronous functions running on a different thread) + pub(crate) tasks: Vec } impl<'a, T: LayoutScreen> AppState<'a, T> { @@ -29,6 +32,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { data: Arc::new(Mutex::new(initial_data)), resources: AppResources::default(), deamons: FastHashMap::default(), + tasks: Vec::new(), } } @@ -185,4 +189,19 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { } should_update_screen } + + /// Remove all tasks that have finished executing + pub(crate) fn clean_up_finished_tasks(&mut self) + { + self.tasks.retain(|x| x.is_finished()); + } } + +impl<'a, T: LayoutScreen + Send + 'static> AppState<'a, T> { + /// Tasks, once started, cannot be stopped + pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) + { + let task = Task::new(&self.data, callback); + self.tasks.push(task); + } +} \ No newline at end of file diff --git a/src/task.rs b/src/task.rs index 727df98a4..4dd2ecfc7 100644 --- a/src/task.rs +++ b/src/task.rs @@ -12,8 +12,12 @@ pub struct Task { } impl Task { - pub fn new(app_state: Arc>, callback: fn(Arc>, Arc<()>) -> ()) -> Self { - + pub fn new( + app_state: &Arc>, + callback: fn(Arc>, Arc<()>)) + -> Self + where T: LayoutScreen + Send + 'static + { let thread_check = Arc::new(()); let thread_weak = Arc::downgrade(&thread_check); let app_state_clone = app_state.clone(); @@ -28,7 +32,7 @@ impl Task { } } - pub fn is_ready(&self) -> bool { + pub fn is_finished(&self) -> bool { self.dropcheck.upgrade().is_none() } } From f8b0904947f0df9062fdb29bb662754b88b9acbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 20:52:58 +0200 Subject: [PATCH 064/868] Make dynamic CSS properties do something, add window state diffing, prepare asynchronous window updates Currently, the design is to let the user set properties on the `AppState`, and then, after the redraw, check what windows need updating / if there are new windows and set the new CSS properties. For this, we want to decouple the actual window from the state of the window and the dynamic CSS properties from the window. So we expose a Vec on the AppState, which the user can freely modify during the callbacks. Afterwards, we diff the state and check for changes. This is also a preparation for CSS hover events (which are going to be implemented using special dynamic CSS IDs). --- examples/debug.rs | 1 + examples/test_content.css | 1 - src/app.rs | 4 - src/app_state.rs | 10 +- src/css.rs | 16 +-- src/display_list.rs | 69 +++++++------ src/dom.rs | 2 - src/input.rs | 103 -------------------- src/lib.rs | 8 +- src/traits.rs | 10 +- src/ui_description.rs | 7 +- src/ui_state.rs | 11 ++- src/window.rs | 200 +++++++++++++++++++------------------- src/window_state.rs | 44 ++++----- 14 files changed, 196 insertions(+), 290 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index b504612be..0e97ad902 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -22,6 +22,7 @@ impl LayoutScreen for MyAppData { fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { + // app_state.data[event.window_id].css.set_dynamic_property("my_id", ("color", "orange).into()); let should_start_deamon = { let mut app_state_lock = app_state.data.lock().unwrap(); app_state_lock.my_data += 1; diff --git a/examples/test_content.css b/examples/test_content.css index 41f1f6af8..663b7c10a 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -9,7 +9,6 @@ width: [[ my_id | 200px ]]; height: 200px; min-height: 400px; - min-width: 500px; text-align: center; flex-direction: row; flex-wrap: nowrap; diff --git a/src/app.rs b/src/app.rs index b0ab56148..d9e7ccc46 100644 --- a/src/app.rs +++ b/src/app.rs @@ -154,10 +154,6 @@ impl<'a, T: LayoutScreen> App<'a, T> { ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowId { id: idx }); - if window.css.is_dirty { - frame_event_info.should_redraw_window = true; - } - // Macro to avoid duplication between the new_window_size and the new_dpi_factor event // TODO: refactor this into proper functions (when the WindowState is working) macro_rules! update_display { diff --git a/src/app_state.rs b/src/app_state.rs index e73776d71..67e5e5a6d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use window_state::WindowState; use task::Task; use dom::UpdateScreen; use traits::LayoutScreen; @@ -16,12 +17,18 @@ use std::sync::{Arc, Mutex}; pub struct AppState<'a, T: LayoutScreen> { /// Your data (the global struct which all callbacks will have access to) pub data: Arc>, + /// Note: this isn't the real window state. This is a "mock" window state which + /// can be modified by the user, i.e: + /// ```no_run,ignore + /// app_state.windows[event.window_id].css.set_dynamic_property("my_id", ("color", "orange).into()); + /// ``` + pub windows: Vec, /// Fonts and images that are currently loaded into the app pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) pub(crate) deamons: FastHashMap UpdateScreen>, /// Currently running tasks (asynchronous functions running on a different thread) - pub(crate) tasks: Vec + pub(crate) tasks: Vec, } impl<'a, T: LayoutScreen> AppState<'a, T> { @@ -30,6 +37,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { pub fn new(initial_data: T) -> Self { Self { data: Arc::new(Mutex::new(initial_data)), + windows: Vec::new(), resources: AppResources::default(), deamons: FastHashMap::default(), tasks: Vec::new(), diff --git a/src/css.rs b/src/css.rs index 014952847..d76a951b6 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,4 +1,5 @@ //! CSS parsing and styling +use FastHashMap; use std::ops::Add; use css_parser::{ParsedCssProperty, CssParsingError}; @@ -21,11 +22,11 @@ const RELAYOUT_RULES: [&str; 13] = [ #[derive(Debug, Clone, PartialEq)] pub struct Css { pub(crate) rules: Vec, - /* - /// The dynamic properties that have to be set for this frame - rules_to_change: FastHashMap, - */ - pub(crate) is_dirty: bool, + /// The dynamic properties that have to be overridden for this frame + /// + /// - `String`: The ID of the dynamic property + /// - `ParsedCssProperty`: What to override it with + pub(crate) dynamic_css_overrides: FastHashMap, /// Has the CSS changed in a way where it needs a re-layout? /// /// Ex. if only a background color has changed, we need to redraw, but we @@ -122,8 +123,8 @@ impl Css { pub fn empty() -> Self { Self { rules: Vec::new(), - is_dirty: false, needs_relayout: false, + dynamic_css_overrides: FastHashMap::default(), } } @@ -219,10 +220,9 @@ impl Css { Ok(Self { rules: css_rules, - // force repaint for the first frame - is_dirty: true, // force re-layout for the first frame needs_relayout: true, + dynamic_css_overrides: FastHashMap::default(), }) } diff --git a/src/display_list.rs b/src/display_list.rs index acb011868..df30d65a7 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -91,7 +91,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { let display_rect_arena = arena.transform(|node, node_id| { let style = ui_description.styled_nodes.get(&node_id).unwrap_or(&ui_description.default_style_of_node); let mut rect = DisplayRectangle::new(node.tag, style); - populate_css_properties(&mut rect); + populate_css_properties(&mut rect, &ui_description.dynamic_css_overrides); rect }); @@ -619,41 +619,46 @@ fn push_font( } /// Populate and parse the CSS style properties -fn populate_css_properties(rect: &mut DisplayRectangle) +fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHashMap) { + use css_parser::ParsedCssProperty::{self, *}; + + fn apply_parsed_css_property(rect: &mut DisplayRectangle, property: &ParsedCssProperty) { + match property { + BorderRadius(b) => { rect.style.border_radius = Some(*b); }, + BackgroundColor(c) => { rect.style.background_color = Some(*c); }, + TextColor(t) => { rect.style.font_color = Some(*t); }, + Border(widths, details) => { rect.style.border = Some((*widths, *details)); }, + Background(b) => { rect.style.background = Some(b.clone()); }, + FontSize(f) => { rect.style.font_size = Some(*f); }, + FontFamily(f) => { rect.style.font_family = Some(f.clone()); }, + TextOverflow(to) => { rect.style.text_overflow = Some(*to); }, + TextAlign(ta) => { rect.style.text_align = Some(*ta); }, + BoxShadow(opt_box_shadow) => { rect.style.box_shadow = *opt_box_shadow; }, + + Width(w) => { rect.layout.width = Some(*w); }, + Height(h) => { rect.layout.height = Some(*h); }, + MinWidth(mw) => { rect.layout.min_width = Some(*mw); }, + MinHeight(mh) => { rect.layout.min_height = Some(*mh); }, + MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, + MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, + + FlexWrap(w) => { rect.layout.wrap = Some(*w); }, + FlexDirection(d) => { rect.layout.direction = Some(*d); }, + JustifyContent(j) => { rect.layout.justify_content = Some(*j); }, + AlignItems(a) => { rect.layout.align_items = Some(*a); }, + AlignContent(a) => { rect.layout.align_content = Some(*a); }, + } + } + for constraint in &rect.styled_node.css_constraints.list { use css::CssDeclaration::*; match constraint { - Static(static_property) => { - use css_parser::ParsedCssProperty::*; - match static_property { - BorderRadius(b) => { rect.style.border_radius = Some(*b); }, - BackgroundColor(c) => { rect.style.background_color = Some(*c); }, - TextColor(t) => { rect.style.font_color = Some(*t); }, - Border(widths, details) => { rect.style.border = Some((*widths, *details)); }, - Background(b) => { rect.style.background = Some(b.clone()); }, - FontSize(f) => { rect.style.font_size = Some(*f); }, - FontFamily(f) => { rect.style.font_family = Some(f.clone()); }, - TextOverflow(to) => { rect.style.text_overflow = Some(*to); }, - TextAlign(ta) => { rect.style.text_align = Some(*ta); }, - BoxShadow(opt_box_shadow) => { rect.style.box_shadow = *opt_box_shadow; }, - - Width(w) => { rect.layout.width = Some(*w); }, - Height(h) => { rect.layout.height = Some(*h); }, - MinWidth(mw) => { rect.layout.min_width = Some(*mw); }, - MinHeight(mh) => { rect.layout.min_height = Some(*mh); }, - MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, - MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, - - FlexWrap(w) => { rect.layout.wrap = Some(*w); }, - FlexDirection(d) => { rect.layout.direction = Some(*d); }, - JustifyContent(j) => { rect.layout.justify_content = Some(*j); }, - AlignItems(a) => { rect.layout.align_items = Some(*a); }, - AlignContent(a) => { rect.layout.align_content = Some(*a); }, - } - }, - Dynamic(_) => { - // TODO + Static(static_property) => apply_parsed_css_property(rect, static_property), + Dynamic(dynamic_property) => { + let calculated_property = css_overrides.get(&dynamic_property.dynamic_id) + .unwrap_or(&dynamic_property.default); + apply_parsed_css_property(rect, calculated_property); } } } diff --git a/src/dom.rs b/src/dom.rs index d5212cf3c..0e6354730 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -69,8 +69,6 @@ impl Eq for Callback { } impl Copy for Callback { } -use traits::Widget; - /// List of allowed DOM node types that are supported by `azul`. /// /// All node types are purely convenience functions around `Div`, diff --git a/src/input.rs b/src/input.rs index a4bee5aa6..2ba730eed 100644 --- a/src/input.rs +++ b/src/input.rs @@ -3,106 +3,3 @@ use webrender::api::{HitTestResult, PipelineId, DocumentId, HitTestFlags, Render pub fn hit_test_ui(api: &RenderApi, document_id: DocumentId, pipeline_id: Option, point: WorldPoint) -> HitTestResult { api.hit_test(document_id, pipeline_id, point, HitTestFlags::FIND_ALL) } - -use std::time::{Instant, Duration}; -use glium::glutin::{MouseCursor, VirtualKeyCode}; - -/// Determines which keys are pressed currently (modifiers, etc.) -#[derive(Debug, Clone)] -pub struct KeyboardState -{ - /// Modifier keys that are currently actively pressed during this cycle - pub modifiers: Vec, - /// Hidden keys, such as the "n" in CTRL + n. Always lowercase - pub hidden_keys: Vec, - /// Actual keys pressed during this cycle (i.e. regular text input) - pub keys: Vec, -} - -impl KeyboardState -{ - pub fn new() -> Self - { - Self { - modifiers: Vec::new(), - hidden_keys: Vec::new(), - keys: Vec::new(), - } - } -} - -/// Mouse position on the screen -#[derive(Debug, Copy, Clone)] -pub struct MouseState -{ - /// Current mouse cursor type - pub mouse_cursor_type: MouseCursor, - //// Where the mouse cursor is. None if the window is not focused - pub mouse_cursor: Option<(i32, i32)>, - //// Is the left MB down? - pub left_down: bool, - //// Is the right MB down? - pub right_down: bool, - //// Is the middle MB down? - pub middle_down: bool, - /// How far has the mouse scrolled in x direction? - pub mouse_scroll_x: f32, - /// How far has the mouse scrolled in y direction? - pub mouse_scroll_y: f32, -} - -impl MouseState -{ - /// Creates a new mouse state - /// Input: How fast the scroll (mouse) should be converted into pixels - /// Usually around 10.0 (10 pixels per mouse wheel line) - pub fn new() -> Self - { - MouseState { - mouse_cursor_type: MouseCursor::Default, - mouse_cursor: Some((0, 0)), - left_down: false, - right_down: false, - middle_down: false, - mouse_scroll_x: 0.0, - mouse_scroll_y: 0.0, - } - } -} - -/// State, size, etc of the window, for comparing to the last frame -#[derive(Debug, Clone)] -pub struct WindowState -{ - /// The state of the keyboard - pub(crate) keyboard_state: KeyboardState, - /// The state of the mouse - pub(crate) mouse_state: MouseState, - /// Width of the window - pub width: u32, - /// Height of the window - pub height: u32, - /// Time of the last rendering update, set after the `redraw()` method - pub time_of_last_update: Instant, - /// Minimum frame time - pub min_frame_time: Duration, -} - -impl WindowState -{ - /// Creates a new window state - pub fn new( - width: u32, - height: u32, - ) -> Self - { - Self { - keyboard_state: KeyboardState::new(), - mouse_state: MouseState::new(), - width, - height, - time_of_last_update: Instant::now(), - min_frame_time: Duration::from_millis(16), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 8cb1ed1d2..533ba81c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,10 +110,10 @@ pub mod prelude { pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; pub use traits::LayoutScreen; pub use webrender::api::{ColorF, ColorU}; - pub use window::{MonitorIter, Window, WindowCreateOptions, - WindowId, WindowPlacement}; - pub use window::{MouseMode, UpdateBehaviour, UpdateMode, WindowClass, - WindowCreateError, WindowDecorations, WindowMonitorTarget, RendererType}; + pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, + MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, + WindowMonitorTarget, RendererType}; + pub use window_state::WindowState; pub use font::FontError; pub use images::ImageType; diff --git a/src/traits.rs b/src/traits.rs index 869de332e..6625f2869 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -21,8 +21,7 @@ pub trait LayoutScreen { /// Applies the CSS styles to the nodes calculated from the `layout_screen` /// function and calculates the final display list that is submitted to the /// renderer. - fn style_dom(dom: &Dom, css: &mut Css) -> UiDescription where Self: Sized { - css.is_dirty = false; + fn style_dom(dom: &Dom, css: &Css) -> UiDescription where Self: Sized { match_dom_css_selectors(dom.root, &dom.arena, &ParsedCss::from_css(css), css, 0) } } @@ -34,10 +33,6 @@ pub trait GetCssId { fn get_css_id(&self) -> &'static str; } -pub trait Widget: GetCssId + Clone + PartialEq + Eq + Hash { } - -impl Widget for T { } - pub(crate) struct ParsedCss<'a> { pub(crate) pure_global_rules: Vec<&'a CssRule>, pub(crate) pure_div_rules: Vec<&'a CssRule>, @@ -135,7 +130,8 @@ fn match_dom_css_selectors<'a, T: LayoutScreen>( ui_descr_arena: (*arena).clone(), ui_descr_root: Some(root), styled_nodes: styled_nodes, - .. Default::default() + default_style_of_node: StyledNode::default(), + dynamic_css_overrides: css.dynamic_css_overrides.clone(), } } diff --git a/src/ui_description.rs b/src/ui_description.rs index b57f107f2..8016543c3 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -1,3 +1,4 @@ +use css_parser::ParsedCssProperty; use FastHashMap; use id_tree::{Arena, NodeId}; use traits::LayoutScreen; @@ -21,6 +22,8 @@ pub struct UiDescription { /// and the reference to that style has to live as least as long as the `self.styled_nodes` /// This is why we need this field here pub(crate) default_style_of_node: StyledNode, + /// The CSS properties that should be overridden for this frame, cloned from the `Css` + pub(crate) dynamic_css_overrides: FastHashMap, } impl Clone for UiDescription { @@ -30,6 +33,7 @@ impl Clone for UiDescription { ui_descr_root: self.ui_descr_root.clone(), styled_nodes: self.styled_nodes.clone(), default_style_of_node: self.default_style_of_node.clone(), + dynamic_css_overrides: self.dynamic_css_overrides.clone(), } } } @@ -41,12 +45,13 @@ impl Default for UiDescription { ui_descr_root: None, styled_nodes: BTreeMap::new(), default_style_of_node: StyledNode::default(), + dynamic_css_overrides: FastHashMap::default(), } } } impl UiDescription { - pub fn from_ui_state(ui_state: &UiState, style: &mut Css) -> Self + pub fn from_ui_state(ui_state: &UiState, style: &Css) -> Self { T::style_dom(&ui_state.dom, style) } diff --git a/src/ui_state.rs b/src/ui_state.rs index 599848025..99916c88d 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -13,11 +13,12 @@ pub struct UiState { impl fmt::Debug for UiState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "UiState {{ - dom: {:?}, - callback_list: {:?}, - node_ids_to_callbacks_list: {:?} -}}", + write!(f, + "UiState {{ \ + \tdom: {:?}, \ + \tcallback_list: {:?}, \ + \tnode_ids_to_callbacks_list: {:?} \ + }}", self.dom, self.callback_list, self.node_ids_to_callbacks_list) diff --git a/src/window.rs b/src/window.rs index 69037bc66..13bf8a16a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,6 @@ //! Window creation module +use window_state::{WindowState, WindowPosition}; use std::{time::Duration, fmt}; use webrender::{ @@ -45,8 +46,8 @@ impl WindowId { /// Options on how to initially create the window #[derive(Debug, Clone)] pub struct WindowCreateOptions { - /// Title of the window - pub title: String, + /// State of the window, set the initial title / width / height here. + pub state: WindowState, /// OpenGL clear color pub background: ColorF, /// Clear the stencil buffer with the given value. If not set, stencil buffer is not cleared @@ -63,12 +64,6 @@ pub struct WindowCreateOptions { /// Should the window update regardless if the mouse is hovering /// over the window? (useful for games vs. applications) pub update_behaviour: UpdateBehaviour, - /// How should the window be decorated? - pub decorations: WindowDecorations, - /// Size and position of the window - pub size: WindowPlacement, - /// What type of window (full screen, popup, normal) - pub class: WindowClass, /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? pub renderer_type: RendererType, } @@ -76,7 +71,7 @@ pub struct WindowCreateOptions { impl Default for WindowCreateOptions { fn default() -> Self { Self { - title: self::DEFAULT_TITLE.into(), + state: WindowState::new(DEFAULT_TITLE, DEFAULT_WIDTH, DEFAULT_HEIGHT), background: ColorF::new(1.0, 1.0, 1.0, 1.0), clear_stencil: None, clear_depth: None, @@ -84,9 +79,6 @@ impl Default for WindowCreateOptions { monitor: WindowMonitorTarget::default(), mouse_mode: MouseMode::default(), update_behaviour: UpdateBehaviour::default(), - decorations: WindowDecorations::default(), - size: WindowPlacement::default(), - class: WindowClass::default(), renderer_type: RendererType::default(), } } @@ -117,82 +109,6 @@ impl Default for RendererType { } } -/// How should the window be decorated -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum WindowDecorations { - /// Regular window decorations - Normal, - /// Maximize button disabled - MaximizeDisabled, - /// Minimize button disabled - MinimizeDisabled, - /// Both maximize and minimize button disabled - MaximizeMinimizeDisabled, - /// No decorations (borderless window) - /// - /// Combine this with `WindowClass::FullScreen` - /// to get borderless fullscreen mode - /// (useful for correct Alt+Tab behaviour) - NoDecorations, -} - -impl Default for WindowDecorations { - fn default() -> Self { - WindowDecorations::Normal - } -} - -/// Where the window should be positioned, -/// from the top left corner of the screen -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct WindowPlacement { - pub x: u32, - pub y: u32, - pub width: u32, - pub height: u32, -} - -impl Default for WindowPlacement { - fn default() -> Self { - Self { - x: 0, - y: 0, - width: self::DEFAULT_WIDTH, - height: self::DEFAULT_HEIGHT, - } - } -} - -/// What class the window should have (important for window managers). -/// Currently not in use. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum WindowClass { - /// Regular desktop window - Normal, - /// Popup window (some window managers handle this differently) - Popup, - /// Will open the window in full-screen mode - /// and set it as the top-level window on the given monitor. - /// Window size is ignored - FullScreen, - /// Start the window maximized - Maximized, - /// Start the window minimized - Minimized, - /// Window is hidden at startup. - /// - /// This is useful for background rendering. Many windowing systems - /// do not properly support off-screen rendering (via OSMesa or similar). - /// As a workaround, you can just create a hidden window - Hidden, -} - -impl Default for WindowClass { - fn default() -> Self { - WindowClass::Normal - } -} - /// Should the window be updated only if the mouse cursor is hovering over it? #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum UpdateBehaviour { @@ -374,8 +290,15 @@ impl Default for WindowMonitorTarget { pub struct Window { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, - // TODO: Migrate to the window_state for state diffing - pub(crate) options: WindowCreateOptions, + /// Current state of the window, stores the keyboard / mouse state, + /// visibility of the window, etc. of the LAST frame. The user never sets this + /// field directly, but rather sets the WindowState he wants to have for the NEXT frame, + /// then azul compares the changes (i.e. if we are currently in fullscreen mode and + /// the user wants the next screen to be in fullscreen mode, too, simply do nothing), then it + /// updates this field to reflect the changes. + /// + /// This field is initialized from the `WindowCreateOptions`. + pub(crate) state: WindowState, /// The webrender renderer pub(crate) renderer: Option, /// The display, i.e. the window @@ -459,13 +382,23 @@ impl Window { let events_loop = EventsLoop::new(); let mut window = WindowBuilder::new() - .with_dimensions(options.size.width, options.size.height) - .with_title(options.title.clone()) - .with_decorations(options.decorations != WindowDecorations::NoDecorations) - .with_visibility(options.class != WindowClass::Hidden) - .with_maximized(options.class == WindowClass::Maximized); - - if options.class == WindowClass::FullScreen { + .with_dimensions(options.state.size.width, options.state.size.height) + .with_title(options.state.title.clone()) + .with_decorations(options.state.has_decorations) + .with_visibility(options.state.is_visible) + .with_transparency(options.state.is_transparent) + .with_maximized(options.state.is_maximized) + .with_multitouch(); + + // TODO: Update winit to have: + // .with_always_on_top(options.state.is_always_on_top) + // + // winit 0.13 -> winit 0.15 + + // TODO: Add all the extensions for X11 / Mac / Windows, + // like setting the taskbar icon, setting the titlebar icon, etc. + + if options.state.is_fullscreen { let monitor = match options.monitor { WindowMonitorTarget::Primary => events_loop.get_primary_monitor(), WindowMonitorTarget::Custom(ref id) => id.clone(), @@ -474,6 +407,14 @@ impl Window { window = window.with_fullscreen(Some(monitor)); } + if let Some((min_w, min_h)) = options.state.size.min_dimensions { + window = window.with_min_dimensions(min_w, min_h); + } + + if let Some((max_w, max_h)) = options.state.size.max_dimensions { + window = window.with_max_dimensions(max_w, max_h); + } + fn create_context_builder<'a>(vsync: bool, srgb: bool) -> ContextBuilder<'a> { let mut builder = ContextBuilder::new() .with_gl(glutin::GlRequest::GlThenGles { @@ -501,6 +442,11 @@ impl Window { .or_else(|_| GlWindow::new(window, create_context_builder(false, false), &events_loop))?; let hidpi_factor = gl_window.hidpi_factor(); + + if let Some(WindowPosition { x, y }) = options.state.position { + gl_window.window().set_position(x as i32, y as i32); + } + let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; unsafe { @@ -607,7 +553,7 @@ impl Window { let window = Window { events_loop: events_loop, - options: options, + state: options.state, renderer: Some(renderer), display: display, compositor: compositor, @@ -639,6 +585,64 @@ impl Window { inner: EventsLoop::new().get_available_monitors(), } } + + /// Updates the window state, diff the `self.state` with the `new_state` + /// and updating the platform window to reflect the changes + /// + /// Note: Currently, setting `mouse_state.position`, `window.size` or + /// `window.position` has no effect on the platform window, since they are very + /// frequently modified by the user (other properties are always set by the + /// application developer) + pub fn update_window_state(&mut self, new_state: WindowState) { + + let gl_window = self.display.gl_window(); + let window = gl_window.window(); + let old_state = &mut self.state; + + // Compare the old and new state, field by field + + if old_state.title != new_state.title { + window.set_title(&new_state.title); + } + + if old_state.mouse_state.mouse_cursor_type != new_state.mouse_state.mouse_cursor_type { + window.set_cursor(new_state.mouse_state.mouse_cursor_type); + } + + if old_state.is_maximized != new_state.is_maximized { + window.set_maximized(new_state.is_maximized); + } + + if old_state.is_fullscreen != new_state.is_fullscreen { + if new_state.is_fullscreen { + window.set_fullscreen(Some(window.get_current_monitor())); + } else { + window.set_fullscreen(None); + } + } + + if old_state.has_decorations != new_state.has_decorations { + window.set_decorations(new_state.has_decorations); + } + + if old_state.is_visible != new_state.is_visible { + if new_state.is_visible { + window.show(); + } else { + window.hide(); + } + } + + if old_state.size.min_dimensions != new_state.size.min_dimensions { + window.set_min_dimensions(new_state.size.min_dimensions); + } + + if old_state.size.max_dimensions != new_state.size.max_dimensions { + window.set_max_dimensions(new_state.size.max_dimensions); + } + + *old_state = new_state; + } } impl Drop for Window { diff --git a/src/window_state.rs b/src/window_state.rs index cfad7673b..bedb7e007 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -57,25 +57,29 @@ pub struct WindowState /// Current title of the window pub title: String, /// The state of the keyboard for this frame - pub keyboard_state: KeyboardState, + pub(crate) keyboard_state: KeyboardState, /// The "global" application menu of this window (one window usually only has one menu) pub application_menu: Option, /// The current context menu for this window pub context_menu: Option, - /// The x and y positon, (0, 0) by default - pub position: WindowPosition, + /// The x and y position, or None to let the WM decide where to put the window (default) + pub position: Option, /// The state of the mouse - pub mouse_state: MouseState, + pub(crate) mouse_state: MouseState, /// Size of the window + max width / max height: 800 x 600 by default pub size: WindowSize, /// Is the window currently maximized - pub maximized: bool, + pub is_maximized: bool, /// Is the window currently fullscreened? - pub fullscreen: bool, + pub is_fullscreen: bool, /// Does the window have decorations (close, minimize, maximize, title bar)? - pub decorations: bool, + pub has_decorations: bool, /// Is the window currently visible? - pub visible: bool, + pub is_visible: bool, + /// Is the window background transparent? + pub is_transparent: bool, + /// Is the window always on top? + pub is_always_on_top: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -86,15 +90,6 @@ pub struct WindowPosition { pub y: u32, } -impl Default for WindowPosition { - fn default() -> Self { - Self { - x: 0, - y: 0, - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct WindowSize { @@ -106,7 +101,6 @@ pub struct WindowSize { pub min_dimensions: Option<(u32, u32)>, /// Maximum dimensions of the window pub max_dimensions: Option<(u32, u32)>, - } impl Default for WindowSize { @@ -123,19 +117,21 @@ impl Default for WindowSize { impl WindowState { /// Creates a new window state - pub(crate) fn new>(width: u32, height: u32, title: S ) -> Self { + pub(crate) fn new>(title: S, width: u32, height: u32) -> Self { Self { title: title.into(), keyboard_state: KeyboardState::default(), mouse_state: MouseState::default(), application_menu: None, context_menu: None, - position: WindowPosition::default(), + position: None, size: WindowSize { width, height, .. Default::default() }, - maximized: false, - fullscreen: false, - decorations: true, - visible: true, + is_maximized: false, + is_fullscreen: false, + has_decorations: true, + is_visible: true, + is_transparent: false, + is_always_on_top: false, } } From 73e5c2b8b0bf03766242c5be06e38c645150427b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 21:12:06 +0200 Subject: [PATCH 065/868] Implemented Default for WindowState, fixed tests --- src/app.rs | 2 +- src/window.rs | 6 +----- src/window_state.rs | 21 +++++++++++++-------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/app.rs b/src/app.rs index d9e7ccc46..a03ec41f9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -358,7 +358,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { pub fn mock_render_frame(&mut self) { use prelude::*; let hidden_create_options = WindowCreateOptions { - class: WindowClass::Hidden, + state: WindowState { is_visible: false, .. Default::default() }, /// force sofware renderer (OSMesa) renderer_type: RendererType::Software, .. Default::default() diff --git a/src/window.rs b/src/window.rs index 13bf8a16a..95faa17b9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -29,10 +29,6 @@ use cache::{EditVariableCache, DomTreeCache}; use id_tree::NodeId; use compositor::Compositor; -const DEFAULT_TITLE: &str = "Azul App"; -const DEFAULT_WIDTH: u32 = 800; -const DEFAULT_HEIGHT: u32 = 600; - /// azul-internal ID for a window #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] pub struct WindowId { @@ -71,7 +67,7 @@ pub struct WindowCreateOptions { impl Default for WindowCreateOptions { fn default() -> Self { Self { - state: WindowState::new(DEFAULT_TITLE, DEFAULT_WIDTH, DEFAULT_HEIGHT), + state: WindowState::default(), background: ColorF::new(1.0, 1.0, 1.0, 1.0), clear_stencil: None, clear_depth: None, diff --git a/src/window_state.rs b/src/window_state.rs index bedb7e007..048146de0 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -9,6 +9,10 @@ use glium::glutin::{ use dom::On; use menu::{ApplicationMenu, ContextMenu}; +const DEFAULT_TITLE: &str = "Azul App"; +const DEFAULT_WIDTH: u32 = 800; +const DEFAULT_HEIGHT: u32 = 600; + /// Determines which keys are pressed currently (modifiers, etc.) #[derive(Debug, Default, Clone)] pub struct KeyboardState @@ -106,26 +110,24 @@ pub struct WindowSize { impl Default for WindowSize { fn default() -> Self { Self { - width: 800, - height: 600, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, min_dimensions: None, max_dimensions: None, } } } -impl WindowState -{ - /// Creates a new window state - pub(crate) fn new>(title: S, width: u32, height: u32) -> Self { +impl Default for WindowState { + fn default() -> Self { Self { - title: title.into(), + title: DEFAULT_TITLE.into(), keyboard_state: KeyboardState::default(), mouse_state: MouseState::default(), application_menu: None, context_menu: None, position: None, - size: WindowSize { width, height, .. Default::default() }, + size: WindowSize::default(), is_maximized: false, is_fullscreen: false, has_decorations: true, @@ -134,7 +136,10 @@ impl WindowState is_always_on_top: false, } } +} +impl WindowState +{ // Determine which event / which callback(s) should be called and in which order // // This function also updates / mutates the current window state, From 6447a68d662ccdefa231b6ad07be94d261939d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 21:39:56 +0200 Subject: [PATCH 066/868] Removed unnecessary input module The module was only 3 lines long, at that point I think we can remove the file. --- src/app.rs | 12 ++++++------ src/input.rs | 5 ----- src/lib.rs | 2 -- 3 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 src/input.rs diff --git a/src/app.rs b/src/app.rs index a03ec41f9..e1b5a4cea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,6 @@ use css::Css; use resources::AppResources; use app_state::AppState; use traits::LayoutScreen; -use input::hit_test_ui; use ui_state::UiState; use ui_description::UiDescription; @@ -15,7 +14,7 @@ use std::io::Read; use images::{ImageType}; use image::ImageError; use font::FontError; -use webrender::api::RenderApi; +use webrender::api::{RenderApi, HitTestFlags}; /// Graphical application that maintains some kind of application state pub struct App<'a, T: LayoutScreen> { @@ -126,10 +125,11 @@ impl<'a, T: LayoutScreen> App<'a, T> { let cursor_x = frame_event_info.cur_cursor_pos.0 as f32; let cursor_y = frame_event_info.cur_cursor_pos.1 as f32; let point = WorldPoint::new(cursor_x, cursor_y); - let hit_test_results = hit_test_ui(&window.internal.api, - window.internal.document_id, - Some(window.internal.pipeline_id), - point); + let hit_test_results = window.internal.api.hit_test( + window.internal.document_id, + Some(window.internal.pipeline_id), + point, + HitTestFlags::FIND_ALL); let mut should_update_screen = UpdateScreen::DontRedraw; diff --git a/src/input.rs b/src/input.rs deleted file mode 100644 index 2ba730eed..000000000 --- a/src/input.rs +++ /dev/null @@ -1,5 +0,0 @@ -use webrender::api::{HitTestResult, PipelineId, DocumentId, HitTestFlags, RenderApi, WorldPoint}; - -pub fn hit_test_ui(api: &RenderApi, document_id: DocumentId, pipeline_id: Option, point: WorldPoint) -> HitTestResult { - api.hit_test(document_id, pipeline_id, point, HitTestFlags::FIND_ALL) -} diff --git a/src/lib.rs b/src/lib.rs index 533ba81c9..c58a1868c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,8 +65,6 @@ pub mod task; pub mod svg; /// Font & image resource handling, lookup and caching mod resources; -/// Input handling (mostly glium) -mod input; /// UI Description & display list handling (webrender) mod ui_description; /// Constraint handling From c645bb6c79a2b281bdc35ffa1a871b4c8699f3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 26 May 2018 23:47:46 +0200 Subject: [PATCH 067/868] Added FakeCss and FakeWindow, allow user to manipulate the state of the window This means that dynamic properties in CSS now work as intended, as does updating / diffing the window state for properties. There is currently a huge bug in the timing - the overridden CSS values are not updated when the change occurs. See the notes in the app.rs file. NOTE: We should really migrate from a Vec to a HashMap, in order to make addition / removal of windows not dependent on their position in the Vec. --- examples/debug.rs | 12 ++++++++++-- src/app.rs | 31 +++++++++++++++++++++++++++---- src/app_state.rs | 10 +++++++--- src/css.rs | 27 +++++++++++++++++++++++++++ src/css_parser.rs | 2 +- src/display_list.rs | 17 ++++++++++++++--- src/dom.rs | 3 ++- src/lib.rs | 3 ++- src/traits.rs | 21 +++++++++++++++++++++ src/window.rs | 30 +++++++++++++++++++++++++++++- 10 files changed, 140 insertions(+), 16 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 0e97ad902..ae4036cee 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -20,9 +20,16 @@ impl LayoutScreen for MyAppData { } } -fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen { +fn my_button_click_handler(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + + // TODO: The DisplayList does somehow not register / override the new ID + // This is probably an issue of timing, see the notes in the app.rs file + app_state.windows[event.window].css.set_dynamic_property("my_id", ("width", "500px")).unwrap(); + + // This works: When the mouse is moved over the button, the title switches to "Hello". + // TODO: performance optimize this + app_state.windows[event.window].state.title = String::from("Hello"); - // app_state.data[event.window_id].css.set_dynamic_property("my_id", ("color", "orange).into()); let should_start_deamon = { let mut app_state_lock = app_state.data.lock().unwrap(); app_state_lock.my_data += 1; @@ -34,6 +41,7 @@ fn my_button_click_handler(app_state: &mut AppState) -> UpdateScreen } else { app_state.delete_deamon("hello"); } + UpdateScreen::Redraw } diff --git a/src/app.rs b/src/app.rs index e1b5a4cea..30e772052 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use dom::UpdateScreen; -use css::Css; +use window::FakeWindow; +use css::{Css, FakeCss}; use resources::AppResources; use app_state::AppState; use traits::LayoutScreen; @@ -59,7 +60,12 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// Spawn a new window on the screen. If an application has no windows, /// the [`run`](#method.run) function will exit immediately. pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { - self.windows.push(Window::new(options, css)?); + let window = Window::new(options, css)?; + self.app_state.windows.push(FakeWindow { + state: window.state.clone(), + css: FakeCss::default(), + }); + self.windows.push(window); Ok(()) } @@ -136,10 +142,19 @@ impl<'a, T: LayoutScreen> App<'a, T> { for item in hit_test_results.items { let callback_list_opt = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0); if let Some(callback_list) = callback_list_opt { + use window::WindowEvent; // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) - // currently, just invoke all actions + // Currently, this just invoke all actions + let window_event = WindowEvent { + window: idx, + // TODO: currently we don't have information about what DOM node was hit + number_of_previous_siblings: None, + cursor_relative_to_item: (item.point_in_viewport.x, item.point_in_viewport.y), + cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), + }; + for callback_id in callback_list.values() { - let update = (ui_state_cache[idx].callback_list[callback_id].0)(&mut self.app_state); + let update = (ui_state_cache[idx].callback_list[callback_id].0)(&mut self.app_state, window_event); if update == UpdateScreen::Redraw { should_update_screen = UpdateScreen::Redraw; } @@ -149,6 +164,11 @@ impl<'a, T: LayoutScreen> App<'a, T> { if should_update_screen == UpdateScreen::Redraw { frame_event_info.should_redraw_window = true; + // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! + // Copy the current fake CSS changes to the real CSS, then clear the fake CSS again + // TODO: .clone() and .clear() can be one operation + window.css.dynamic_css_overrides = self.app_state.windows[idx].css.dynamic_css_overrides.clone(); + self.app_state.windows[idx].css.clear(); } } @@ -198,6 +218,9 @@ impl<'a, T: LayoutScreen> App<'a, T> { let time_end = ::std::time::Instant::now(); debug_has_repainted = Some(time_end - time_start); } + + // Update the window state every frame, no matter if the window has gotten an event or not + window.update_window_state(self.app_state.windows[idx].state.clone()); } // close windows if necessary diff --git a/src/app_state.rs b/src/app_state.rs index 67e5e5a6d..4da1f4701 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use window::FakeWindow; use window_state::WindowState; use task::Task; use dom::UpdateScreen; @@ -20,9 +21,12 @@ pub struct AppState<'a, T: LayoutScreen> { /// Note: this isn't the real window state. This is a "mock" window state which /// can be modified by the user, i.e: /// ```no_run,ignore - /// app_state.windows[event.window_id].css.set_dynamic_property("my_id", ("color", "orange).into()); + /// // For one frame, set the dynamic CSS value with `my_id` to `color: orange` + /// app_state.windows[event.window].css.set_dynamic_property("my_id", ("color", "orange")).unwrap(); + /// // Update the title + /// app_state.windows[event.window].state.title = "Hello"; /// ``` - pub windows: Vec, + pub windows: Vec, /// Fonts and images that are currently loaded into the app pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) @@ -122,7 +126,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// } /// } /// - /// fn my_callback(app_state: &mut AppState) -> UpdateScreen { + /// fn my_callback(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { /// /// Here you can add your font at runtime to the app_state /// app_state.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); /// UpdateScreen::DontRedraw diff --git a/src/css.rs b/src/css.rs index d76a951b6..b33d84c29 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,4 +1,5 @@ //! CSS parsing and styling +use traits::IntoParsedCssProperty; use FastHashMap; use std::ops::Add; use css_parser::{ParsedCssProperty, CssParsingError}; @@ -34,6 +35,32 @@ pub struct Css { pub(crate) needs_relayout: bool, } +/// Fake CSS that can be changed by the user +#[derive(Debug, Default, Clone)] +pub struct FakeCss { + pub dynamic_css_overrides: FastHashMap, +} + +impl FakeCss { + /// Set a dynamic CSS property for the duration of one frame + pub fn set_dynamic_property<'a, S, T>(&mut self, id: S, css_value: T) + -> Result<(), CssParsingError<'a>> + where S: Into, + T: IntoParsedCssProperty<'a>, + { + let value = css_value.into_parsed_css_property()?; + self.dynamic_css_overrides.insert(id.into(), value); + Ok(()) + } + + /// Library-internal only: clear the dynamic overrides + /// + /// Is usually invoked at the end of the frame, to get a clean slate + pub(crate) fn clear(&mut self) { + self.dynamic_css_overrides = FastHashMap::default(); + } +} + /// Error that can happen during the parsing of a CSS value #[derive(Debug, Clone, PartialEq)] pub enum CssParseError<'a> { diff --git a/src/css_parser.rs b/src/css_parser.rs index 73cc28d48..f5a0c4a7f 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -68,7 +68,7 @@ macro_rules! typed_pixel_value_parser { /// A successfully parsed CSS property #[derive(Debug, Clone, PartialEq)] -pub(crate) enum ParsedCssProperty { +pub enum ParsedCssProperty { BorderRadius(BorderRadius), BackgroundColor(BackgroundColor), TextColor(TextColor), diff --git a/src/display_list.rs b/src/display_list.rs index df30d65a7..ba5572d0c 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -651,14 +651,25 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash } } + // Assert that the types of two properties matches + fn property_type_matches(a: &ParsedCssProperty, b: &ParsedCssProperty) -> bool { + use std::mem::discriminant; + discriminant(a) == discriminant(b) + } + for constraint in &rect.styled_node.css_constraints.list { use css::CssDeclaration::*; match constraint { Static(static_property) => apply_parsed_css_property(rect, static_property), Dynamic(dynamic_property) => { - let calculated_property = css_overrides.get(&dynamic_property.dynamic_id) - .unwrap_or(&dynamic_property.default); - apply_parsed_css_property(rect, calculated_property); + let calculated_property = css_overrides.get(&dynamic_property.dynamic_id); + if let Some(overridden_property) = calculated_property { + assert!(property_type_matches(overridden_property, &dynamic_property.default), + "css values don't have the same discriminant type"); + apply_parsed_css_property(rect, overridden_property); + } else { + apply_parsed_css_property(rect, &dynamic_property.default); + } } } } diff --git a/src/dom.rs b/src/dom.rs index 0e6354730..3b40a56ff 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,3 +1,4 @@ +use window::WindowEvent; use traits::GetCssId; use app_state::AppState; use traits::LayoutScreen; @@ -34,7 +35,7 @@ pub enum UpdateScreen { /// The CSS is not affected by this, so if you push to the windows' CSS inside the /// function, the screen will not be automatically redrawn, unless you return an /// `UpdateScreen::Redraw` from the function -pub struct Callback(pub fn(&mut AppState) -> UpdateScreen); +pub struct Callback(pub fn(&mut AppState, WindowEvent) -> UpdateScreen); impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { diff --git a/src/lib.rs b/src/lib.rs index c58a1868c..af4fcdffc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,10 +110,11 @@ pub mod prelude { pub use webrender::api::{ColorF, ColorU}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, - WindowMonitorTarget, RendererType}; + WindowMonitorTarget, RendererType, WindowEvent}; pub use window_state::WindowState; pub use font::FontError; pub use images::ImageType; + pub use css_parser::{CssParsingError, ParsedCssProperty}; // from the extern crate image pub use image::ImageError; diff --git a/src/traits.rs b/src/traits.rs index 6625f2869..5a9601f82 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -7,6 +7,7 @@ use id_tree::{NodeId, Arena}; use std::rc::Rc; use std::cell::RefCell; use std::hash::Hash; +use css_parser::{ParsedCssProperty, CssParsingError}; pub trait LayoutScreen { /// Updates the DOM, must be provided by the final application. @@ -40,6 +41,26 @@ pub(crate) struct ParsedCss<'a> { pub(crate) pure_id_rules: Vec<&'a CssRule>, } +/// This trait exists because `TryFrom` / `TryInto` are not yet stabilized. +/// +/// This is the same as `Into`, but with an additional error case +/// (the conversion could fail) +pub trait IntoParsedCssProperty<'a> { + fn into_parsed_css_property(self) -> Result>; +} + +impl<'a> IntoParsedCssProperty<'a> for ParsedCssProperty { + fn into_parsed_css_property(self) -> Result> { + Ok(self.clone()) + } +} + +impl<'a> IntoParsedCssProperty<'a> for (&'a str, &'a str) { + fn into_parsed_css_property(self) -> Result> { + ParsedCssProperty::from_kv(self.0, self.1) + } +} + impl<'a> ParsedCss<'a> { pub(crate) fn from_css(css: &'a Css) -> Self { diff --git a/src/window.rs b/src/window.rs index 95faa17b9..e47439cb0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,6 @@ //! Window creation module +use css::FakeCss; use window_state::{WindowState, WindowPosition}; use std::{time::Duration, fmt}; @@ -32,13 +33,40 @@ use compositor::Compositor; /// azul-internal ID for a window #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] pub struct WindowId { - pub id: usize, + pub(crate) id: usize, } impl WindowId { pub fn new(id: usize) -> Self { Self { id: id } } } +/// User-modifiable fake window +#[derive(Debug, Clone)] +pub struct FakeWindow { + /// The CSS (use this field to override dynamic CSS ids). + pub css: FakeCss, + /// The window state for the next frame + pub state: WindowState, +} + +/// Window event that is passed to the user when a callback is invoked +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct WindowEvent { + /// The ID of the window that the event was clicked on (for indexing into + /// `app_state.windows`). `app_state.windows[event.window]` should never panic. + pub window: usize, + /// The nth child of the parent DOM node will generate a value of `Some(n)` + /// when it is hit - i.e. if an element is hit, this number is set to + /// + /// Is set to `None` if the hit element is the root of the window + /// (since the root node obviously has no parent). + pub number_of_previous_siblings: Option, + /// The (x, y) position of the mouse cursor, **relative to top left of the element that was hit**. + pub cursor_relative_to_item: (f32, f32), + /// The (x, y) position of the mouse cursor, **relative to top left of the window**. + pub cursor_in_viewport: (f32, f32), +} + /// Options on how to initially create the window #[derive(Debug, Clone)] pub struct WindowCreateOptions { From 2904e0cc8e32b8969b670f4e98b8e70a5e950a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 27 May 2018 12:07:11 +0200 Subject: [PATCH 068/868] Re-exported a lot of webrender / error types --- src/css.rs | 3 ++- src/css_parser.rs | 14 ++++++++++---- src/lib.rs | 35 ++++++++++++++++++++++++++++------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/css.rs b/src/css.rs index b33d84c29..2b203f6cc 100644 --- a/src/css.rs +++ b/src/css.rs @@ -3,6 +3,7 @@ use traits::IntoParsedCssProperty; use FastHashMap; use std::ops::Add; use css_parser::{ParsedCssProperty, CssParsingError}; +use errors::CssSyntaxError; #[cfg(target_os="windows")] const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); @@ -65,7 +66,7 @@ impl FakeCss { #[derive(Debug, Clone, PartialEq)] pub enum CssParseError<'a> { /// A hard error in the CSS syntax - ParseError(::simplecss::Error), + ParseError(CssSyntaxError), /// Braces are not balanced properly UnclosedBlock, /// Invalid syntax, such as `#div { #div: "my-value" }` diff --git a/src/css_parser.rs b/src/css_parser.rs index f5a0c4a7f..7a1a7964c 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1,9 +1,15 @@ //! Contains utilities to convert strings (CSS strings) to servo types -use webrender::api::{ColorU, BorderRadius, LayoutVector2D, LayoutPoint, - ColorF, BoxShadowClipMode, LayoutSize, BorderStyle, - BorderDetails, BorderSide, NormalBorder, BorderWidths, - ExtendMode, LayoutRect, LayoutPixel}; +pub use euclid::{TypedSize2D, SideOffsets2D}; +pub use webrender::api::{ + BorderRadius, BorderWidths, BorderDetails, NormalBorder, + NinePatchBorder, GradientBorder, RadialGradientBorder, + LayoutPixel, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, + Gradient, RadialGradient, LayoutPoint, LayoutSize, + ExtendMode +}; +// TODO: 9patch images! +use webrender::api::{BorderStyle, BorderSide, LayoutRect}; use std::num::{ParseIntError, ParseFloatError}; use euclid::{TypedRotation2D, Angle, TypedPoint2D}; diff --git a/src/lib.rs b/src/lib.rs index af4fcdffc..2d7a68828 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,25 +98,46 @@ mod platform_ext; type FastHashMap = ::std::collections::HashMap>; type FastHashSet = ::std::collections::HashSet>; -pub use app::App; -pub use app_state::AppState; -pub use css::Css; - /// Quick exports of common types pub mod prelude { - pub use {App, AppState, Css}; + pub use app::App; + pub use app_state::AppState; + pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; pub use traits::LayoutScreen; - pub use webrender::api::{ColorF, ColorU}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, WindowMonitorTarget, RendererType, WindowEvent}; pub use window_state::WindowState; pub use font::FontError; pub use images::ImageType; - pub use css_parser::{CssParsingError, ParsedCssProperty}; + pub use css_parser::{ + ParsedCssProperty, BorderRadius, BackgroundColor, TextColor, + BorderWidths, BorderDetails, Background, FontSize, + FontFamily, TextOverflowBehaviour, TextAlignment, + BoxShadowPreDisplayItem, LayoutWidth, LayoutHeight, + LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, + LayoutMaxHeight, LayoutWrap, LayoutDirection, + LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, + LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, + + LayoutPixel, TypedSize2D, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, + Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, + ExtendMode, PixelValue, + }; // from the extern crate image pub use image::ImageError; } +/// Re-exports of errors +pub mod errors { + pub use css_parser::{ + CssParsingError, CssBorderParseError, CssShadowParseError, InvalidValueErr, + PixelParseError, CssImageParseError, CssFontFamilyParseError, + CssBackgroundParseError, CssColorParseError, CssBorderRadiusParseError, + CssDirectionParseError, CssGradientStopParseError, CssShapeParseError, + }; + pub use simplecss::Error as CssSyntaxError; + pub use css::{CssParseError, DynamicCssParseError}; +} From 770792bb2f45eba058ea5d477d381c7d28ac3f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 28 May 2018 21:29:48 +0200 Subject: [PATCH 069/868] Overhaul text layout (does not compile yet) - Add line-height awareness - Layout tabs / return characters - Prepare calculations for scrollbars --- examples/debug.rs | 2 +- src/css_parser.rs | 93 +++++++- src/display_list.rs | 23 +- src/lib.rs | 8 +- src/text_layout.rs | 555 ++++++++++++++++++++++++++++++-------------- 5 files changed, 492 insertions(+), 189 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index ae4036cee..7584ddd90 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -14,7 +14,7 @@ pub struct MyAppData { impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label(format!("{}", self.my_data))) + Dom::new(NodeType::Label(format!("Hello\tbutton\ncool"))) .with_class("__azul-native-button") .with_event(On::MouseUp, Callback(my_button_click_handler)) } diff --git a/src/css_parser.rs b/src/css_parser.rs index 7a1a7964c..a60d0e173 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -83,8 +83,9 @@ pub enum ParsedCssProperty { FontSize(FontSize), FontFamily(FontFamily), TextOverflow(TextOverflowBehaviour), - TextAlign(TextAlignment), + TextAlign(TextAlignmentHorz), BoxShadow(Option), + LineHeight(LineHeight), Width(LayoutWidth), Height(LayoutHeight), @@ -105,7 +106,8 @@ impl_from_no_lifetimes!(Background, ParsedCssProperty::Background); impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); -impl_from_no_lifetimes!(TextAlignment, ParsedCssProperty::TextAlign); +impl_from_no_lifetimes!(TextAlignmentHorz, ParsedCssProperty::TextAlign); +impl_from_no_lifetimes!(LineHeight, ParsedCssProperty::LineHeight); impl_from_no_lifetimes!(LayoutWidth, ParsedCssProperty::Width); impl_from_no_lifetimes!(LayoutHeight, ParsedCssProperty::Height); @@ -150,6 +152,7 @@ impl ParsedCssProperty { "font-size" => Ok(parse_css_font_size(value)?.into()), "font-family" => Ok(parse_css_font_family(value)?.into()), "box-shadow" => Ok(parse_css_box_shadow(value)?.into()), + "line-height" => Ok(parse_line_height(value)?.into()), "width" => Ok(parse_layout_width(value)?.into()), "height" => Ok(parse_layout_height(value)?.into()), @@ -180,6 +183,7 @@ pub enum CssParsingError<'a> { CssShadowParseError(CssShadowParseError<'a>), InvalidValueErr(InvalidValueErr<'a>), PixelParseError(PixelParseError<'a>), + PercentageParseError(PercentageParseError), CssImageParseError(CssImageParseError<'a>), CssFontFamilyParseError(CssFontFamilyParseError<'a>), CssBackgroundParseError(CssBackgroundParseError<'a>), @@ -206,14 +210,27 @@ impl<'a> From<(&'a str, &'a str)> for CssParsingError<'a> { } } +impl<'a> From for CssParsingError<'a> { + fn from(e: PercentageParseError) -> Self { + CssParsingError::PercentageParseError(e) + } +} + /// Simple "invalid value" error, used for #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct InvalidValueErr<'a>(pub &'a str); #[derive(Debug, PartialEq, Copy, Clone)] pub struct PixelValue { - metric: CssMetric, - number: f32, + pub metric: CssMetric, + pub number: f32, +} + +/// "100%" or "1.0" value +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct PercentageValue { + /// Normalized value, 100% = 1.0 + pub number: f32, } #[derive(Debug, PartialEq, Clone, Copy)] @@ -386,6 +403,36 @@ fn parse_pixel_value<'a>(input: &'a str) }) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PercentageParseError { + ValueParseErr(ParseFloatError), +} + +// Parse "1.2" or "120%" (similar to parse_pixel_value) +fn parse_percentage_value(input: &str) +-> Result +{ + let mut split_pos = 0; + for (idx, ch) in input.char_indices() { + if ch.is_numeric() || ch == '.' { + split_pos = idx; + } + } + + split_pos += 1; + + let unit = &input[split_pos..]; + let mut number = input[..split_pos].parse::().map_err(|e| PercentageParseError::ValueParseErr(e))?; + + if unit == "%" { + number /= 100.0; + } + + Ok(PercentageValue { + number: number, + }) +} + /// Parse any valid CSS color, INCLUDING THE HASH /// /// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) @@ -1231,7 +1278,7 @@ pub enum CssGradientStopParseError<'a> { ColorParseError(CssColorParseError<'a>), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct GradientStopPre { pub offset: Option, // this is set to None if there was no offset that could be parsed pub color: ColorF, @@ -1382,6 +1429,9 @@ pub struct LayoutMinHeight(pub PixelValue); #[derive(Debug, PartialEq, Copy, Clone)] pub struct LayoutMaxHeight(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LineHeight(pub PercentageValue); + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutDirection { Horizontal, @@ -1455,15 +1505,28 @@ impl Default for TextOverflowBehaviour { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TextAlignment { - Center, +pub enum TextAlignmentHorz { Left, + Center, Right, } -impl Default for TextAlignment { +impl Default for TextAlignmentHorz { fn default() -> Self { - TextAlignment::Left + TextAlignmentHorz::Left + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TextAlignmentVert { + Top, + Center, + Bottom, +} + +impl Default for TextAlignmentVert { + fn default() -> Self { + TextAlignmentVert::Top } } @@ -1486,9 +1549,11 @@ pub(crate) struct RectStyle { /// Text color pub(crate) font_color: Option, /// Text alignment - pub(crate) text_align: Option, + pub(crate) text_align: Option, /// Text overflow behaviour pub(crate) text_overflow: Option, + /// `line-height` property + pub(crate) line_height: Option, } // Layout constraints for a given rectangle, such as "" @@ -1514,6 +1579,12 @@ typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); typed_pixel_value_parser!(parse_layout_max_width, LayoutMaxWidth); typed_pixel_value_parser!(parse_layout_max_height, LayoutMaxHeight); +fn parse_line_height(input: &str) +-> Result +{ + parse_percentage_value(input).and_then(|e| Ok(LineHeight(e))) +} + #[derive(Debug, PartialEq, Copy, Clone)] pub struct FontSize(pub PixelValue); @@ -1618,7 +1689,7 @@ multi_type_parser!(parse_layout_text_overflow, TextOverflowBehaviour, ["visible", Visible], ["hidden", Hidden]); -multi_type_parser!(parse_layout_text_align, TextAlignment, +multi_type_parser!(parse_layout_text_align, TextAlignmentHorz, ["center", Center], ["left", Left], ["right", Right]); diff --git a/src/display_list.rs b/src/display_list.rs index ba5572d0c..2fa2a0bc7 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -397,7 +397,7 @@ fn push_text( use dom::NodeType::*; use euclid::{TypedPoint2D, Length}; use text_layout; - use css_parser::{TextAlignment, TextOverflowBehaviour}; + use css_parser::{TextAlignmentHorz, TextOverflowBehaviour}; // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it let arena = display_list.ui_descr.ui_descr_arena.borrow(); @@ -417,8 +417,8 @@ fn push_text( }; let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); - let font_size = Length::new(font_size.0.to_pixels()); - let font_size_app_units = (font_size.0 as i32) * AU_PER_PX; + let font_size = font_size.0.to_pixels(); + let font_size_app_units = (font_size as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); let font_size_app_units = Au(font_size_app_units as i32); // * text_layout::WEBRENDER_DPI_HACK) as i32 let font_result = push_font(font_id, font_size_app_units, @@ -429,11 +429,23 @@ fn push_text( None => return, }; + let vert_alignment = TextAlignmentVert::Center; // TODO + let line_height = style.line_height; + let font = &app_resources.font_data[font_id].0; - let alignment = style.text_align.unwrap_or(TextAlignment::default()); + let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); let overflow_behaviour = style.text_overflow.unwrap_or(TextOverflowBehaviour::default()); + let positioned_glyphs = text_layout::put_text_in_bounds( - text, font, font_size, alignment, overflow_behaviour, bounds); + text, + font, + font_size, + line_height, + horz_alignment, + vert_alignment, + overflow_behaviour, + bounds + ); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); let flags = FontInstanceFlags::SUBPIXEL_BGR; @@ -635,6 +647,7 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash TextOverflow(to) => { rect.style.text_overflow = Some(*to); }, TextAlign(ta) => { rect.style.text_align = Some(*ta); }, BoxShadow(opt_box_shadow) => { rect.style.box_shadow = *opt_box_shadow; }, + LineHeight(lh) => { rect.style.line_height = Some(*lh); }, Width(w) => { rect.layout.width = Some(*w); }, Height(h) => { rect.layout.height = Some(*h); }, diff --git a/src/lib.rs b/src/lib.rs index 2d7a68828..dc29c4156 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ #![deny(unused_must_use)] +#![deny(missing_copy_implementations)] #![allow(dead_code)] #![allow(unused_imports)] @@ -114,7 +115,7 @@ pub mod prelude { pub use css_parser::{ ParsedCssProperty, BorderRadius, BackgroundColor, TextColor, BorderWidths, BorderDetails, Background, FontSize, - FontFamily, TextOverflowBehaviour, TextAlignment, + FontFamily, TextOverflowBehaviour, TextAlignmentHorz, BoxShadowPreDisplayItem, LayoutWidth, LayoutHeight, LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, LayoutMaxHeight, LayoutWrap, LayoutDirection, @@ -123,7 +124,7 @@ pub mod prelude { LayoutPixel, TypedSize2D, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, - ExtendMode, PixelValue, + ExtendMode, PixelValue, PercentageValue, }; // from the extern crate image @@ -134,7 +135,8 @@ pub mod prelude { pub mod errors { pub use css_parser::{ CssParsingError, CssBorderParseError, CssShadowParseError, InvalidValueErr, - PixelParseError, CssImageParseError, CssFontFamilyParseError, + PixelParseError, CssImageParseError, CssFontFamilyParseError, CssMetric, + PercentageParseError, CssBackgroundParseError, CssColorParseError, CssBorderRadiusParseError, CssDirectionParseError, CssGradientStopParseError, CssShapeParseError, }; diff --git a/src/text_layout.rs b/src/text_layout.rs index 2d98375e3..e25c6c3a9 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,9 +1,9 @@ #![allow(unused_variables, dead_code)] use webrender::api::*; -use euclid::{Length, TypedRect, TypedPoint2D}; -use rusttype::{Font, Scale}; -use css_parser::{TextAlignment, TextOverflowBehaviour}; +use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; +use rusttype::{Font, Scale, GlyphId}; +use css_parser::{TextAlignmentHorz, TextAlignmentVert, LineHeight, TextOverflowBehaviour}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing @@ -13,58 +13,112 @@ const PX_TO_PT: f32 = 72.0 / 96.0; /// Lines is responsible for layouting the lines of the rectangle to struct Lines<'a> { - align: TextAlignment, - max_lines_before_overflow: usize, - line_height: Length, - max_horizontal_width: Length, + /// Horizontal text alignment + horz_align: TextAlignmentHorz, + /// Vertical text alignment (only respected when the + /// characters don't overflow the bounds) + vert_align: TextAlignmentVert, + /// Line height multiplier (X * `self.font_size`) - default 1.0 + line_height: Option, + /// The font to use for layouting the characters font: &'a Font<'a>, - font_size: Scale, - origin: TypedPoint2D, - current_line: usize, + // Font size of the font + font_size: f32, + /// The bounds of the lines (bounding rectangle) + bounds: TypedRect, } #[derive(Debug)] -struct Word<'a> { +struct Word { // the original text - pub text: &'a str, + pub text: String, // glyphs, positions are relative to the first character of the word pub glyphs: Vec, // the sum of the width of all the characters pub total_width: f32, } -pub(crate) enum TextOverflow { - /// Text is overflowing in the vertical direction - IsOverflowing, - /// Text is in bounds - InBounds, +#[derive(Debug)] +enum SemanticWordItem { + /// Encountered a word (delimited by spaces) + Word(Word), + // `\t` or `x09` + Tab, + /// `\r`, `\n` or `\r\n`, escaped: `\x0D`, `\x0A` or `\x0D\x0A` + Return, +} + +/// Returned struct for the pass-1 text run test. +/// +/// Once the text is parsed and split into words + normalized, we can calculate +/// (without looking at the text itself), if the text overflows the parent rectangle, +/// in order to determine if we need to show a scroll bar. +#[derive(Debug, Clone)] +pub(crate) struct TextOverflowPass1 { + /// Is the text overflowing in the horizontal direction? + pub(crate) horizontal: TextOverflow, + /// Is the text overflowing in the vertical direction? + pub(crate) vertical: TextOverflow, } +/// In the case that we do overflow the rectangle (in any direction), +/// we need to now re-calculate the positions for the words (because of the reduced available +/// space that is now taken up by the scrollbars). +#[derive(Debug, Copy, Clone)] +pub(crate) struct TextOverflowPass2 { + /// Is the text overflowing in the horizontal direction? + pub(crate) horizontal: TextOverflow, + /// Is the text overflowing in the vertical direction? + pub(crate) vertical: TextOverflow, +} + +/// These metrics are important for showing the scrollbars +#[derive(Debug, Copy, Clone)] +pub(crate) enum TextOverflow { + /// Text is overflowing, by how much (in pixels)? + /// Necessary for determining the size of the scrollbar + IsOverflowing(f32), + /// Text is in bounds, how much space (in pixels) is available until + /// the edge of the rectangle? Necessary for centering / aligning text vertically. + InBounds(f32), +} #[derive(Debug, Copy, Clone)] struct HarfbuzzAdjustment(pub f32); -impl<'a> Lines<'a> { +#[derive(Debug, Copy, Clone)] +struct KnuthPlassAdjustment(pub f32); +/// Temporary struct so I don't have to pass the three parameters around seperately all the time +#[derive(Debug, Copy, Clone)] +struct FontMetrics { + /// Width of the space character + space_width: f32, + /// Usually 4 * space_width + tab_width: f32, + /// font_size * line_height + vertical_advance: f32, +} + +impl<'a> Lines<'a> +{ + #[inline] pub(crate) fn from_bounds( bounds: &TypedRect, - alignment: TextAlignment, + horiz_alignment: TextAlignmentHorz, + vert_alignment: TextAlignmentVert, font: &'a Font<'a>, - font_size: Length) + font_size: f32, + line_height: Option) -> Self { - let max_lines_before_overflow = (bounds.size.height / font_size.0).floor() as usize; - let max_horizontal_width = Length::new(bounds.size.width); - Self { - align: alignment, - max_lines_before_overflow: max_lines_before_overflow, - line_height: font_size, + horz_align: horiz_alignment, + vert_align: vert_alignment, + line_height: line_height, font: font, - origin: bounds.origin, - max_horizontal_width: max_horizontal_width, - font_size: Scale::uniform(font_size.0), - current_line: 0, + bounds: *bounds, + font_size: font_size, } } @@ -74,62 +128,236 @@ impl<'a> Lines<'a> { /// This function will only process the glyphs until they overflow /// (we don't process glyphs that are out of the bounds of the rectangle, since /// they don't get drawn anyway). - pub(crate) fn get_glyphs(&mut self, text: &str, _overflow_behaviour: TextOverflowBehaviour) -> (Vec, TextOverflow) { - + pub(crate) fn get_glyphs(&mut self, text: &str, overflow_behaviour: TextOverflowBehaviour) + -> (Vec, TextOverflowPass2) + { let font = &self.font; - let font_size = self.font_size; - let max_horizontal_width = self.max_horizontal_width.0; - let max_lines_before_overflow = self.max_lines_before_overflow; - - // (1) Normalize characters, i.e. A + ^ = Â - let text = normalize_unicode_characters(text); - - // (2) Harfbuzz pass, for getting glyph-individual character shaping offsets + let font_size = Scale::uniform(self.font_size); + let max_horizontal_width = self.bounds.size.width; + + let line_height = match self.line_height { Some(lh) => (lh.0).number, None => 1.0 }; + // Maximum number of lines that can be shown in the rectangle + // before the text overflows + let max_lines_before_overflow = (self.bounds.size.height / (self.font_size * line_height)).floor() as usize; + // Width of the ' ' (space) character (for adding spacing between words) + let space_width = self.font.glyph(' ').scaled(Scale::uniform(self.font_size)).h_metrics().advance_width; + + let tab_width = 4.0 * space_width; // TODO: make this configurable + + let font_metrics = FontMetrics { + vertical_advance: self.font_size * line_height, + space_width: space_width, + tab_width: tab_width, + }; + + // (1) Split the text into semantic items (word, tab or newline) + // This function also normalizes the unicode characters and calculates kerning. + // + // TODO: cache the words somewhere + let words = split_text_into_words(text, font, font_size); + + // (2) Calculate the additions / subtractions that have to be take into account let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); - // (3) Split the text into words - let words = split_text_into_words(&text, font, font_size); + // (3) Determine if the words will overflow the bounding rectangle + let overflow_pass_1 = estimate_overflow_pass_1(&words, &self.bounds.size, &font_metrics, &overflow_behaviour); - // (4) Align text to the left - let (mut positioned_glyphs, line_break_offsets) = words_to_left_aligned_glyphs(words, font, font_size, max_horizontal_width, max_lines_before_overflow); + // (4) If the lines overflow, subtract the space needed for the scrollbars and calculate the length + // again (TODO: already layout characters here?) + let overflow_pass_2 = estimate_overflow_pass_2(&mut words, &self.bounds.size, &font_metrics, &overflow_behaviour, overflow_pass_1); - // (5) Add the harfbuzz adjustments to the positioned glyphs + // (5) Align text to the left, initial layout of glyphs + let (mut positioned_glyphs, line_break_offsets) = + words_to_left_aligned_glyphs(words, font, self.font_size, max_horizontal_width, max_lines_before_overflow, &font_metrics); + + // (6) Add the harfbuzz adjustments to the positioned glyphs apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); - // (6) Knuth-Plass layout, TODO - knuth_plass(&mut positioned_glyphs); + // (7) Calculate the Knuth-Plass adjustments for the (now layouted) glyphs + let knuth_plass_adjustments = calculate_knuth_plass_adjustments(&positioned_glyphs, &line_break_offsets); + + // (8) Add the Knuth-Plass adjustments to the positioned glyphs + apply_knuth_plass_adjustments(&mut positioned_glyphs, knuth_plass_adjustments); - // (7) Center- or right align text if necessary (modifies words) - align_text(self.align, &mut positioned_glyphs, &line_break_offsets); + // (9) Align text horizontally (early return if left-aligned) + align_text_horz(self.horz_align, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); - // (8) (Optional) - Add the self.origin to all the glyphs to bring them from - add_origin(&mut positioned_glyphs, self.origin.x, self.origin.y); + // (10) Align text vertically (early return if text overflows) + align_text_vert(self.vert_align, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); - (positioned_glyphs, TextOverflow::InBounds) + // (11) Add the self.origin to all the glyphs to bring them from glyph space into world space + add_origin(&mut positioned_glyphs, self.bounds.origin.x, self.bounds.origin.y); + + (positioned_glyphs, overflow_pass_2) } } -/// Adds the X and Y offset to each glyph in the positioned glyph -#[inline] -fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) { - for c in positioned_glyphs { - c.point.x += x; - c.point.y += y; +#[inline(always)] +fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: Scale) +-> Vec +{ + use unicode_normalization::UnicodeNormalization; + + let mut words = Vec::new(); + + let mut word_caret = 0.0; + let mut cur_word_length = 0.0; + let mut chars_in_this_word = Vec::new(); + let mut glyphs_in_this_word = Vec::new(); + let mut last_glyph = None; + + fn end_word(words: &mut Vec, + chars_in_this_word: &mut Vec, + glyphs_in_this_word: &mut Vec, + cur_word_length: &mut f32, + word_caret: &mut f32, + last_glyph: &mut Option) + { + // End of word + words.push(SemanticWordItem::Word(Word { + text: chars_in_this_word.drain(..).collect(), + glyphs: glyphs_in_this_word.drain(..).collect(), + total_width: *cur_word_length, + })); + + // Reset everything + *last_glyph = None; + *word_caret = 0.0; + *cur_word_length = 0.0; } + + for cur_char in text.nfc() { + match cur_char { + '\t' => { + // End of word + tab + if !chars_in_this_word.is_empty() { + end_word( + &mut words, + &mut chars_in_this_word, + &mut glyphs_in_this_word, + &mut cur_word_length, + &mut word_caret, + &mut last_glyph); + } + words.push(SemanticWordItem::Tab); + }, + '\n' => { + // End of word + newline + if !chars_in_this_word.is_empty() { + end_word( + &mut words, + &mut chars_in_this_word, + &mut glyphs_in_this_word, + &mut cur_word_length, + &mut word_caret, + &mut last_glyph); + } + words.push(SemanticWordItem::Return); + }, + ' ' => { + if !chars_in_this_word.is_empty() { + end_word( + &mut words, + &mut chars_in_this_word, + &mut glyphs_in_this_word, + &mut cur_word_length, + &mut word_caret, + &mut last_glyph); + } + }, + cur_char => { + // Regular character + use rusttype::Point; + + let g = font.glyph(cur_char).scaled(font_size); + let id = g.id(); + + if let Some(last) = last_glyph { + word_caret += font.pair_kerning(font_size, last, g.id()); + } + + let g = g.positioned(Point { x: word_caret, y: 0.0 }); + last_glyph = Some(id); + let horiz_advance = g.unpositioned().h_metrics().advance_width; + word_caret += horiz_advance; + cur_word_length += horiz_advance; + + glyphs_in_this_word.push(GlyphInstance { + index: id.0, + point: TypedPoint2D::new(g.position().x, g.position().y), + }); + + chars_in_this_word.push(cur_char); + } + } + } + + // Push last word + if !chars_in_this_word.is_empty() { + end_word( + &mut words, + &mut chars_in_this_word, + &mut glyphs_in_this_word, + &mut cur_word_length, + &mut word_caret, + &mut last_glyph); + } + + words } -#[inline] -fn normalize_unicode_characters(text: &str) -> String { - // TODO: This is currently done on the whole string - // (should it be done after split_text_into_words?) - // TODO: THis is an expensive operation! - use unicode_normalization::UnicodeNormalization; - text.nfc().filter(|c| !c.is_control()).collect::() +// First pass: calculate if the words will overflow (using the tabs) +#[inline(always)] +fn estimate_overflow_pass_1( + words: &[SemanticWordItem], + rect: &TypedSize2D, + font_metrics: &FontMetrics, + overflow_behaviour: &TextOverflowBehaviour) +-> TextOverflowPass1 +{ + let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + + /* + /// Always shows a scroll bar, overflows on scroll + Scroll, + /// Does not show a scroll bar by default, only when text is overflowing + Auto, + /// Never shows a scroll bar, simply clips text + Hidden, + /// Doesn't show a scroll bar, simply overflows the text + Visible, + */ + + let mut min_w = 0.0; + // Minimum height necessary for all the returns + let mut min_h = 0.0; + + for word in words { + match word { + SemanticWordItem::Word(Word { total_width, .. }) => { }, + SemanticWordItem::Tab => { }, + SemanticWordItem::Return => { }, + } + } } -#[inline] -fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) -> Vec { +#[inline(always)] +fn estimate_overflow_pass_2( + words: &[SemanticWordItem], + rect: &TypedSize2D, + font_metrics: &FontMetrics, + overflow_behaviour: &TextOverflowBehaviour, + pass1: TextOverflowPass1) +-> TextOverflowPass2 +{ + let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + +} +#[inline(always)] +fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) +-> Vec +{ use harfbuzz_rs::*; use harfbuzz_rs::rusttype::SetRustTypeFuncs; /* @@ -152,74 +380,18 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) -> Vec(text: &'a str, font: &Font<'a>, font_size: Scale) -> Vec> { - - // TODO: this will currently split the whole text (all words) - // - // A basic optimization would be to track whether we have words that will - // step outside the maximum rectangle width - // - // I.e. only split words until the bounds of the rectangle can't contain - // them anymore (using a rough estimation) - - let mut words = Vec::new(); - - for line in text.lines() { - for word in line.split_whitespace() { - - let mut caret = 0.0; - let mut cur_word_length = 0.0; - let mut glyphs_in_this_word = Vec::new(); - let mut last_glyph = None; - - for c in word.chars() { - - use rusttype::Point; - - let g = font.glyph(c).scaled(font_size); - let id = g.id(); - - if c.is_control() { - continue; - } - - if let Some(last) = last_glyph { - caret += font.pair_kerning(font_size, last, g.id()); - } - - let g = g.positioned(Point { x: caret, y: 0.0 }); - last_glyph = Some(id); - let horiz_advance = g.unpositioned().h_metrics().advance_width; - caret += horiz_advance; - cur_word_length += horiz_advance; - - glyphs_in_this_word.push(GlyphInstance { - index: id.0, - point: TypedPoint2D::new(g.position().x, g.position().y), - }) - } - - words.push(Word { - text: word, - glyphs: glyphs_in_this_word, - total_width: cur_word_length, - }) - } - } - - words -} - -#[inline] +#[inline(always)] fn words_to_left_aligned_glyphs<'a>( - words: Vec>, + words: Vec, font: &Font<'a>, - font_size: Scale, + font_size: f32, max_horizontal_width: f32, - max_lines_before_overflow: usize) + max_lines_before_overflow: usize, + font_metrics: &FontMetrics) -> (Vec, Vec<(usize, f32)>) { + let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, // left-aligned let mut left_aligned_glyphs = Vec::::new(); @@ -230,67 +402,89 @@ fn words_to_left_aligned_glyphs<'a>( // - How much space each line has (to the right edge of the containing rectangle) let mut line_break_offsets = Vec::<(usize, f32)>::new(); - let v_metrics_scaled = font.v_metrics(font_size); + let v_metrics_scaled = font.v_metrics(Scale::uniform(vertical_advance)); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; let offset_top = v_metrics_scaled.ascent; - // In order to space between words, we need to - let space_width = font.glyph(' ').scaled(font_size).h_metrics().advance_width; - // word_caret is the current X position of the "pen" we are writing with let mut word_caret = 0.0; let mut current_line_num = 0; for word in words { + use self::SemanticWordItem::*; + match word { + Word(word) => { + let text_overflows_rect = word_caret + word.total_width > max_horizontal_width; + + // Line break occurred + if text_overflows_rect { + line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + word_caret = 0.0; + current_line_num += 1; + } - let text_overflows_rect = word_caret + word.total_width > max_horizontal_width; - - // Line break occurred - if text_overflows_rect { - line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); - word_caret = 0.0; - current_line_num += 1; - } - - for mut glyph in word.glyphs { - let push_x = word_caret; - let push_y = (current_line_num as f32 * v_advance_scaled) + offset_top; - glyph.point.x += push_x; - glyph.point.y += push_y; - left_aligned_glyphs.push(glyph); - } + for mut glyph in word.glyphs { + let push_x = word_caret; + let push_y = (current_line_num as f32 * v_advance_scaled) + offset_top; + glyph.point.x += push_x; + glyph.point.y += push_y; + left_aligned_glyphs.push(glyph); + } - // Add the word width to the current word_caret - // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret - // later for the last line - word_caret += word.total_width + space_width; + // Add the word width to the current word_caret + // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret + // later for the last line + word_caret += word.total_width + space_width; - if current_line_num > max_lines_before_overflow { - break; + if current_line_num > max_lines_before_overflow { + break; + } + }, + Tab => { + word_caret += tab_width; + }, + Return => { + // TODO: dupliated code + line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + word_caret = 0.0; + current_line_num += 1; + }, } } // push the infos about the last line - line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + if !left_aligned_glyphs.is_empty() { + line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + } (left_aligned_glyphs, line_break_offsets) } -#[inline] -fn apply_harfbuzz_adjustments(positioned_glyphs: &mut [GlyphInstance], harfbuzz_adjustments: Vec) { +#[inline(always)] +fn apply_harfbuzz_adjustments(positioned_glyphs: &mut [GlyphInstance], harfbuzz_adjustments: Vec) +{ // TODO } -#[inline] -fn knuth_plass(positioned_glyphs: &mut [GlyphInstance]) { +#[inline(always)] +fn calculate_knuth_plass_adjustments(positioned_glyphs: &[GlyphInstance], line_break_offsets: &[(usize, f32)]) +-> Vec +{ // TODO + Vec::new() } -#[inline] -fn align_text(alignment: TextAlignment, glyphs: &mut Vec, line_breaks: &[(usize, f32)]) { +#[inline(always)] +fn apply_knuth_plass_adjustments(positioned_glyphs: &mut [GlyphInstance], knuth_plass_adjustments: Vec) +{ + // TODO +} - use css_parser::TextAlignment::*; +#[inline(always)] +fn align_text_horz(alignment: TextAlignmentHorz, glyphs: &mut [GlyphInstance], line_breaks: &[(usize, f32)], overflow: &TextOverflowPass2) +{ + use css_parser::TextAlignmentHorz::*; // Text alignment is theoretically very simple: // @@ -327,7 +521,7 @@ fn align_text(alignment: TextAlignment, glyphs: &mut Vec, line_br // i.e. the last line has to end with the last glyph assert!(glyphs.len() - 1 == line_breaks[line_breaks.len() - 1].0); - if alignment == TextAlignment::Left { + if alignment == TextAlignmentHorz::Left { return; } @@ -347,17 +541,40 @@ fn align_text(alignment: TextAlignment, glyphs: &mut Vec, line_br } } -#[inline] +#[inline(always)] +fn align_text_vert(alignment: TextAlignmentVert, glyphs: &mut [GlyphInstance], line_breaks: &[(usize, f32)], overflow: &TextOverflowPass2) { + +} + +/// Adds the X and Y offset to each glyph in the positioned glyph +#[inline(always)] +fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) +{ + for c in positioned_glyphs { + c.point.x += x; + c.point.y += y; + } +} + pub(crate) fn put_text_in_bounds<'a>( text: &str, font: &Font<'a>, - font_size: Length, - alignment: TextAlignment, + font_size: f32, + line_height: Option, + horz_align: TextAlignmentHorz, + vert_align: TextAlignmentVert, overflow_behaviour: TextOverflowBehaviour, bounds: &TypedRect) -> Vec { - let mut lines = Lines::from_bounds(bounds, alignment, font, font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT); + let mut lines = Lines::from_bounds( + bounds, + horz_align, + vert_align, + font, + font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT, + line_height); + let (glyphs, overflow) = lines.get_glyphs(text, overflow_behaviour); glyphs } \ No newline at end of file From 717918b3d8999892c4eeb931c053c9b2a760c314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 29 May 2018 14:15:01 +0200 Subject: [PATCH 070/868] Added parsing for overflow-{x/y} CSS property (necessary for text run) --- src/css_parser.rs | 68 ++++++++++++++++++++++++++++++++++++++++----- src/display_list.rs | 31 ++++++++++++++++++--- src/dom.rs | 2 +- src/text_layout.rs | 19 ++++++------- 4 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index a60d0e173..f06db182a 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -82,7 +82,6 @@ pub enum ParsedCssProperty { Background(Background), FontSize(FontSize), FontFamily(FontFamily), - TextOverflow(TextOverflowBehaviour), TextAlign(TextAlignmentHorz), BoxShadow(Option), LineHeight(LineHeight), @@ -99,13 +98,14 @@ pub enum ParsedCssProperty { JustifyContent(LayoutJustifyContent), AlignItems(LayoutAlignItems), AlignContent(LayoutAlignContent), + Overflow(LayoutOverflow), } impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); impl_from_no_lifetimes!(Background, ParsedCssProperty::Background); impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); impl_from_no_lifetimes!(FontFamily, ParsedCssProperty::FontFamily); -impl_from_no_lifetimes!(TextOverflowBehaviour, ParsedCssProperty::TextOverflow); +impl_from_no_lifetimes!(LayoutOverflow, ParsedCssProperty::Overflow); impl_from_no_lifetimes!(TextAlignmentHorz, ParsedCssProperty::TextAlign); impl_from_no_lifetimes!(LineHeight, ParsedCssProperty::LineHeight); @@ -166,7 +166,27 @@ impl ParsedCssProperty { "justify-content" => Ok(parse_layout_justify_content(value)?.into()), "align-items" => Ok(parse_layout_align_items(value)?.into()), "align-content" => Ok(parse_layout_align_content(value)?.into()), - "overflow" => Ok(parse_layout_text_overflow(value)?.into()), + "overflow" => { + let overflow_both_directions = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: TextOverflowBehaviour::Modified(overflow_both_directions), + vertical: TextOverflowBehaviour::Modified(overflow_both_directions), + }.into()) + }, + "overflow-x" => { + let overflow_x = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: TextOverflowBehaviour::Modified(overflow_x), + vertical: TextOverflowBehaviour::default(), + }.into()) + }, + "overflow-y" => { + let overflow_y = parse_layout_text_overflow(value)?; + Ok(LayoutOverflow { + horizontal: TextOverflowBehaviour::default(), + vertical: TextOverflowBehaviour::Modified(overflow_y), + }.into()) + }, "text-align" => Ok(parse_layout_text_align(value)?.into()), _ => Err((key, value).into()) @@ -174,6 +194,28 @@ impl ParsedCssProperty { } } +/// Wrapper for the `overflow-{x,y}` + `overflow` property +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct LayoutOverflow { + pub horizontal: TextOverflowBehaviour, + pub vertical: TextOverflowBehaviour, +} + +impl LayoutOverflow { + // "merges" two LayoutOverflow properties + pub fn merge(&mut self, other: &LayoutOverflow) { + fn merge_property(p: &mut TextOverflowBehaviour, other: &TextOverflowBehaviour) { + if *other == TextOverflowBehaviour::NotModified { + return; + } + *p = *other; + } + + merge_property(&mut self.horizontal, &other.horizontal); + merge_property(&mut self.vertical, &other.vertical); + } +} + /// Error containing all sub-errors that could happen during CSS parsing /// /// Usually we want to crash on the first error, to notify the user of the problem. @@ -1488,6 +1530,18 @@ pub enum LayoutAlignContent { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TextOverflowBehaviour { + NotModified, + Modified(TextOverflowBehaviourInner), +} + +impl Default for TextOverflowBehaviour { + fn default() -> Self { + TextOverflowBehaviour::NotModified + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TextOverflowBehaviourInner { /// Always shows a scroll bar, overflows on scroll Scroll, /// Does not show a scroll bar by default, only when text is overflowing @@ -1498,9 +1552,9 @@ pub enum TextOverflowBehaviour { Visible, } -impl Default for TextOverflowBehaviour { +impl Default for TextOverflowBehaviourInner { fn default() -> Self { - TextOverflowBehaviour::Auto + TextOverflowBehaviourInner::Auto } } @@ -1551,7 +1605,7 @@ pub(crate) struct RectStyle { /// Text alignment pub(crate) text_align: Option, /// Text overflow behaviour - pub(crate) text_overflow: Option, + pub(crate) overflow: Option, /// `line-height` property pub(crate) line_height: Option, } @@ -1683,7 +1737,7 @@ multi_type_parser!(parse_shape, Shape, ["circle", Circle], ["ellipse", Ellipse]); -multi_type_parser!(parse_layout_text_overflow, TextOverflowBehaviour, +multi_type_parser!(parse_layout_text_overflow, TextOverflowBehaviourInner, ["auto", Auto], ["scroll", Scroll], ["visible", Visible], diff --git a/src/display_list.rs b/src/display_list.rs index 2fa2a0bc7..1fef3dbca 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -24,6 +24,7 @@ use { css::Css, cache::DomChangeSet, ui_description::CssConstraintList, + text_layout::TextOverflowPass2, }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); @@ -434,16 +435,16 @@ fn push_text( let font = &app_resources.font_data[font_id].0; let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); - let overflow_behaviour = style.text_overflow.unwrap_or(TextOverflowBehaviour::default()); + let scrollbar_display_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); - let positioned_glyphs = text_layout::put_text_in_bounds( + let (positioned_glyphs, scrollbar_info) = text_layout::put_text_in_bounds( text, font, font_size, line_height, horz_alignment, vert_alignment, - overflow_behaviour, + &scrollbar_display_behaviour, bounds ); @@ -453,7 +454,23 @@ fn push_text( render_mode: FontRenderMode::Subpixel, flags: flags, }; + builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); + + // If the rectangle should have a scrollbar, push a scrollbar onto the display list + push_scrollbar(info, builder, &scrollbar_display_behaviour, &scrollbar_info, bounds) +} + +/// Adds a scrollbar to the left or bottom side of a rectangle. +/// TODO: make styling configurable (like the width / style of the scrollbar) +fn push_scrollbar( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + display_behaviour: &LayoutOverflow, + scrollbar_info: &TextOverflowPass2, + bounds: &TypedRect) +{ + // TODO: add a scrollbar to the rectangle } /// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the @@ -644,7 +661,13 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash Background(b) => { rect.style.background = Some(b.clone()); }, FontSize(f) => { rect.style.font_size = Some(*f); }, FontFamily(f) => { rect.style.font_family = Some(f.clone()); }, - TextOverflow(to) => { rect.style.text_overflow = Some(*to); }, + Overflow(o) => { + if let Some(ref mut existing_overflow) = rect.style.overflow { + existing_overflow.merge(o); + } else { + rect.style.overflow = Some(*o) + } + }, TextAlign(ta) => { rect.style.text_align = Some(*ta); }, BoxShadow(opt_box_shadow) => { rect.style.box_shadow = *opt_box_shadow; }, LineHeight(lh) => { rect.style.line_height = Some(*lh); }, diff --git a/src/dom.rs b/src/dom.rs index 3b40a56ff..f72a8a59d 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -39,7 +39,7 @@ pub struct Callback(pub fn(&mut AppState, WindowEvent) -> Up impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Callback @ {:x?}", self.0 as usize) + write!(f, "Callback @ 0x{:x}", self.0 as usize) } } diff --git a/src/text_layout.rs b/src/text_layout.rs index e25c6c3a9..0a09aff39 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -3,7 +3,7 @@ use webrender::api::*; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; -use css_parser::{TextAlignmentHorz, TextAlignmentVert, LineHeight, TextOverflowBehaviour}; +use css_parser::{TextAlignmentHorz, TextAlignmentVert, LineHeight, LayoutOverflow}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing @@ -128,7 +128,7 @@ impl<'a> Lines<'a> /// This function will only process the glyphs until they overflow /// (we don't process glyphs that are out of the bounds of the rectangle, since /// they don't get drawn anyway). - pub(crate) fn get_glyphs(&mut self, text: &str, overflow_behaviour: TextOverflowBehaviour) + pub(crate) fn get_glyphs(&mut self, text: &str, overflow: &LayoutOverflow) -> (Vec, TextOverflowPass2) { let font = &self.font; @@ -160,11 +160,11 @@ impl<'a> Lines<'a> let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); // (3) Determine if the words will overflow the bounding rectangle - let overflow_pass_1 = estimate_overflow_pass_1(&words, &self.bounds.size, &font_metrics, &overflow_behaviour); + let overflow_pass_1 = estimate_overflow_pass_1(&words, &self.bounds.size, &font_metrics, &overflow); // (4) If the lines overflow, subtract the space needed for the scrollbars and calculate the length // again (TODO: already layout characters here?) - let overflow_pass_2 = estimate_overflow_pass_2(&mut words, &self.bounds.size, &font_metrics, &overflow_behaviour, overflow_pass_1); + let overflow_pass_2 = estimate_overflow_pass_2(&mut words, &self.bounds.size, &font_metrics, &overflow, overflow_pass_1); // (5) Align text to the left, initial layout of glyphs let (mut positioned_glyphs, line_break_offsets) = @@ -312,7 +312,7 @@ fn estimate_overflow_pass_1( words: &[SemanticWordItem], rect: &TypedSize2D, font_metrics: &FontMetrics, - overflow_behaviour: &TextOverflowBehaviour) + overflow: &LayoutOverflow) -> TextOverflowPass1 { let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; @@ -346,7 +346,7 @@ fn estimate_overflow_pass_2( words: &[SemanticWordItem], rect: &TypedSize2D, font_metrics: &FontMetrics, - overflow_behaviour: &TextOverflowBehaviour, + overflow: &LayoutOverflow, pass1: TextOverflowPass1) -> TextOverflowPass2 { @@ -563,9 +563,9 @@ pub(crate) fn put_text_in_bounds<'a>( line_height: Option, horz_align: TextAlignmentHorz, vert_align: TextAlignmentVert, - overflow_behaviour: TextOverflowBehaviour, + overflow: &LayoutOverflow, bounds: &TypedRect) --> Vec +-> (Vec, TextOverflowPass2) { let mut lines = Lines::from_bounds( bounds, @@ -575,6 +575,5 @@ pub(crate) fn put_text_in_bounds<'a>( font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT, line_height); - let (glyphs, overflow) = lines.get_glyphs(text, overflow_behaviour); - glyphs + lines.get_glyphs(text, overflow) } \ No newline at end of file From f10bd68a438c1621a9736d66c4ffe6dfcd5c1f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 29 May 2018 19:16:12 +0200 Subject: [PATCH 071/868] Added text run for determining overflow / positioning of scrollbars --- src/css.rs | 6 +- src/css_parser.rs | 13 +++ src/lib.rs | 2 +- src/text_layout.rs | 234 +++++++++++++++++++++++++++++++++++++-------- 4 files changed, 213 insertions(+), 42 deletions(-) diff --git a/src/css.rs b/src/css.rs index 2b203f6cc..b20952e8c 100644 --- a/src/css.rs +++ b/src/css.rs @@ -371,10 +371,10 @@ fn determine_static_or_dynamic_css_property<'a>(key: &'a str, value: &'a str) #[test] fn test_detect_static_or_dynamic_property() { - use css_parser::{TextAlignment, PixelParseError, InvalidValueErr}; + use css_parser::{TextAlignmentHorz, PixelParseError, InvalidValueErr}; assert_eq!( determine_static_or_dynamic_css_property("text-align", " center "), - Ok(CssDeclaration::Static(ParsedCssProperty::TextAlign(TextAlignment::Center))) + Ok(CssDeclaration::Static(ParsedCssProperty::TextAlign(TextAlignmentHorz::Center))) ); assert_eq!( @@ -394,7 +394,7 @@ fn test_detect_static_or_dynamic_property() { assert_eq!( determine_static_or_dynamic_css_property("text-align", "[[ hello | center ]]"), Ok(CssDeclaration::Dynamic(DynamicCssProperty { - default: ParsedCssProperty::TextAlign(TextAlignment::Center), + default: ParsedCssProperty::TextAlign(TextAlignmentHorz::Center), dynamic_id: String::from("hello"), })) ); diff --git a/src/css_parser.rs b/src/css_parser.rs index f06db182a..07ae90b2b 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -202,6 +202,7 @@ pub struct LayoutOverflow { } impl LayoutOverflow { + // "merges" two LayoutOverflow properties pub fn merge(&mut self, other: &LayoutOverflow) { fn merge_property(p: &mut TextOverflowBehaviour, other: &TextOverflowBehaviour) { @@ -214,6 +215,18 @@ impl LayoutOverflow { merge_property(&mut self.horizontal, &other.horizontal); merge_property(&mut self.vertical, &other.vertical); } + + pub fn allows_horizontal_overflow(&self) -> bool { + use self::TextOverflowBehaviourInner::*; + match self.horizontal { + TextOverflowBehaviour::Modified(m) => match m { + Scroll | Auto => true, + Hidden | Visible => false, + }, + // default: allow horizontal overflow + TextOverflowBehaviour::NotModified => false, + } + } } /// Error containing all sub-errors that could happen during CSS parsing diff --git a/src/lib.rs b/src/lib.rs index dc29c4156..8e474a22c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,7 +115,7 @@ pub mod prelude { pub use css_parser::{ ParsedCssProperty, BorderRadius, BackgroundColor, TextColor, BorderWidths, BorderDetails, Background, FontSize, - FontFamily, TextOverflowBehaviour, TextAlignmentHorz, + FontFamily, TextOverflowBehaviour, TextOverflowBehaviourInner, TextAlignmentHorz, BoxShadowPreDisplayItem, LayoutWidth, LayoutHeight, LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, LayoutMaxHeight, LayoutWrap, LayoutDirection, diff --git a/src/text_layout.rs b/src/text_layout.rs index 0a09aff39..f0437c186 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -48,6 +48,16 @@ enum SemanticWordItem { Return, } +impl SemanticWordItem { + pub fn is_return(&self) -> bool { + use self::SemanticWordItem::*; + match self { + Return => true, + _ => false, + } + } +} + /// Returned struct for the pass-1 text run test. /// /// Once the text is parsed and split into words + normalized, we can calculate @@ -83,6 +93,16 @@ pub(crate) enum TextOverflow { InBounds(f32), } +impl TextOverflow { + pub fn is_overflowing(&self) -> bool { + use self::TextOverflow::*; + match self { + IsOverflowing(_) => true, + InBounds(_) => false, + } + } +} + #[derive(Debug, Copy, Clone)] struct HarfbuzzAdjustment(pub f32); @@ -131,17 +151,12 @@ impl<'a> Lines<'a> pub(crate) fn get_glyphs(&mut self, text: &str, overflow: &LayoutOverflow) -> (Vec, TextOverflowPass2) { + use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; + let font = &self.font; let font_size = Scale::uniform(self.font_size); - let max_horizontal_width = self.bounds.size.width; - let line_height = match self.line_height { Some(lh) => (lh.0).number, None => 1.0 }; - // Maximum number of lines that can be shown in the rectangle - // before the text overflows - let max_lines_before_overflow = (self.bounds.size.height / (self.font_size * line_height)).floor() as usize; - // Width of the ' ' (space) character (for adding spacing between words) let space_width = self.font.glyph(' ').scaled(Scale::uniform(self.font_size)).h_metrics().advance_width; - let tab_width = 4.0 * space_width; // TODO: make this configurable let font_metrics = FontMetrics { @@ -164,11 +179,16 @@ impl<'a> Lines<'a> // (4) If the lines overflow, subtract the space needed for the scrollbars and calculate the length // again (TODO: already layout characters here?) - let overflow_pass_2 = estimate_overflow_pass_2(&mut words, &self.bounds.size, &font_metrics, &overflow, overflow_pass_1); + let (new_size, overflow_pass_2) = estimate_overflow_pass_2(&words, &self.bounds.size, &font_metrics, &overflow, overflow_pass_1); + + let max_horizontal_text_width = if overflow.allows_horizontal_overflow() { None } else { Some(new_size.width) }; + + // Maximum number of lines that can be shown in the rectangle before the text overflows + let max_lines_before_overflow = (new_size.height / (self.font_size * line_height)).floor() as usize; // (5) Align text to the left, initial layout of glyphs let (mut positioned_glyphs, line_break_offsets) = - words_to_left_aligned_glyphs(words, font, self.font_size, max_horizontal_width, max_lines_before_overflow, &font_metrics); + words_to_left_aligned_glyphs(words, font, self.font_size, max_horizontal_text_width, max_lines_before_overflow, &font_metrics); // (6) Add the harfbuzz adjustments to the positioned glyphs apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); @@ -310,48 +330,144 @@ fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: Scale) #[inline(always)] fn estimate_overflow_pass_1( words: &[SemanticWordItem], - rect: &TypedSize2D, + rect_dimensions: &TypedSize2D, font_metrics: &FontMetrics, overflow: &LayoutOverflow) -> TextOverflowPass1 { + use self::SemanticWordItem::*; + let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; - /* - /// Always shows a scroll bar, overflows on scroll - Scroll, - /// Does not show a scroll bar by default, only when text is overflowing - Auto, - /// Never shows a scroll bar, simply clips text - Hidden, - /// Doesn't show a scroll bar, simply overflows the text - Visible, - */ + let max_text_line_len_horizontal = 0.0; - let mut min_w = 0.0; - // Minimum height necessary for all the returns - let mut min_h = 0.0; + // Determine the maximum width and height that the text needs for layout - for word in words { - match word { - SemanticWordItem::Word(Word { total_width, .. }) => { }, - SemanticWordItem::Tab => { }, - SemanticWordItem::Return => { }, + // This is actually tricky. Horizontal scrollbars and vertical scrollbars + // behave differently. + // + // Vertical scrollbars always show the length - when the + // vertical length = the height of the rectangle, the scrollbar (the actual bar) + // is 0.5x the height of the rectangle, aligned at the top. + // + // Horizontal scrollbars, on the other hand, are 1.0x the width of the rectangle, + // when the width is filled. + + // TODO: this is duplicated code + + let mut max_hor_len = None; + + let vertical_length = { + if overflow.allows_horizontal_overflow() { + // If we can overflow horizontally, we only need to sum up the `Return` + // characters, since the actual length of the line doesn't matter + words.iter().filter(|w| w.is_return()).count() as f32 * vertical_advance + } else { + // TODO: should this be cached? The calculation is probably quick, but this + // is essentially the same thing as we do in the actual text layout stage + let mut max_line_cursor: f32 = 0.0; + let mut cur_line_cursor = 0.0; + let mut cur_vertical = 0.0; + + for w in words { + match w { + Word(w) => { + if cur_line_cursor + w.total_width > rect_dimensions.width { + max_line_cursor = max_line_cursor.max(cur_line_cursor); + cur_line_cursor = 0.0; + cur_vertical += vertical_advance; + } else { + cur_line_cursor += w.total_width; + } + }, + // TODO: also check for rect break after tabs? Kinda pointless, isn't it? + Tab => cur_line_cursor += tab_width, + Return => { + max_line_cursor = max_line_cursor.max(cur_line_cursor); + cur_line_cursor = 0.0; + cur_vertical += vertical_advance; + } + } + } + + max_hor_len = Some(cur_line_cursor); + + cur_vertical } + }; + + let vertical_length = if vertical_length > rect_dimensions.height { + TextOverflow::IsOverflowing(vertical_length - rect_dimensions.height) + } else { + TextOverflow::InBounds(rect_dimensions.height - vertical_length) + }; + + let horizontal_length = { + + let horz_max = if overflow.allows_horizontal_overflow() { + + let mut cur_line_cursor = 0.0; + let mut max_line_cursor: f32 = 0.0; + + for w in words { + match w { + Word(w) => cur_line_cursor += w.total_width, + Tab => cur_line_cursor += tab_width, + Return => { + max_line_cursor = max_line_cursor.max(cur_line_cursor); + cur_line_cursor = 0.0; + } + } + } + + max_line_cursor + } else { + max_hor_len.unwrap() + }; + + if horz_max > rect_dimensions.width { + TextOverflow::IsOverflowing(horz_max - rect_dimensions.width) + } else { + TextOverflow::InBounds(rect_dimensions.width - horz_max) + } + }; + + TextOverflowPass1 { + horizontal: horizontal_length, + vertical: vertical_length, } } #[inline(always)] fn estimate_overflow_pass_2( words: &[SemanticWordItem], - rect: &TypedSize2D, + rect_dimensions: &TypedSize2D, font_metrics: &FontMetrics, overflow: &LayoutOverflow, pass1: TextOverflowPass1) --> TextOverflowPass2 +-> (TypedSize2D, TextOverflowPass2) { let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + let mut new_size = *rect_dimensions; + + // TODO: make this 10px stylable + + // Subtract the space necessary for the scrollbars from the rectangle + if pass1.horizontal.is_overflowing() { + new_size.width -= 10.0; + } + + if pass1.vertical.is_overflowing() { + new_size.height -= 10.0; + } + + let recalc_scrollbar_info = estimate_overflow_pass_1(words, &new_size, font_metrics, overflow); + + (new_size, TextOverflowPass2 { + horizontal: recalc_scrollbar_info.horizontal, + vertical: recalc_scrollbar_info.vertical, + }) } #[inline(always)] @@ -380,12 +496,14 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) Vec::new() // TODO } +/// If `max_horizontal_width` is `None`, it means that the text is allowed to overflow +/// the rectangle horizontally #[inline(always)] fn words_to_left_aligned_glyphs<'a>( words: Vec, font: &Font<'a>, font_size: f32, - max_horizontal_width: f32, + max_horizontal_width: Option, max_lines_before_overflow: usize, font_metrics: &FontMetrics) -> (Vec, Vec<(usize, f32)>) @@ -396,11 +514,16 @@ fn words_to_left_aligned_glyphs<'a>( // left-aligned let mut left_aligned_glyphs = Vec::::new(); + enum WordCaretMax { + SomeMaxWidth(f32), + NoMaxWidth(f32), + } + // The line break offsets (neded for center- / right-aligned text contains: // // - The index of the glyph at which the line breaks // - How much space each line has (to the right edge of the containing rectangle) - let mut line_break_offsets = Vec::<(usize, f32)>::new(); + let mut line_break_offsets = Vec::<(usize, WordCaretMax)>::new(); let v_metrics_scaled = font.v_metrics(Scale::uniform(vertical_advance)); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; @@ -410,16 +533,28 @@ fn words_to_left_aligned_glyphs<'a>( // word_caret is the current X position of the "pen" we are writing with let mut word_caret = 0.0; let mut current_line_num = 0; + let mut max_word_caret = 0.0; for word in words { use self::SemanticWordItem::*; match word { Word(word) => { - let text_overflows_rect = word_caret + word.total_width > max_horizontal_width; + let text_overflows_rect = match max_horizontal_width { + Some(max) => word_caret + word.total_width > max, + // If we don't have a maximum horizontal width, the text can overflow the + // bounding rectangle in the horizontal direction + None => false, + }; - // Line break occurred if text_overflows_rect { - line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + let space_until_horz_return = match max_horizontal_width { + Some(s) => WordCaretMax::SomeMaxWidth(s - word_caret), + None => WordCaretMax::NoMaxWidth(word_caret), + }; + line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + if word_caret > max_word_caret { + max_word_caret = word_caret; + } word_caret = 0.0; current_line_num += 1; } @@ -436,17 +571,25 @@ fn words_to_left_aligned_glyphs<'a>( // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret // later for the last line word_caret += word.total_width + space_width; - + /* if current_line_num > max_lines_before_overflow { break; } + */ }, Tab => { word_caret += tab_width; }, Return => { // TODO: dupliated code - line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + let space_until_horz_return = match max_horizontal_width { + Some(s) => WordCaretMax::SomeMaxWidth(s - word_caret), + None => WordCaretMax::NoMaxWidth(word_caret), + }; + line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + if word_caret > max_word_caret { + max_word_caret = word_caret; + } word_caret = 0.0; current_line_num += 1; }, @@ -455,9 +598,24 @@ fn words_to_left_aligned_glyphs<'a>( // push the infos about the last line if !left_aligned_glyphs.is_empty() { - line_break_offsets.push((left_aligned_glyphs.len() - 1, max_horizontal_width - word_caret)); + let space_until_horz_return = match max_horizontal_width { + Some(s) => WordCaretMax::SomeMaxWidth(s - word_caret), + None => WordCaretMax::NoMaxWidth(word_caret), + }; + line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + if word_caret > max_word_caret { + max_word_caret = word_caret; + } } + let line_break_offsets = line_break_offsets.into_iter().map(|(line, space_r)| { + let space_r = match space_r { + WordCaretMax::SomeMaxWidth(s) => s, + WordCaretMax::NoMaxWidth(word_caret) => max_word_caret - word_caret, + }; + (line, space_r) + }).collect(); + (left_aligned_glyphs, line_break_offsets) } From 856cae792958456b6c259f3775c08edad27351af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 12:48:59 +0200 Subject: [PATCH 072/868] Updated webrender, reserved space for scrollbars correctly --- Cargo.toml | 5 +- examples/debug.rs | 25 ++--- src/display_list.rs | 28 +++++- src/text_layout.rs | 233 +++++++++++++++++++++++--------------------- src/window.rs | 2 +- 5 files changed, 156 insertions(+), 137 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90fdd48a6..38aeeff6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" authors = ["Felix Schütt "] [dependencies] -webrender = { git = "https://github.com/servo/webrender", rev = "60f417726a817051108a0124c8ba77fd9c6c52cf" } cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" @@ -20,6 +19,10 @@ resvg = "0.2.0" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.0" +[dependencies.webrender] +git = "https://github.com/servo/webrender" +rev = "5e4f257d9f3cdb5691ac62f3affe2e9189d66447" + [features] # The reason we do this is because doctests don't get cfg(test) # See: https://github.com/rust-lang/cargo/issues/4669 diff --git a/examples/debug.rs b/examples/debug.rs index 7584ddd90..fd1430794 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -14,7 +14,13 @@ pub struct MyAppData { impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label(format!("Hello\tbutton\ncool"))) + Dom::new(NodeType::Label(format!("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. +Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. +At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."))) .with_class("__azul-native-button") .with_event(On::MouseUp, Callback(my_button_click_handler)) } @@ -30,26 +36,9 @@ fn my_button_click_handler(app_state: &mut AppState, event: WindowEve // TODO: performance optimize this app_state.windows[event.window].state.title = String::from("Hello"); - let should_start_deamon = { - let mut app_state_lock = app_state.data.lock().unwrap(); - app_state_lock.my_data += 1; - app_state_lock.my_data % 2 == 0 - }; - - if should_start_deamon { - app_state.add_deamon("hello", deamon_test_start); - } else { - app_state.delete_deamon("hello"); - } - UpdateScreen::Redraw } -fn deamon_test_start(app_state: &mut MyAppData) -> UpdateScreen { - println!("Hello!"); - UpdateScreen::DontRedraw -} - fn main() { // Parse and validate the CSS diff --git a/src/display_list.rs b/src/display_list.rs index 1fef3dbca..ec49ccdaf 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -24,7 +24,7 @@ use { css::Css, cache::DomChangeSet, ui_description::CssConstraintList, - text_layout::TextOverflowPass2, + text_layout::{TextOverflowPass2, ScrollbarInfo}, }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); @@ -435,7 +435,24 @@ fn push_text( let font = &app_resources.font_data[font_id].0; let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); - let scrollbar_display_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); + let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); + + let mut scrollbar_bar_style = RectStyle::default(); + scrollbar_bar_style.background_color = Some(BackgroundColor(ColorU { r: 80, g: 80, b: 80, a: 255 })); + + let mut scrollbar_background_style = RectStyle::default(); + scrollbar_background_style.background_color = Some(BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 })); + + let mut scrollbar_triangle_style = RectStyle::default(); + scrollbar_triangle_style.background_color = Some(BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 })); + + let scrollbar_style = ScrollbarInfo { + width: 27, + padding: 2, + background_style: scrollbar_background_style, + triangle_style: scrollbar_triangle_style, + bar_style: scrollbar_bar_style, + }; let (positioned_glyphs, scrollbar_info) = text_layout::put_text_in_bounds( text, @@ -444,7 +461,8 @@ fn push_text( line_height, horz_alignment, vert_alignment, - &scrollbar_display_behaviour, + &overflow_behaviour, + &scrollbar_style, bounds ); @@ -458,7 +476,7 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); // If the rectangle should have a scrollbar, push a scrollbar onto the display list - push_scrollbar(info, builder, &scrollbar_display_behaviour, &scrollbar_info, bounds) + push_scrollbar(info, builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds) } /// Adds a scrollbar to the left or bottom side of a rectangle. @@ -468,9 +486,11 @@ fn push_scrollbar( builder: &mut DisplayListBuilder, display_behaviour: &LayoutOverflow, scrollbar_info: &TextOverflowPass2, + scrollbar_style: &ScrollbarInfo, bounds: &TypedRect) { // TODO: add a scrollbar to the rectangle + println!("text overflow: {:#?}", scrollbar_info) } /// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the diff --git a/src/text_layout.rs b/src/text_layout.rs index f0437c186..86899c278 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -3,7 +3,7 @@ use webrender::api::*; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; -use css_parser::{TextAlignmentHorz, TextAlignmentVert, LineHeight, LayoutOverflow}; +use css_parser::{TextAlignmentHorz, RectStyle, TextAlignmentVert, LineHeight, LayoutOverflow}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing @@ -11,23 +11,6 @@ const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; const PX_TO_PT: f32 = 72.0 / 96.0; -/// Lines is responsible for layouting the lines of the rectangle to -struct Lines<'a> { - /// Horizontal text alignment - horz_align: TextAlignmentHorz, - /// Vertical text alignment (only respected when the - /// characters don't overflow the bounds) - vert_align: TextAlignmentVert, - /// Line height multiplier (X * `self.font_size`) - default 1.0 - line_height: Option, - /// The font to use for layouting the characters - font: &'a Font<'a>, - // Font size of the font - font_size: f32, - /// The bounds of the lines (bounding rectangle) - bounds: TypedRect, -} - #[derive(Debug)] struct Word { // the original text @@ -109,6 +92,22 @@ struct HarfbuzzAdjustment(pub f32); #[derive(Debug, Copy, Clone)] struct KnuthPlassAdjustment(pub f32); +/// Holds info necessary for layouting / styling scrollbars +#[derive(Debug, Clone)] +pub(crate) struct ScrollbarInfo { + /// Total width (for vertical scrollbars) or height (for horizontal scrollbars) + /// of the scrollbar in pixels + pub(crate) width: usize, + /// Padding of the scrollbar, in pixels. The inner bar is `width - padding` pixels wide. + pub(crate) padding: usize, + /// Style of the scrollbar (how to draw it) + pub(crate) bar_style: RectStyle, + /// How to draw the "up / down" arrows + pub(crate) triangle_style: RectStyle, + /// Style of the scrollbar background + pub(crate) background_style: RectStyle, +} + /// Temporary struct so I don't have to pass the three parameters around seperately all the time #[derive(Debug, Copy, Clone)] struct FontMetrics { @@ -118,98 +117,107 @@ struct FontMetrics { tab_width: f32, /// font_size * line_height vertical_advance: f32, + /// Offset of the font from the top of the bounding rectangle + offset_top: f32, } -impl<'a> Lines<'a> +/// ## Inputs +/// +/// - `bounds`: The bounds of the rectangle containing the text +/// - `horiz_alignment`: Usually parsed from the `text-align` attribute: horizontal alignment of the text +/// - `vert_alignment`: Usually parsed from the `align-items` attribute on the parent node +/// or the `align-self` on the child node: horizontal alignment of the text +/// - `font`: The font to use for layouting +/// - `font_size`: The font size (without line height) +/// - `line_height`: The line height (100% = 1.0). I.e. `line-height = 1.2;` scales the text vertically by 1.2x +/// - `text`: The actual text to layout. Will be unicode-normalized after the Unicode Normalization Form C +/// (canonical decomposition followed by canonical composition). +/// - `overflow`: If the scrollbars should be show, parsed from the `overflow-{x / y}` fields +/// - `scrollbar_info`: Mostly used to reserve space for the scrollbar, if necessary. +/// +/// ## Returns +/// +/// - `Vec`: The layouted glyphs. If a scrollbar is necessary, they will be layouted so that +/// the scrollbar has space to the left or bottom (so it doesn't overlay the text) +/// - `TextOverflowPass2`: This is internally used for aligning text (horizontally / vertically), but +/// it is necessary for drawing the scrollbars later on, to determine the height of the bar. Contains +/// info about if the text has overflown the rectangle, and if yes, by how many pixels +/// +/// ## Notes +/// +/// This function is currently very expensive, since it doesn't cache the string. So it does many small +/// allocations. This should be cleaned up in the future by caching `BlobStrings` and only re-layouting +/// when it's absolutely necessary. +pub(crate) fn get_glyphs<'a>( + bounds: &TypedRect, + horiz_alignment: TextAlignmentHorz, + vert_alignment: TextAlignmentVert, + font: &'a Font<'a>, + font_size: f32, + line_height: Option, + text: &str, + overflow: &LayoutOverflow, + scrollbar_info: &ScrollbarInfo) +-> (Vec, TextOverflowPass2) { - #[inline] - pub(crate) fn from_bounds( - bounds: &TypedRect, - horiz_alignment: TextAlignmentHorz, - vert_alignment: TextAlignmentVert, - font: &'a Font<'a>, - font_size: f32, - line_height: Option) - -> Self - { - Self { - horz_align: horiz_alignment, - vert_align: vert_alignment, - line_height: line_height, - font: font, - bounds: *bounds, - font_size: font_size, - } - } - - /// NOTE: The glyphs are in the space of the bounds, not of the layer! - /// You'd need to offset them by `bounds.origin` to get the correct position - /// - /// This function will only process the glyphs until they overflow - /// (we don't process glyphs that are out of the bounds of the rectangle, since - /// they don't get drawn anyway). - pub(crate) fn get_glyphs(&mut self, text: &str, overflow: &LayoutOverflow) - -> (Vec, TextOverflowPass2) - { - use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; - - let font = &self.font; - let font_size = Scale::uniform(self.font_size); - let line_height = match self.line_height { Some(lh) => (lh.0).number, None => 1.0 }; - let space_width = self.font.glyph(' ').scaled(Scale::uniform(self.font_size)).h_metrics().advance_width; - let tab_width = 4.0 * space_width; // TODO: make this configurable - - let font_metrics = FontMetrics { - vertical_advance: self.font_size * line_height, - space_width: space_width, - tab_width: tab_width, - }; - - // (1) Split the text into semantic items (word, tab or newline) - // This function also normalizes the unicode characters and calculates kerning. - // - // TODO: cache the words somewhere - let words = split_text_into_words(text, font, font_size); + use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; + + let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; + let font_size_with_line_height = Scale::uniform(font_size * line_height); + let font_size_no_line_height = Scale::uniform(font_size); + let space_width = font.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; + let tab_width = 4.0 * space_width; // TODO: make this configurable + let offset_top = font.v_metrics(font_size_with_line_height).ascent; + + let font_metrics = FontMetrics { + vertical_advance: font_size_with_line_height.y, + space_width: space_width, + tab_width: tab_width, + offset_top: offset_top, + }; - // (2) Calculate the additions / subtractions that have to be take into account - let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); + // (1) Split the text into semantic items (word, tab or newline) + // This function also normalizes the unicode characters and calculates kerning. + // + // TODO: cache the words somewhere + let words = split_text_into_words(text, font, font_size_no_line_height); - // (3) Determine if the words will overflow the bounding rectangle - let overflow_pass_1 = estimate_overflow_pass_1(&words, &self.bounds.size, &font_metrics, &overflow); + // (2) Calculate the additions / subtractions that have to be take into account + let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); - // (4) If the lines overflow, subtract the space needed for the scrollbars and calculate the length - // again (TODO: already layout characters here?) - let (new_size, overflow_pass_2) = estimate_overflow_pass_2(&words, &self.bounds.size, &font_metrics, &overflow, overflow_pass_1); + // (3) Determine if the words will overflow the bounding rectangle + let overflow_pass_1 = estimate_overflow_pass_1(&words, &bounds.size, &font_metrics, &overflow); - let max_horizontal_text_width = if overflow.allows_horizontal_overflow() { None } else { Some(new_size.width) }; + // (4) If the lines overflow, subtract the space needed for the scrollbars and calculate the length + // again (TODO: already layout characters here?) + let (new_size, overflow_pass_2) = + estimate_overflow_pass_2(&words, &bounds.size, &font_metrics, &overflow, scrollbar_info, overflow_pass_1); - // Maximum number of lines that can be shown in the rectangle before the text overflows - let max_lines_before_overflow = (new_size.height / (self.font_size * line_height)).floor() as usize; + let max_horizontal_text_width = if overflow.allows_horizontal_overflow() { None } else { Some(new_size.width) }; - // (5) Align text to the left, initial layout of glyphs - let (mut positioned_glyphs, line_break_offsets) = - words_to_left_aligned_glyphs(words, font, self.font_size, max_horizontal_text_width, max_lines_before_overflow, &font_metrics); + // (5) Align text to the left, initial layout of glyphs + let (mut positioned_glyphs, line_break_offsets) = + words_to_left_aligned_glyphs(words, font, max_horizontal_text_width, &font_metrics); - // (6) Add the harfbuzz adjustments to the positioned glyphs - apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); + // (6) Add the harfbuzz adjustments to the positioned glyphs + apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); - // (7) Calculate the Knuth-Plass adjustments for the (now layouted) glyphs - let knuth_plass_adjustments = calculate_knuth_plass_adjustments(&positioned_glyphs, &line_break_offsets); + // (7) Calculate the Knuth-Plass adjustments for the (now layouted) glyphs + let knuth_plass_adjustments = calculate_knuth_plass_adjustments(&positioned_glyphs, &line_break_offsets); - // (8) Add the Knuth-Plass adjustments to the positioned glyphs - apply_knuth_plass_adjustments(&mut positioned_glyphs, knuth_plass_adjustments); + // (8) Add the Knuth-Plass adjustments to the positioned glyphs + apply_knuth_plass_adjustments(&mut positioned_glyphs, knuth_plass_adjustments); - // (9) Align text horizontally (early return if left-aligned) - align_text_horz(self.horz_align, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); + // (9) Align text horizontally (early return if left-aligned) + align_text_horz(horiz_alignment, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); - // (10) Align text vertically (early return if text overflows) - align_text_vert(self.vert_align, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); + // (10) Align text vertically (early return if text overflows) + align_text_vert(vert_alignment, &mut positioned_glyphs, &line_break_offsets, &overflow_pass_2); - // (11) Add the self.origin to all the glyphs to bring them from glyph space into world space - add_origin(&mut positioned_glyphs, self.bounds.origin.x, self.bounds.origin.y); + // (11) Add the self.origin to all the glyphs to bring them from glyph space into world space + add_origin(&mut positioned_glyphs, bounds.origin.x, bounds.origin.y); - (positioned_glyphs, overflow_pass_2) - } + (positioned_glyphs, overflow_pass_2) } #[inline(always)] @@ -337,7 +345,7 @@ fn estimate_overflow_pass_1( { use self::SemanticWordItem::*; - let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; let max_text_line_len_horizontal = 0.0; @@ -367,7 +375,7 @@ fn estimate_overflow_pass_1( // is essentially the same thing as we do in the actual text layout stage let mut max_line_cursor: f32 = 0.0; let mut cur_line_cursor = 0.0; - let mut cur_vertical = 0.0; + let mut cur_vertical = offset_top; for w in words { match w { @@ -444,22 +452,28 @@ fn estimate_overflow_pass_2( rect_dimensions: &TypedSize2D, font_metrics: &FontMetrics, overflow: &LayoutOverflow, + scrollbar_info: &ScrollbarInfo, pass1: TextOverflowPass1) -> (TypedSize2D, TextOverflowPass2) { - let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; let mut new_size = *rect_dimensions; // TODO: make this 10px stylable // Subtract the space necessary for the scrollbars from the rectangle + // + // NOTE: this is switched around - if the text overflows vertically, the + // scrollbar gets shown on the right edge, so we need to subtract from the + // **width** of the rectangle. + if pass1.horizontal.is_overflowing() { - new_size.width -= 10.0; + new_size.height -= scrollbar_info.width as f32; } if pass1.vertical.is_overflowing() { - new_size.height -= 10.0; + new_size.width -= scrollbar_info.width as f32; } let recalc_scrollbar_info = estimate_overflow_pass_1(words, &new_size, font_metrics, overflow); @@ -502,13 +516,11 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) fn words_to_left_aligned_glyphs<'a>( words: Vec, font: &Font<'a>, - font_size: f32, max_horizontal_width: Option, - max_lines_before_overflow: usize, font_metrics: &FontMetrics) -> (Vec, Vec<(usize, f32)>) { - let FontMetrics { space_width, tab_width, vertical_advance } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, // left-aligned @@ -528,8 +540,6 @@ fn words_to_left_aligned_glyphs<'a>( let v_metrics_scaled = font.v_metrics(Scale::uniform(vertical_advance)); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - let offset_top = v_metrics_scaled.ascent; - // word_caret is the current X position of the "pen" we are writing with let mut word_caret = 0.0; let mut current_line_num = 0; @@ -571,11 +581,6 @@ fn words_to_left_aligned_glyphs<'a>( // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret // later for the last line word_caret += word.total_width + space_width; - /* - if current_line_num > max_lines_before_overflow { - break; - } - */ }, Tab => { word_caret += tab_width; @@ -722,16 +727,18 @@ pub(crate) fn put_text_in_bounds<'a>( horz_align: TextAlignmentHorz, vert_align: TextAlignmentVert, overflow: &LayoutOverflow, + scrollbar_info: &ScrollbarInfo, bounds: &TypedRect) -> (Vec, TextOverflowPass2) { - let mut lines = Lines::from_bounds( + get_glyphs( bounds, horz_align, vert_align, font, font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT, - line_height); - - lines.get_glyphs(text, overflow) + line_height, + text, + overflow, + scrollbar_info) } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index e47439cb0..2ce0101e7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -506,7 +506,7 @@ impl Window { enable_aa: true, clear_color: clear_color, enable_render_on_scroll: false, - cached_programs: Some(ProgramCache::new()), + cached_programs: Some(ProgramCache::new(None)), renderer_kind: if native { RendererKind::Native } else { From c74a00416e8fcb9276f9844361e7d16845a852ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 14:08:26 +0200 Subject: [PATCH 073/868] Fixed line-height not working in conjunction with overflow-y --- examples/test_content.css | 1 + src/text_layout.rs | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/examples/test_content.css b/examples/test_content.css index 663b7c10a..79e553e9e 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -15,6 +15,7 @@ justify-content: space-around; align-items: center; align-content: center; + line-height: 1.5; } * { diff --git a/src/text_layout.rs b/src/text_layout.rs index 86899c278..6683901ca 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -167,10 +167,13 @@ pub(crate) fn get_glyphs<'a>( let font_size_no_line_height = Scale::uniform(font_size); let space_width = font.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; let tab_width = 4.0 * space_width; // TODO: make this configurable - let offset_top = font.v_metrics(font_size_with_line_height).ascent; + + let v_metrics_scaled = font.v_metrics(font_size_with_line_height); + let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; + let offset_top = v_metrics_scaled.ascent; let font_metrics = FontMetrics { - vertical_advance: font_size_with_line_height.y, + vertical_advance: v_advance_scaled, space_width: space_width, tab_width: tab_width, offset_top: offset_top, @@ -375,7 +378,7 @@ fn estimate_overflow_pass_1( // is essentially the same thing as we do in the actual text layout stage let mut max_line_cursor: f32 = 0.0; let mut cur_line_cursor = 0.0; - let mut cur_vertical = offset_top; + let mut cur_line = 0; for w in words { match w { @@ -383,23 +386,24 @@ fn estimate_overflow_pass_1( if cur_line_cursor + w.total_width > rect_dimensions.width { max_line_cursor = max_line_cursor.max(cur_line_cursor); cur_line_cursor = 0.0; - cur_vertical += vertical_advance; - } else { - cur_line_cursor += w.total_width; + cur_line += 1; } + cur_line_cursor += w.total_width + space_width; }, // TODO: also check for rect break after tabs? Kinda pointless, isn't it? Tab => cur_line_cursor += tab_width, Return => { max_line_cursor = max_line_cursor.max(cur_line_cursor); cur_line_cursor = 0.0; - cur_vertical += vertical_advance; + cur_line += 1; } } } max_hor_len = Some(cur_line_cursor); + let cur_vertical = (cur_line as f32 * vertical_advance) + offset_top; + cur_vertical } }; @@ -476,7 +480,12 @@ fn estimate_overflow_pass_2( new_size.width -= scrollbar_info.width as f32; } - let recalc_scrollbar_info = estimate_overflow_pass_1(words, &new_size, font_metrics, overflow); + // If the words are not overflowing, just take the result from the first pass + let recalc_scrollbar_info = if pass1.horizontal.is_overflowing() || pass1.vertical.is_overflowing() { + estimate_overflow_pass_1(words, &new_size, font_metrics, overflow) + } else { + pass1 + }; (new_size, TextOverflowPass2 { horizontal: recalc_scrollbar_info.horizontal, @@ -537,9 +546,6 @@ fn words_to_left_aligned_glyphs<'a>( // - How much space each line has (to the right edge of the containing rectangle) let mut line_break_offsets = Vec::<(usize, WordCaretMax)>::new(); - let v_metrics_scaled = font.v_metrics(Scale::uniform(vertical_advance)); - let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - // word_caret is the current X position of the "pen" we are writing with let mut word_caret = 0.0; let mut current_line_num = 0; @@ -571,15 +577,13 @@ fn words_to_left_aligned_glyphs<'a>( for mut glyph in word.glyphs { let push_x = word_caret; - let push_y = (current_line_num as f32 * v_advance_scaled) + offset_top; + let push_y = (current_line_num as f32 * vertical_advance) + offset_top; glyph.point.x += push_x; glyph.point.y += push_y; left_aligned_glyphs.push(glyph); } // Add the word width to the current word_caret - // NOTE: has to happen BEFORE the `break` statment, since we use the word_caret - // later for the last line word_caret += word.total_width + space_width; }, Tab => { From 80c481c788dc6a14883e95ea83337e5a216f6519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 14:45:49 +0200 Subject: [PATCH 074/868] Add github issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 46 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 29 ++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..a9be897e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,46 @@ +--- +name: Bug report +about: Report a problem encountered while using azul + +--- + +## Description + + +## Version / OS + + +* azul version: + + +* Operating system: + + +* Windowing system (X11 or Wayland, Linux only): + + +## Steps to Reproduce + + + +## Expected Behavior + + +## Actual Behavior + + +## Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..e98d3d041 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea to improve azul + +--- + +## Description + + +## Describe your solution + + +## Are there alternatives or drawbacks? + + +## Is this a breaking change + + +## Additional notes + From c280c75d574e57a1b21491a1c776406a6717e427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 15:52:13 +0200 Subject: [PATCH 075/868] Added vertical text centering and initial (hacky) scrollbars --- examples/debug.rs | 13 +++++--- examples/test_content.css | 1 - src/display_list.rs | 66 ++++++++++++++++++++++++++++++--------- src/text_layout.rs | 23 +++++++++++--- 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index fd1430794..46f3b3172 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -11,16 +11,19 @@ pub struct MyAppData { // Your app data goes here pub my_data: u32, } - -impl LayoutScreen for MyAppData { - fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label(format!("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. +/* +rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. -Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."))) +Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. +*/ + +impl LayoutScreen for MyAppData { + fn get_dom(&self, _window_id: WindowId) -> Dom { + Dom::new(NodeType::Label(format!("Hello"))) .with_class("__azul-native-button") .with_event(On::MouseUp, Callback(my_button_click_handler)) } diff --git a/examples/test_content.css b/examples/test_content.css index 79e553e9e..663b7c10a 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -15,7 +15,6 @@ justify-content: space-around; align-items: center; align-content: center; - line-height: 1.5; } * { diff --git a/src/display_list.rs b/src/display_list.rs index ec49ccdaf..012175fff 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -345,11 +345,6 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { &full_screen_rect, BoxShadowClipMode::Inset); - push_border( - &info, - &mut builder, - &rect.style); - push_text( &info, &self, @@ -361,6 +356,11 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { &bounds, &mut resource_updates); + push_border( + &info, + &mut builder, + &rect.style); + // Pop clip if clip_region_id.is_some() { builder.pop_clip_id(); @@ -421,9 +421,8 @@ fn push_text( let font_size = font_size.0.to_pixels(); let font_size_app_units = (font_size as i32) * AU_PER_PX; let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); - let font_size_app_units = Au(font_size_app_units as i32); // * text_layout::WEBRENDER_DPI_HACK) as i32 - let font_result = push_font(font_id, font_size_app_units, - resource_updates, app_resources, render_api); + let font_size_app_units = Au(font_size_app_units as i32); + let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { Some(f) => f, @@ -438,16 +437,16 @@ fn push_text( let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); let mut scrollbar_bar_style = RectStyle::default(); - scrollbar_bar_style.background_color = Some(BackgroundColor(ColorU { r: 80, g: 80, b: 80, a: 255 })); + scrollbar_bar_style.background_color = Some(BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 })); let mut scrollbar_background_style = RectStyle::default(); scrollbar_background_style.background_color = Some(BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 })); let mut scrollbar_triangle_style = RectStyle::default(); - scrollbar_triangle_style.background_color = Some(BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 })); + scrollbar_triangle_style.background_color = Some(BackgroundColor(ColorU { r: 163, g: 163, b: 163, a: 255 })); let scrollbar_style = ScrollbarInfo { - width: 27, + width: 17, padding: 2, background_style: scrollbar_background_style, triangle_style: scrollbar_triangle_style, @@ -476,21 +475,58 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); // If the rectangle should have a scrollbar, push a scrollbar onto the display list - push_scrollbar(info, builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds) + push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds) } /// Adds a scrollbar to the left or bottom side of a rectangle. /// TODO: make styling configurable (like the width / style of the scrollbar) fn push_scrollbar( - info: &PrimitiveInfo, builder: &mut DisplayListBuilder, display_behaviour: &LayoutOverflow, scrollbar_info: &TextOverflowPass2, scrollbar_style: &ScrollbarInfo, bounds: &TypedRect) { - // TODO: add a scrollbar to the rectangle - println!("text overflow: {:#?}", scrollbar_info) + use euclid::TypedPoint2D; + + { + // Background of scrollbar (vertical) + let scrollbar_vertical_background = TypedRect:: { + origin: TypedPoint2D::new(bounds.origin.x + bounds.size.width - scrollbar_style.width as f32, bounds.origin.y), + size: TypedSize2D::new(scrollbar_style.width as f32, bounds.size.height), + }; + + let scrollbar_vertical_background_info = PrimitiveInfo { + rect: scrollbar_vertical_background, + clip_rect: *bounds, + is_backface_visible: false, + tag: None, // TODO: for hit testing + }; + + push_rect(&scrollbar_vertical_background_info, builder, &scrollbar_style.background_style); + } + + { + // Actual scroll bar + let scrollbar_vertical_bar = TypedRect:: { + origin: TypedPoint2D::new( + bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, + bounds.origin.y + scrollbar_style.width as f32), + size: TypedSize2D::new( + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, + bounds.size.height - (scrollbar_style.width * 2) as f32), + }; + + let scrollbar_vertical_bar_info = PrimitiveInfo { + rect: scrollbar_vertical_bar, + clip_rect: *bounds, + is_backface_visible: false, + tag: None, // TODO: for hit testing + }; + + push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_style); + } + } /// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the diff --git a/src/text_layout.rs b/src/text_layout.rs index 6683901ca..9a6e5d2f6 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -688,14 +688,10 @@ fn align_text_horz(alignment: TextAlignmentHorz, glyphs: &mut [GlyphInstance], l // i.e. the last line has to end with the last glyph assert!(glyphs.len() - 1 == line_breaks[line_breaks.len() - 1].0); - if alignment == TextAlignmentHorz::Left { - return; - } - let multiply_factor = match alignment { Left => { return; }, - Right => 1.0, // move the line by the full width Center => 0.5, // move the line by the half width + Right => 1.0, // move the line by the full width }; let mut current_line_num = 0; @@ -711,6 +707,23 @@ fn align_text_horz(alignment: TextAlignmentHorz, glyphs: &mut [GlyphInstance], l #[inline(always)] fn align_text_vert(alignment: TextAlignmentVert, glyphs: &mut [GlyphInstance], line_breaks: &[(usize, f32)], overflow: &TextOverflowPass2) { + use self::TextOverflow::*; + use self::TextAlignmentVert::*; + + assert!(glyphs.len() - 1 == line_breaks[line_breaks.len() - 1].0); + + let multiply_factor = match alignment { + Top => return, + Center => 0.5, + Bottom => 1.0, + }; + + let space_to_add = match overflow.vertical { + IsOverflowing(_) => return, + InBounds(s) => s * multiply_factor, + }; + + glyphs.iter_mut().for_each(|g| g.point.y += space_to_add); } /// Adds the X and Y offset to each glyph in the positioned glyph From b882ca82e48b1cac2a7217d234facf2b759becbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 20:17:06 +0200 Subject: [PATCH 076/868] Initial push_triangle function for drawing the up / down arrow on the scrollbar --- src/display_list.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/display_list.rs b/src/display_list.rs index 012175fff..27cd55e27 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -527,6 +527,80 @@ fn push_scrollbar( push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_style); } + { + // Triangle 1 + let scrollbar_vertical_bar = TypedRect:: { + origin: TypedPoint2D::new( + bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, + bounds.origin.y + scrollbar_style.padding as f32), + size: TypedSize2D::new( + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32), + }; + + push_triangle(&scrollbar_vertical_bar, builder, &scrollbar_style.triangle_style, TriangleDirection::PointUp); + } +} + +enum TriangleDirection { + PointUp, + PointDown, + PointRight, + PointLeft, +} + +fn push_triangle( + bounds: &TypedRect, + builder: &mut DisplayListBuilder, + style: &RectStyle, + direction: TriangleDirection) +{ + use euclid::TypedPoint2D; + use self::TriangleDirection::*; + + // see: https://css-tricks.com/snippets/css/css-triangle/ + // uses the "3d effect" for making a triangle + + let background_color = match style.background_color { + None => return, + Some(s) => s, + }; + + let triangle_rect_info = PrimitiveInfo { + rect: *bounds, + clip_rect: *bounds, + is_backface_visible: false, + tag: None, + }; + + const TRANSPARENT: ColorU = ColorU { r: 0, b: 0, g: 0, a: 0 }; + + // make all borders but one transparent + let [b_left, b_right, b_top, b_bottom] = match direction { + PointUp => [(TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid) ], + PointDown => [(TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden)], + PointLeft => [(TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden)], + PointRight => [(background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden)], + }; + + let border_details = BorderDetails::Normal(NormalBorder { + left: BorderSide { color: b_left.0.into(), style: b_left.1 }, + right: BorderSide { color: b_right.0.into(), style: b_right.1 }, + top: BorderSide { color: b_top.0.into(), style: b_top.1 }, + bottom: BorderSide { color: b_bottom.0.into(), style: b_bottom.1 }, + radius: BorderRadius::zero(), + }); + + // make the borders half the width / height of the rectangle, + // so that the border looks like a triangle + let border_widths = BorderWidths { + left: bounds.size.width / 2.0, + top: bounds.size.height / 2.0, + right: bounds.size.width / 2.0, + bottom: bounds.size.height / 2.0, + }; + + builder.push_border(&triangle_rect_info, border_widths, border_details); } /// WARNING: For "inset" shadows, you must push a clip ID first, otherwise the From 4dd72f44b956901f41ea0e7071a2cb946958b4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 20:17:21 +0200 Subject: [PATCH 077/868] Updated supported CSS properties in README --- README.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 70be829fb..9560a2144 100644 --- a/README.md +++ b/README.md @@ -371,22 +371,26 @@ and reduce duplication (so you don't have to do `extern crate image`, just do `u ### Current - Supported CSS attributes (syntax is the same as CSS, expect when marked otherwise): + - `border-radius` - `background-color` - - `background`: **Note**: `image()` takes an ID instead of a URL, see below. - `color` - - `border-radius` + - `border` + - `background`: **Note**: `image()` takes an ID instead of a URL, see below. - `font-size` - - `font-family`: **Note**: same as with the `background` property, you need to register fonts first, see below. - - `text-align`: **Note**: block-text is not supported. - - `width` - - `height` - - `min-width` - - `min-height` - - `flex-direction`: **Note**: not implemented yet - - `flex-wrap`: **Note**: not implemented yet - - `justify-content`: **Note**: not implemented yet - - `align-items`: **Note**: not implemented yet - - `align-content`: **Note**: not implemented yet + - `font-family`: **Note**: Like images, you need to register font IDs first (from Rust) + - `box-shadow` + - `line-height` + - `width`, `min-width`, `max-width` + - `height`, `min-height`, `max-height` + - `align-items`: **Note**: Currently only implemented for centering text vertically + - `overflow`, `overflow-x`, `overflow-y` + - `text-align`: **Note**: Justified text is not supported. + +- Not implemented yet, but planned: + - `flex-wrap` + - `flex-direction` + - `justify-content` + - `align-content` Remarks: From cfd2f6df3069ec8640be19d285b3583a2fef21f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 30 May 2018 20:53:51 +0200 Subject: [PATCH 078/868] Revised scroll bars (with Chrome-like triangles). No actual scrolling yet. --- examples/debug.rs | 18 ++++++++---------- examples/test_content.css | 4 +--- src/display_list.rs | 32 +++++++++++++++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 46f3b3172..5bcde1055 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -11,19 +11,17 @@ pub struct MyAppData { // Your app data goes here pub my_data: u32, } -/* -rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. -Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. -Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. -Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. -Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. -At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. -Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. -*/ impl LayoutScreen for MyAppData { fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label(format!("Hello"))) + Dom::new(NodeType::Label(format!( + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, \ + sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam \ + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, \ + consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore \ + magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et \ + ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."))) .with_class("__azul-native-button") .with_event(On::MouseUp, Callback(my_button_click_handler)) } diff --git a/examples/test_content.css b/examples/test_content.css index 663b7c10a..6e7bb839f 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -1,6 +1,5 @@ .__azul-native-button { background-color: #fcfcfc; - color: tomato; border: 1px solid #b7b7b7; border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; @@ -9,7 +8,6 @@ width: [[ my_id | 200px ]]; height: 200px; min-height: 400px; - text-align: center; flex-direction: row; flex-wrap: nowrap; justify-content: space-around; @@ -18,7 +16,7 @@ } * { - font-size: 16px; + font-size: 12px; font-family: "Webly Sleeky UI", sans-serif; color: black; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 27cd55e27..65cbf5eb3 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -475,7 +475,7 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); // If the rectangle should have a scrollbar, push a scrollbar onto the display list - push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds) + push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) } /// Adds a scrollbar to the left or bottom side of a rectangle. @@ -485,10 +485,19 @@ fn push_scrollbar( display_behaviour: &LayoutOverflow, scrollbar_info: &TextOverflowPass2, scrollbar_style: &ScrollbarInfo, - bounds: &TypedRect) + bounds: &TypedRect, + border: &Option<(BorderWidths, BorderDetails)>) { use euclid::TypedPoint2D; + // The border is inside the rectangle - subtract the border width on the left and bottom side, + // so that the scrollbar is laid out correctly + let mut bounds = *bounds; + if let Some((border_widths, _)) = border { + bounds.size.width -= border_widths.left; + bounds.size.height -= border_widths.bottom; + } + { // Background of scrollbar (vertical) let scrollbar_vertical_background = TypedRect:: { @@ -498,7 +507,7 @@ fn push_scrollbar( let scrollbar_vertical_background_info = PrimitiveInfo { rect: scrollbar_vertical_background, - clip_rect: *bounds, + clip_rect: bounds, is_backface_visible: false, tag: None, // TODO: for hit testing }; @@ -519,7 +528,7 @@ fn push_scrollbar( let scrollbar_vertical_bar_info = PrimitiveInfo { rect: scrollbar_vertical_bar, - clip_rect: *bounds, + clip_rect: bounds, is_backface_visible: false, tag: None, // TODO: for hit testing }; @@ -528,8 +537,8 @@ fn push_scrollbar( } { - // Triangle 1 - let scrollbar_vertical_bar = TypedRect:: { + // Triangle top + let mut scrollbar_triangle_rect = TypedRect:: { origin: TypedPoint2D::new( bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, bounds.origin.y + scrollbar_style.padding as f32), @@ -538,7 +547,16 @@ fn push_scrollbar( (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32), }; - push_triangle(&scrollbar_vertical_bar, builder, &scrollbar_style.triangle_style, TriangleDirection::PointUp); + scrollbar_triangle_rect.origin.x += scrollbar_triangle_rect.size.width / 4.0; + scrollbar_triangle_rect.origin.y += scrollbar_triangle_rect.size.height / 4.0; + scrollbar_triangle_rect.size.width /= 2.0; + scrollbar_triangle_rect.size.height /= 2.0; + + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_style, TriangleDirection::PointUp); + + // Triangle bottom + scrollbar_triangle_rect.origin.y += bounds.size.height - scrollbar_style.width as f32 + scrollbar_style.padding as f32; + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_style, TriangleDirection::PointDown); } } From 7e6726070d7765e3199655385057bcb62ddc1699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 1 Jun 2018 00:04:15 +0200 Subject: [PATCH 079/868] Added (simplistic) SVG parser / SVG path storage & caching infrastructure Note: when zooming in on an SVG, we need to re-tesselate the SvgLayerType::Circle and SvgLayerType::Rect structs using lyons simple-tesselation functions, not the regular path tesselator --- Cargo.toml | 2 +- assets/svg/test.svg | 126 ++++++++++++ examples/debug.rs | 9 + src/app.rs | 2 +- src/app_state.rs | 34 +++- src/css_parser.rs | 2 +- src/display_list.rs | 18 +- src/dom.rs | 59 +----- src/lib.rs | 7 +- src/resources.rs | 52 ++++- src/svg.rs | 481 +++++++++++++++++++++++++++++++++----------- src/traits.rs | 16 ++ 12 files changed, 625 insertions(+), 183 deletions(-) create mode 100644 assets/svg/test.svg diff --git a/Cargo.toml b/Cargo.toml index 38aeeff6f..801b9133f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ rusttype = "0.5.2" app_units = "0.6" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" -resvg = "0.2.0" +svg = "0.5.10" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.0" diff --git a/assets/svg/test.svg b/assets/svg/test.svg new file mode 100644 index 000000000..ce83f3fa3 --- /dev/null +++ b/assets/svg/test.svg @@ -0,0 +1,126 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + Hello + + diff --git a/examples/debug.rs b/examples/debug.rs index 5bcde1055..70d0c36cf 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -5,11 +5,14 @@ use azul::prelude::*; const TEST_CSS: &str = include_str!("test_content.css"); const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); +const TEST_SVG: &[u8] = include_bytes!("../assets/svg/test.svg"); #[derive(Debug)] pub struct MyAppData { // Your app data goes here pub my_data: u32, + // SVG IDs + pub my_svg_ids: Vec, } impl LayoutScreen for MyAppData { @@ -37,6 +40,11 @@ fn my_button_click_handler(app_state: &mut AppState, event: WindowEve // TODO: performance optimize this app_state.windows[event.window].state.title = String::from("Hello"); + // SVG parsing test + let mut svg_ids = app_state.add_svg(TEST_SVG).unwrap(); + println!("{:?}", svg_ids); + app_state.data.modify(|data| data.my_svg_ids.append(&mut svg_ids)); + UpdateScreen::Redraw } @@ -47,6 +55,7 @@ fn main() { let my_app_data = MyAppData { my_data: 0, + my_svg_ids: Vec::new(), }; let mut app = App::new(my_app_data); diff --git a/src/app.rs b/src/app.rs index 30e772052..839ba845f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -459,7 +459,7 @@ fn render( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, - app_resources: &mut AppResources, + app_resources: &mut AppResources, has_window_size_changed: bool) { use webrender::api::*; diff --git a/src/app_state.rs b/src/app_state.rs index 4da1f4701..9178a2a14 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,6 +11,7 @@ use font::FontError; use std::collections::hash_map::Entry::*; use FastHashMap; use std::sync::{Arc, Mutex}; +use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `LayoutScreen` trait (how the application @@ -28,7 +29,7 @@ pub struct AppState<'a, T: LayoutScreen> { /// ``` pub windows: Vec, /// Fonts and images that are currently loaded into the app - pub(crate) resources: AppResources<'a>, + pub(crate) resources: AppResources<'a, T>, /// Currently running deamons (polling functions) pub(crate) deamons: FastHashMap UpdateScreen>, /// Currently running tasks (asynchronous functions running on a different thread) @@ -188,6 +189,37 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { self.deamons.remove(id.as_ref()).is_some() } + /// A "SvgLayer" represents one or more shapes that get drawn using the same style (necessary for batching). + /// Adds the SVG layer as a resource to the internal resources, the returns the ID, which you can use in the + /// `NodeType::SvgLayer` to draw the SVG layer. + pub fn add_svg_layer(&mut self, layer: SvgLayer) + -> SvgLayerId + { + self.resources.add_svg_layer(layer) + } + + /// Deletes a specific shape from the app-internal resources. When drawing with an invalid ID, the app will crash + /// (in debug mode) or simply not draw the shape (in release mode) + pub fn delete_svg_layer(&mut self, svg_id: SvgLayerId) + { + self.resources.delete_svg_layer(svg_id); + } + + /// Clears all crate-internal resources and shapes. Use with care. + pub fn clear_all_svg_layers(&mut self) + { + self.resources.clear_all_svg_layers(); + } + + /// Parses an input source, parses the SVG, adds the shapes as layers into + /// the registry, returns the IDs of the added shapes, in the order that + /// they appeared in the SVG text. + pub fn add_svg(&mut self, input: R) + -> Result, SvgParseError> + { + self.resources.add_svg(input) + } + /// Run all currently registered deamons pub(crate) fn run_all_deamons(&self) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; diff --git a/src/css_parser.rs b/src/css_parser.rs index 07ae90b2b..3dbf69b85 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -492,7 +492,7 @@ fn parse_percentage_value(input: &str) /// /// "blue" -> "00FF00" -> ColorF { r: 0, g: 255, b: 0 }) /// "#00FF00" -> ColorF { r: 0, g: 255, b: 0 }) -fn parse_css_color<'a>(input: &'a str) +pub(crate) fn parse_css_color<'a>(input: &'a str) -> Result> { if input.starts_with('#') { diff --git a/src/display_list.rs b/src/display_list.rs index 65cbf5eb3..828baea31 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -105,7 +105,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { /// Looks if any new images need to be uploaded and stores the in the image resources fn update_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { Self::update_image_resources(api, app_resources, resource_updates); @@ -114,7 +114,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { fn update_image_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { use images::{ImageState, ImageInfo}; @@ -164,7 +164,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // have two HashMaps that need to be updated fn update_font_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { use font::FontState; @@ -214,7 +214,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi, mut has_window_size_changed: bool) -> Option @@ -390,7 +390,7 @@ fn push_text( rect_idx: NodeId, builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, resource_updates: &mut Vec) @@ -674,12 +674,12 @@ fn push_box_shadow( } #[inline] -fn push_background( +fn push_background( info: &PrimitiveInfo, bounds: &TypedRect, builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &AppResources) + app_resources: &AppResources) { let background = match style.background { Some(ref bg) => bg, @@ -745,11 +745,11 @@ fn push_border( } #[inline] -fn push_font( +fn push_font( font_id: &css_parser::Font, font_size_app_units: Au, resource_updates: &mut Vec, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi) -> Option { diff --git a/src/dom.rs b/src/dom.rs index f72a8a59d..be7fce6ce 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -11,7 +11,7 @@ use std::cell::RefCell; use std::hash::{Hash, Hasher}; use webrender::api::ColorU; use glium::Texture2d; -use svg::Svg; +use svg::SvgLayerId; /// This is only accessed from the main thread, so it's safe to use pub(crate) static mut NODE_ID: u64 = 0; @@ -76,64 +76,23 @@ impl Copy for Callback { } /// `Image` and `Label`. For example a `Ul` is simply a convenience /// wrapper around a repeated (`Div` + `Label`) clone where the first /// `Div` is shaped like a circle (for `Ul`). -pub enum NodeType { +#[derive(Debug, Clone, PartialEq, Hash, Eq)] +pub enum NodeType { /// Regular div Div, /// A label that can be (optionally) be selectable with the mouse Label(String), - Svg(Svg), + SvgLayer(SvgLayerId), // GlTexture } -impl fmt::Debug for NodeType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "NodeType<{}>", self.get_css_id()) - } -} - -impl Clone for NodeType { - fn clone(&self) -> Self { - use self::NodeType::*; - match self { - Div => Div, - Label(text) => Label(text.clone()), - Svg(svg) => Svg(svg.clone()), - } - } -} - -impl PartialEq for NodeType { - fn eq(&self, rhs: &Self) -> bool { - use self::NodeType::*; - match (self, rhs) { - (Div, Div) => true, - (Label(a), Label(b)) => a == b, - (Svg(a), Svg(b)) => *a == *b, - _ => false, - } - } -} - -impl Eq for NodeType { } - -impl Hash for NodeType { - fn hash(&self, state: &mut H) { - use self::NodeType::*; - match self { - Div => 0.hash(state), - Label(l) => { 1.hash(state); l.hash(state) }, - Svg(s) => { 2.hash(state); s.hash(state) }, - } - } -} - -impl GetCssId for NodeType { +impl GetCssId for NodeType { fn get_css_id(&self) -> &'static str { use self::NodeType::*; match *self { Div => "div", Label(_) => "p", - Svg(_) => "svg", + SvgLayer(_) => "svg", } } } @@ -176,7 +135,7 @@ pub enum On { #[derive(PartialEq, Eq)] pub(crate) struct NodeData { /// `div` - pub node_type: NodeType, + pub node_type: NodeType, /// `#main` pub id: Option, /// `.myclass .otherclass` @@ -250,7 +209,7 @@ impl CallbackList { impl NodeData { /// Creates a new NodeData - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType) -> Self { Self { node_type: node_type, id: None, @@ -329,7 +288,7 @@ impl Dom { /// Creates an empty DOM #[inline] - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType) -> Self { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new(node_type)); Self { diff --git a/src/lib.rs b/src/lib.rs index 8e474a22c..28a9972b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,8 @@ pub extern crate image; #[macro_use] extern crate lazy_static; extern crate euclid; -extern crate resvg; extern crate lyon; +extern crate svg as svg_crate; extern crate webrender; extern crate cassowary; extern crate twox_hash; @@ -105,7 +105,7 @@ pub mod prelude { pub use app_state::AppState; pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; - pub use traits::LayoutScreen; + pub use traits::{LayoutScreen, ModifyAppState}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, WindowMonitorTarget, RendererType, WindowEvent}; @@ -127,6 +127,8 @@ pub mod prelude { ExtendMode, PixelValue, PercentageValue, }; + pub use svg::{SvgLayerId, SvgLayer}; + // from the extern crate image pub use image::ImageError; } @@ -142,4 +144,5 @@ pub mod errors { }; pub use simplecss::Error as CssSyntaxError; pub use css::{CssParseError, DynamicCssParseError}; + pub use svg::SvgParseError; } diff --git a/src/resources.rs b/src/resources.rs index 68fd8ca83..1f1b31de1 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,3 +1,4 @@ +use traits::LayoutScreen; use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey, FontInstanceKey}; use FastHashMap; @@ -10,6 +11,7 @@ use std::collections::hash_map::Entry::*; use app_units::Au; use css_parser; use css_parser::Font::ExternalFont; +use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; /// Font and image keys /// @@ -21,8 +23,8 @@ use css_parser::Font::ExternalFont; /// /// Images and fonts can be references across window contexts /// (not yet tested, but should work). -#[derive(Default, Clone)] -pub(crate) struct AppResources<'a> { +#[derive(Clone)] +pub(crate) struct AppResources<'a, T: LayoutScreen> { /// Image cache pub(crate) images: FastHashMap, // Fonts are trickier to handle than images. @@ -35,9 +37,23 @@ pub(crate) struct AppResources<'a> { // the font instance key (if there is any). If there is no font instance key, // we first need to create one. pub(crate) fonts: FastHashMap>, + /// Stores the polygon data for all SVGs. Polygons can be shared across windows + /// without duplicating the data. This doesn't store any rendering-related data, only the polygons + pub(crate) svg_registry: SvgRegistry, } -impl<'a> AppResources<'a> { +impl<'a, T: LayoutScreen> Default for AppResources<'a, T> { + fn default() -> Self { + Self { + svg_registry: SvgRegistry::default(), + fonts: FastHashMap::default(), + font_data: FastHashMap::default(), + images: FastHashMap::default(), + } + } +} + +impl<'a, T: LayoutScreen> AppResources<'a, T> { /// See `AppState::add_image()` pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) @@ -128,4 +144,34 @@ impl<'a> AppResources<'a> { } } } + + /// A "SvgLayer" represents one or more shapes that get drawn using the same style (necessary for batching). + /// Adds the SVG layer as a resource to the internal resources, the returns the ID, which you can use in the + /// `NodeType::SvgLayer` to draw the SVG layer. + pub(crate) fn add_svg_layer(&mut self, layer: SvgLayer) + -> SvgLayerId + { + self.svg_registry.add_layer(layer) + } + + /// See `AppState::` + pub(crate) fn delete_svg_layer(&mut self, svg_id: SvgLayerId) + { + self.svg_registry.delete_layer(svg_id); + } + + /// Clears all crate-internal resources and shapes. Use with care. + pub(crate) fn clear_all_svg_layers(&mut self) + { + self.svg_registry.clear_all_layers(); + } + + /// Parses an input source, parses the SVG, adds the shapes as layers into + /// the registry, returns the IDs of the added shapes, in the order that + /// they appeared in the SVG text. + pub fn add_svg(&mut self, input: R) + -> Result, SvgParseError> + { + self.svg_registry.add_svg(input) + } } \ No newline at end of file diff --git a/src/svg.rs b/src/svg.rs index 6ac1458be..7bd950716 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,64 +1,94 @@ +use std::io::Read; +use lyon::path::default::Path; +use webrender::api::ColorU; use dom::Callback; use traits::LayoutScreen; -use std::sync::atomic::AtomicUsize; +use std::sync::atomic::{Ordering, AtomicUsize}; use FastHashMap; use std::hash::{Hash, Hasher}; +use svg_crate::parser::Error as SvgError; +use std::io::Error as IoError; +use std::fmt; /// In order to store / compare SVG files, we have to pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct SvgId(usize); +pub struct SvgLayerId(usize); -pub(crate) struct SvgRegistry { - svg_items: FastHashMap, +#[derive(Clone)] +pub(crate) struct SvgRegistry { + // note: one "layer" merely describes one or more polygons that have the same style + layers: FastHashMap>, } -impl SvgRegistry { - pub fn add_shape(&mut self, polygon: SvgShape) -> SvgId { - // TODO - SvgId(0) - } - pub fn delete_shape(&mut self, svg_id: SvgId) { - // TODO +impl Default for SvgRegistry { + fn default() -> Self { + Self { + layers: FastHashMap::default(), + } } } -pub struct Svg { - pub layers: Vec>, +impl fmt::Debug for SvgRegistry { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for layer in self.layers.keys() { + write!(f, "{:?}", layer)?; + } + Ok(()) + } } -impl Clone for Svg { - fn clone(&self) -> Self { - Self { layers: self.layers.clone() } +impl SvgRegistry { + + pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { + let new_svg_id = SvgLayerId(unsafe { SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst) }); + self.layers.insert(new_svg_id, layer); + new_svg_id } -} -impl Hash for Svg { - fn hash(&self, state: &mut H) { - for layer in &self.layers { - layer.hash(state); - } + pub fn delete_layer(&mut self, svg_id: SvgLayerId) { + self.layers.remove(&svg_id); + } + + pub fn clear_all_layers(&mut self) { + self.layers.clear(); + } + + /// Parses an input source, parses the SVG, adds the shapes as layers into + /// the registry, returns the IDs of the added shapes, in the order that they appeared in the Svg + pub fn add_svg(&mut self, input: R) -> Result, SvgParseError> { + Ok(self::svg_to_lyon::parse_from(input)? + .into_iter() + .map(|layer| + self.add_layer(layer)) + .collect()) } } -impl PartialEq for Svg { - fn eq(&self, rhs: &Self) -> bool { - for (a, b) in self.layers.iter().zip(rhs.layers.iter()) { - if *a != *b { - return false - } - } - true +#[derive(Debug)] +pub enum SvgParseError { + /// Syntax error in the Svg + FailedToParseSvg(SvgError), + /// Io error reading the Svg + IoError(IoError), +} + +impl From for SvgParseError { + fn from(e: SvgError) -> Self { + SvgParseError::FailedToParseSvg(e) } } -impl Eq for Svg { } +impl From for SvgParseError { + fn from(e: IoError) -> Self { + SvgParseError::IoError(e) + } +} pub struct SvgLayer { - pub id: String, - pub data: Vec, + pub data: SvgLayerType, pub callbacks: SvgCallbacks, pub style: SvgStyle, } @@ -66,7 +96,6 @@ pub struct SvgLayer { impl Clone for SvgLayer { fn clone(&self) -> Self { Self { - id: self.id.clone(), data: self.data.clone(), callbacks: self.callbacks.clone(), style: self.style.clone(), @@ -74,26 +103,6 @@ impl Clone for SvgLayer { } } -impl Hash for SvgLayer { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.data.hash(state); - self.callbacks.hash(state); - self.style.hash(state); - } -} - -impl PartialEq for SvgLayer { - fn eq(&self, rhs: &Self) -> bool { - self.id == rhs.id && - self.data == rhs.data && - self.callbacks == rhs.callbacks && - self.style == rhs.style - } -} - -impl Eq for SvgLayer { } - pub enum SvgCallbacks { // No callbacks for this layer None, @@ -101,7 +110,7 @@ pub enum SvgCallbacks { Any(Callback), /// Call the callback when the SvgLayer item at index [x] is /// hovered over / interacted with - Some(Vec<(SvgId, Callback)>), + Some(Vec<(usize, Callback)>), } impl Clone for SvgCallbacks { @@ -140,89 +149,331 @@ impl PartialEq for SvgCallbacks { impl Eq for SvgCallbacks { } -#[derive(Debug, Copy, Clone, PartialEq, Hash)] -pub struct Rgba { - pub data: [u8;4], -} - -#[derive(Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] pub struct SvgStyle { - pub outline: Option, - pub fill: Option, + /// Stroke color + pub stroke: Option, + /// Stroke width * 1000, since otherwise `Hash` can't be derived + /// + /// i.e. a stroke width of `5.0` = `5000`. + pub stroke_width: Option, + /// Fill color + pub fill: Option, + // missing: + // + // fill-opacity + // stroke-miterlimit + // stroke-dasharray + // stroke-opacity } -#[derive(Debug, Clone, PartialEq)] -pub enum SvgShape { - Polygon(Vec<(f32, f32)>), -} +impl SvgStyle { + /// Parses the Svg style from a string, on error returns the default `SvgStyle`. + pub fn from_svg_string(input: &str) -> Self { + use css_parser::parse_css_color; + use FastHashMap; + + let mut style = FastHashMap::<&str, &str>::default(); + + for kv in input.split(";") { + let mut iter = kv.trim().split(":"); + let key = iter.next(); + let value = iter.next(); + if let (Some(k), Some(v)) = (key, value) { + style.insert(k, v); + } + } + + let fill = style.get("fill") + .and_then(|s| parse_css_color(s).ok()); -// SvgShape::Polygon(vec![(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)]) + let stroke = style.get("stroke") + .and_then(|s| parse_css_color(s).ok()); + + let stroke_width = style.get("stroke-width") + .and_then(|s| s.parse::().ok()) + .and_then(|sw_float| Some((sw_float * 1000.0) as usize)); -impl Svg { - pub fn default_testing() -> Self { Self { - layers: vec![SvgLayer { - id: String::from("svg-layer-01"), - // simple triangle for testing - data: vec![SvgId(0)], - callbacks: SvgCallbacks::None, - style: SvgStyle { - outline: Some(Rgba { data: [0, 0, 0, 255] }), - fill: Some(Rgba { data: [255, 0, 0, 255] }), - } - }] + fill, + stroke_width, + stroke, } } } -mod resvg_to_lyon { +/// One "layer" is simply one or more polygons that get drawn using the same style +/// i.e. one SVG `` element +#[derive(Debug, Clone)] +pub enum SvgLayerType { + Polygon(Path), + Circle(SvgCircle), + Rect(SvgRect), + Text(String), +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SvgCircle { + pub center_x: f32, + pub center_y: f32, + pub radius: f32, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct SvgRect { + pub width: f32, + pub height: f32, + pub x: f32, + pub y: f32, + pub rx: f32, + pub ry: f32, +} - use resvg::tree::{self, Color, Paint, Stroke, PathSegment}; +mod svg_to_lyon { + use svg_crate::node::Attributes; + use std::io::Read; + use std::collections::HashMap; + use lyon::path::default::Path; use lyon::{ - path::PathEvent, + path::{PathEvent, default::Builder, builder::SvgPathBuilder}, tessellation::{self, StrokeOptions}, math::Point, + geom::{ArcFlags, euclid::{TypedPoint2D, TypedVector2D, Angle}}, + path::{SvgEvent, builder::SvgBuilder}, }; + use svg::{SvgCircle, SvgRect, SvgParseError, SvgLayer, SvgStyle}; + use svg_crate::node::element::path::Parameters; + use svg_crate::node::element::tag::Tag; + use traits::LayoutScreen; + + pub fn parse_from(svg_source: R) + -> Result>, SvgParseError> + { + use svg_crate::{read, parser::{Event, Error}}; + use std::mem::discriminant; + use svg::{SvgLayerType, SvgCallbacks}; + + let file = read(svg_source)?; + + let mut last_err = None; + + let layer_data = file + // We are only interested in tags, not comments or other stuff + .filter_map(|event| match event { + Event::Tag(id, _, attributes) => Some((id, attributes)), + Event::Error(e) => { /* TODO: hacky */ last_err = Some(e); None }, + _ => None, + } + ) + // assert that the shape has a style. If it doesn't have a style, we can't draw it, + // so there is no point in parsing it + .filter_map(|(id, attributes)| { + let svg_style = match attributes.get("style") { + Some(style_string) => SvgStyle::from_svg_string(style_string), + _ => return None, + }; + Some((id, svg_style, attributes)) + }) + // Now parse the shape + .filter_map(|(id, style, attributes)| { + let layer_data = match id { + "path" => match parse_path(&attributes) { + None => return None, + Some(s) => SvgLayerType::Polygon(s), + } + "circle" => match parse_circle(&attributes) { + None => return None, + Some(s) => SvgLayerType::Circle(s), + }, + "rect" => match parse_rect(&attributes) { + None => return None, + Some(s) => SvgLayerType::Rect(s), + }, + "flowRoot" => match parse_flow_root(&attributes) { + None => return None, + Some(s) => SvgLayerType::Text(s), + }, + "text" => match parse_text(&attributes) { + None => return None, + Some(s) => SvgLayerType::Text(s), + }, + _ => return None, + }; + Some((layer_data, style)) + }) + .map(|(data, style)| { + SvgLayer { + data: data, + callbacks: SvgCallbacks::None, + style: style, + } + }) + .collect(); - fn point(x: f64, y: f64) -> Point { - Point::new(x as f32, y as f32) + if let Some(e) = last_err { + Err(e.into()) + } else { + Ok(layer_data) + } } - pub const FALLBACK_COLOR: Color = Color { red: 0, green: 0, blue: 0 }; - - pub(super) fn as_event(ps: &PathSegment) -> PathEvent { - match *ps { - PathSegment::MoveTo { x, y } => PathEvent::MoveTo(point(x, y)), - PathSegment::LineTo { x, y } => PathEvent::LineTo(point(x, y)), - PathSegment::CurveTo { x1, y1, x2, y2, x, y, } => { - PathEvent::CubicTo(point(x1, y1), point(x2, y2), point(x, y)) + fn parse_path(attributes: &Attributes) -> Option { + use lyon::path::default::Builder; + use lyon::path::builder::SvgPathBuilder; + use lyon::path::builder::FlatPathBuilder; + use lyon::path::SvgEvent; + use svg_crate::node::element::{ + tag::Path, + path::{Command, Command::*, Data}, + }; + use svg_crate::node::element::path::Position::*; + + let data = attributes.get("d")?; + let data = Data::parse(data).ok()?; + + let mut builder = SvgPathBuilder::new(Builder::new()); + + for command in data.iter() { + match command { + Move(position, parameters) => match position { + Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::MoveTo(TypedPoint2D::new(x, y))), + _ => { }, + }), + Relative => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::RelativeMoveTo(TypedVector2D::new(x, y))), + _ => { }, + }), + }, + Line(position, parameters) => match position { + Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::LineTo(TypedPoint2D::new(x, y))), + _ => { }, + }), + Relative => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::RelativeLineTo(TypedVector2D::new(x, y))), + _ => { }, + }), + }, + HorizontalLine(position, parameters) => match position { + Absolute => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::HorizontalLineTo(*num))), + Relative => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::RelativeHorizontalLineTo(*num))), + }, + VerticalLine(position, parameters) => match position { + Absolute => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::VerticalLineTo(*num))), + Relative => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::RelativeVerticalLineTo(*num))), + }, + QuadraticCurve(position, parameters) => match position { + Absolute => parameters.chunks(4).for_each(|chunk| match *chunk { + [x1, y1, x2, y2] => builder.svg_event(SvgEvent::QuadraticTo(TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2))), + _ => { }, + }), + Relative => parameters.chunks(4).for_each(|chunk| match *chunk { + [x1, y1, x2, y2] => builder.svg_event(SvgEvent::RelativeQuadraticTo( + TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2))), + _ => { }, + }), + }, + SmoothQuadraticCurve(position, parameters) => match position { + Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::SmoothQuadraticTo(TypedPoint2D::new(x, y))), + _ => { }, + }), + Relative => parameters.chunks(2).for_each(|chunk| match *chunk { + [x, y] => builder.svg_event(SvgEvent::SmoothRelativeQuadraticTo(TypedVector2D::new(x, y))), + _ => { }, + }), + }, + CubicCurve(position, parameters) => match position { + Absolute => parameters.chunks(6).for_each(|chunk| match *chunk { + [x1, y1, x2, y2, x3, y3] => builder.svg_event(SvgEvent::CubicTo( + TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2), TypedPoint2D::new(x3, y3))), + _ => { }, + }), + Relative => parameters.chunks(6).for_each(|chunk| match *chunk { + [x1, y1, x2, y2, x3, y3] => builder.svg_event(SvgEvent::RelativeCubicTo( + TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2), TypedVector2D::new(x3, y3))), + _ => { }, + }), + }, + SmoothCubicCurve(position, parameters) => match position { + Absolute => parameters.chunks(4).for_each(|chunk| match *chunk { + [x1, y1, x2, y2] => builder.svg_event(SvgEvent::SmoothCubicTo( + TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2))), + _ => { }, + }), + Relative => parameters.chunks(4).for_each(|chunk| match *chunk { + [x1, y1, x2, y2] => builder.svg_event(SvgEvent::SmoothRelativeCubicTo( + TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2))), + _ => { }, + }), + }, + EllipticalArc(position, parameters) => match position { + Absolute => parameters.chunks(5).for_each(|chunk| match *chunk { + [x1, y1, angle, x2, y2] => builder.svg_event( + SvgEvent::ArcTo( + TypedVector2D::new(x1, y1), + Angle::degrees(angle), + ArcFlags { large_arc: true, sweep: true, }, + TypedPoint2D::new(x2, y2) + )), + _ => { }, + }), + Relative => parameters.chunks(5).for_each(|chunk| match *chunk { + [x1, y1, angle, x2, y2] => builder.svg_event( + SvgEvent::ArcTo( + TypedVector2D::new(x1, y1), + Angle::degrees(angle), + ArcFlags { large_arc: true, sweep: true, }, + TypedPoint2D::new(x2, y2) + )), + _ => { }, + }), + }, + Close => { + builder.close(); + }, } - PathSegment::ClosePath => PathEvent::Close, } + + Some(builder.build()) } - pub(super) fn convert_stroke(s: &Stroke) -> (Color, StrokeOptions) { - let color = match s.paint { - Paint::Color(c) => c, - _ => FALLBACK_COLOR, - }; - let linecap = match s.linecap { - tree::LineCap::Butt => tessellation::LineCap::Butt, - tree::LineCap::Square => tessellation::LineCap::Square, - tree::LineCap::Round => tessellation::LineCap::Round, - }; - let linejoin = match s.linejoin { - tree::LineJoin::Miter => tessellation::LineJoin::Miter, - tree::LineJoin::Bevel => tessellation::LineJoin::Bevel, - tree::LineJoin::Round => tessellation::LineJoin::Round, - }; + fn parse_circle(attributes: &Attributes) -> Option { + let center_x = attributes.get("cx")?.parse::().ok()?; + let center_y = attributes.get("cy")?.parse::().ok()?; + let radius = attributes.get("r")?.parse::().ok()?; + + Some(SvgCircle { + center_x, + center_y, + radius + }) + } - let opt = StrokeOptions::tolerance(0.01) - .with_line_width(s.width as f32) - .with_line_cap(linecap) - .with_line_join(linejoin); + fn parse_rect(attributes: &Attributes) -> Option { + let width = attributes.get("width")?.parse::().ok()?; + let height = attributes.get("height")?.parse::().ok()?; + let x = attributes.get("x")?.parse::().ok()?; + let y = attributes.get("y")?.parse::().ok()?; + let rx = attributes.get("rx")?.parse::().ok()?; + let ry = attributes.get("ry")?.parse::().ok()?; + Some(SvgRect { + width, + height, + x, y, + rx, ry + }) + } - (color, opt) + // TODO: use text attributes instead of string + fn parse_flow_root(attributes: &Attributes) -> Option { + Some(String::from("hello")) } -} \ No newline at end of file + + // TODO: use text attributes instead of string + fn parse_text(attributes: &Attributes) -> Option { + Some(String::from("hello")) + } +} diff --git a/src/traits.rs b/src/traits.rs index 5a9601f82..8f63bdaf1 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use std::cell::RefCell; use std::hash::Hash; use css_parser::{ParsedCssProperty, CssParsingError}; +use std::sync::{Arc, Mutex}; pub trait LayoutScreen { /// Updates the DOM, must be provided by the final application. @@ -49,6 +50,21 @@ pub trait IntoParsedCssProperty<'a> { fn into_parsed_css_property(self) -> Result>; } +pub trait ModifyAppState { + /// Modifies the app state and then returns if the modification was successful + /// Takes a FnMut that modifies the state + fn modify(&self, closure: F) -> bool where F: FnMut(&mut T); +} + +impl ModifyAppState for Arc> { + fn modify(&self, mut closure: F) -> bool where F: FnMut(&mut T) { + match self.lock().as_mut() { + Ok(lock) => { closure(&mut *lock); true }, + Err(_) => false, + } + } +} + impl<'a> IntoParsedCssProperty<'a> for ParsedCssProperty { fn into_parsed_css_property(self) -> Result> { Ok(self.clone()) From 88b2bda6a6eb1c6ea278af7ac8a6299352c8bd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 2 Jun 2018 21:15:34 +0200 Subject: [PATCH 080/868] Renamed trait LayoutScreen { fn get_dom() } to trait Layout { fn layout() } --- examples/debug.rs | 37 +++++++++++-------------------- examples/test_content.css | 1 + src/app.rs | 14 ++++++------ src/app_state.rs | 14 ++++++------ src/cache.rs | 6 ++--- src/display_list.rs | 18 +++++++-------- src/dom.rs | 46 +++++++++++++++++++-------------------- src/lib.rs | 2 +- src/resources.rs | 8 +++---- src/svg.rs | 28 ++++++++++++------------ src/task.rs | 4 ++-- src/traits.rs | 14 ++++++------ src/ui_description.rs | 10 ++++----- src/ui_state.rs | 10 ++++----- src/window.rs | 12 +++++----- 15 files changed, 107 insertions(+), 117 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 70d0c36cf..7304cbd0a 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -15,36 +15,25 @@ pub struct MyAppData { pub my_svg_ids: Vec, } -impl LayoutScreen for MyAppData { - fn get_dom(&self, _window_id: WindowId) -> Dom { - Dom::new(NodeType::Label(format!( - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, \ - sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam \ - voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ - no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, \ - consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore \ - magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et \ - ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."))) +impl Layout for MyAppData { + fn layout(&self, _window_id: WindowId) -> Dom { + let mut dom = Dom::new(NodeType::Label(format!("Load SVG file"))) .with_class("__azul-native-button") - .with_event(On::MouseUp, Callback(my_button_click_handler)) - } -} - -fn my_button_click_handler(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + .with_event(On::MouseUp, Callback(my_button_click_handler)); - // TODO: The DisplayList does somehow not register / override the new ID - // This is probably an issue of timing, see the notes in the app.rs file - app_state.windows[event.window].css.set_dynamic_property("my_id", ("width", "500px")).unwrap(); + for polygon in &self.my_svg_ids { + // Draw the cached polygon by its ID + dom.add_sibling(Dom::new(NodeType::SvgLayer(*polygon))); + } - // This works: When the mouse is moved over the button, the title switches to "Hello". - // TODO: performance optimize this - app_state.windows[event.window].state.title = String::from("Hello"); + dom + } +} - // SVG parsing test +fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { + // Load and parse the SVG file, register polygon data as IDs let mut svg_ids = app_state.add_svg(TEST_SVG).unwrap(); - println!("{:?}", svg_ids); app_state.data.modify(|data| data.my_svg_ids.append(&mut svg_ids)); - UpdateScreen::Redraw } diff --git a/examples/test_content.css b/examples/test_content.css index 6e7bb839f..1c33a0261 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -12,6 +12,7 @@ flex-wrap: nowrap; justify-content: space-around; align-items: center; + text-align: center; align-content: center; } diff --git a/src/app.rs b/src/app.rs index 839ba845f..b2763f8b1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use window::FakeWindow; use css::{Css, FakeCss}; use resources::AppResources; use app_state::AppState; -use traits::LayoutScreen; +use traits::Layout; use ui_state::UiState; use ui_description::UiDescription; @@ -18,7 +18,7 @@ use font::FontError; use webrender::api::{RenderApi, HitTestFlags}; /// Graphical application that maintains some kind of application state -pub struct App<'a, T: LayoutScreen> { +pub struct App<'a, T: Layout> { /// The graphical windows, indexed by ID windows: Vec>, /// The global application state @@ -47,7 +47,7 @@ impl Default for FrameEventInfo { } } -impl<'a, T: LayoutScreen> App<'a, T> { +impl<'a, T: Layout> App<'a, T> { /// Create a new, empty application. This does not open any windows. pub fn new(initial_data: T) -> Self { @@ -325,8 +325,8 @@ impl<'a, T: LayoutScreen> App<'a, T> { /// # /// # struct MyAppData { } /// # - /// # impl LayoutScreen for MyAppData { - /// # fn get_dom(&self, _window_id: WindowId) -> Dom { + /// # impl Layout for MyAppData { + /// # fn layout(&self, _window_id: WindowId) -> Dom { /// # Dom::new(NodeType::Div) /// # } /// # } @@ -405,7 +405,7 @@ impl<'a, T: LayoutScreen> App<'a, T> { } } -impl<'a, T: LayoutScreen + Send + 'static> App<'a, T> { +impl<'a, T: Layout + Send + 'static> App<'a, T> { /// Tasks, once started, cannot be stopped, which is why there is no `.delete()` function pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) { @@ -455,7 +455,7 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { false } -fn render( +fn render( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, diff --git a/src/app_state.rs b/src/app_state.rs index 9178a2a14..5f5d79b9f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2,7 +2,7 @@ use window::FakeWindow; use window_state::WindowState; use task::Task; use dom::UpdateScreen; -use traits::LayoutScreen; +use traits::Layout; use resources::{AppResources}; use std::io::Read; use images::ImageType; @@ -14,9 +14,9 @@ use std::sync::{Arc, Mutex}; use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; /// Wrapper for your application data. In order to be layout-able, -/// you need to satisfy the `LayoutScreen` trait (how the application +/// you need to satisfy the `Layout` trait (how the application /// should be laid out) -pub struct AppState<'a, T: LayoutScreen> { +pub struct AppState<'a, T: Layout> { /// Your data (the global struct which all callbacks will have access to) pub data: Arc>, /// Note: this isn't the real window state. This is a "mock" window state which @@ -36,7 +36,7 @@ pub struct AppState<'a, T: LayoutScreen> { pub(crate) tasks: Vec, } -impl<'a, T: LayoutScreen> AppState<'a, T> { +impl<'a, T: Layout> AppState<'a, T> { /// Creates a new `AppState` pub fn new(initial_data: T) -> Self { @@ -119,8 +119,8 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { /// /// struct MyAppData { } /// - /// impl LayoutScreen for MyAppData { - /// fn get_dom(&self, _window_id: WindowId) -> Dom { + /// impl Layout for MyAppData { + /// fn layout(&self, _window_id: WindowId) -> Dom { /// let mut dom = Dom::new(NodeType::Div); /// dom.event(On::MouseEnter, Callback(my_callback)); /// dom @@ -241,7 +241,7 @@ impl<'a, T: LayoutScreen> AppState<'a, T> { } } -impl<'a, T: LayoutScreen + Send + 'static> AppState<'a, T> { +impl<'a, T: Layout + Send + 'static> AppState<'a, T> { /// Tasks, once started, cannot be stopped pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) { diff --git a/src/cache.rs b/src/cache.rs index c48e955c8..a621345d3 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -39,7 +39,7 @@ use std::collections::BTreeMap; use constraints::DisplayRect; use cassowary::Solver; use id_tree::{NodeId, Arena}; -use traits::LayoutScreen; +use traits::Layout; use dom::NodeData; use std::ops::Deref; @@ -83,7 +83,7 @@ impl DomTreeCache { } } - pub(crate) fn update(&mut self, new_root: NodeId, new_nodes_arena: &Arena>) -> DomChangeSet { + pub(crate) fn update(&mut self, new_root: NodeId, new_nodes_arena: &Arena>) -> DomChangeSet { use std::hash::Hash; @@ -158,7 +158,7 @@ impl DomTreeCache { current_root: NodeId, current_dom_arena: &Arena>, changeset: &mut DomChangeSet) - where T: LayoutScreen + where T: Layout { let mut old_child_iterator = previous_root.children(previous_hash_arena); let mut new_child_iterator = current_root.children(previous_hash_arena); diff --git a/src/display_list.rs b/src/display_list.rs index 828baea31..166b8340d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -14,7 +14,7 @@ use cassowary::{Constraint, Solver, Variable}; use { FastHashMap, resources::AppResources, - traits::LayoutScreen, + traits::Layout, constraints::{DisplayRect, CssConstraint}, ui_description::{UiDescription, StyledNode}, window::{WindowDimensions, UiSolver}, @@ -30,7 +30,7 @@ use { const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); -pub(crate) struct DisplayList<'a, T: LayoutScreen + 'a> { +pub(crate) struct DisplayList<'a, T: Layout + 'a> { pub(crate) ui_descr: &'a UiDescription, pub(crate) rectangles: Arena> } @@ -55,12 +55,12 @@ pub(crate) struct DisplayRectangle<'a> { /// with re-creation it can take up to 9 ms. So the goal is to not re-create constraints /// if their contents haven't changed. #[derive(Default)] -pub(crate) struct SolvedLayout { +pub(crate) struct SolvedLayout { // List of previously solved constraints pub(crate) solved_constraints: FastHashMap>, } -impl SolvedLayout { +impl SolvedLayout { pub fn empty() -> Self { Self { solved_constraints: FastHashMap::default(), @@ -80,7 +80,7 @@ impl<'a> DisplayRectangle<'a> { } } -impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { +impl<'a, T: Layout + 'a> DisplayList<'a, T> { /// NOTE: This function assumes that the UiDescription has an initialized arena /// @@ -281,7 +281,7 @@ impl<'a, T: LayoutScreen + 'a> DisplayList<'a, T> { // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); // debug rectangle - let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), LayoutSize::new(200.0, 200.0)); + let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), ui_solver.window_dimensions.layout_size); let info = LayoutPrimitiveInfo { rect: bounds, @@ -384,7 +384,7 @@ fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder } #[inline] -fn push_text( +fn push_text( info: &PrimitiveInfo, display_list: &DisplayList, rect_idx: NodeId, @@ -674,7 +674,7 @@ fn push_box_shadow( } #[inline] -fn push_background( +fn push_background( info: &PrimitiveInfo, bounds: &TypedRect, builder: &mut DisplayListBuilder, @@ -745,7 +745,7 @@ fn push_border( } #[inline] -fn push_font( +fn push_font( font_id: &css_parser::Font, font_size_app_units: Au, resource_updates: &mut Vec, diff --git a/src/dom.rs b/src/dom.rs index be7fce6ce..9aa847563 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,7 +1,7 @@ use window::WindowEvent; use traits::GetCssId; use app_state::AppState; -use traits::LayoutScreen; +use traits::Layout; use std::collections::BTreeMap; use id_tree::{NodeId, Arena}; use std::sync::{Arc, Mutex}; @@ -35,15 +35,15 @@ pub enum UpdateScreen { /// The CSS is not affected by this, so if you push to the windows' CSS inside the /// function, the screen will not be automatically redrawn, unless you return an /// `UpdateScreen::Redraw` from the function -pub struct Callback(pub fn(&mut AppState, WindowEvent) -> UpdateScreen); +pub struct Callback(pub fn(&mut AppState, WindowEvent) -> UpdateScreen); -impl fmt::Debug for Callback { +impl fmt::Debug for Callback { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Callback @ 0x{:x}", self.0 as usize) } } -impl Clone for Callback { +impl Clone for Callback { fn clone(&self) -> Self { Callback(self.0.clone()) } @@ -53,22 +53,22 @@ impl Clone for Callback { /// as a unique ID for the function. This way, we can hash and compare DOM nodes /// (to create diffs between two states). Comparing usizes is more efficient /// than re-creating the whole DOM and serves as a caching mechanism. -impl Hash for Callback { +impl Hash for Callback { fn hash(&self, state: &mut H) where H: Hasher { state.write_usize(self.0 as usize); } } /// Basically compares the function pointers and types for equality -impl PartialEq for Callback { +impl PartialEq for Callback { fn eq(&self, rhs: &Self) -> bool { self.0 as usize == rhs.0 as usize } } -impl Eq for Callback { } +impl Eq for Callback { } -impl Copy for Callback { } +impl Copy for Callback { } /// List of allowed DOM node types that are supported by `azul`. /// @@ -133,7 +133,7 @@ pub enum On { } #[derive(PartialEq, Eq)] -pub(crate) struct NodeData { +pub(crate) struct NodeData { /// `div` pub node_type: NodeType, /// `#main` @@ -146,7 +146,7 @@ pub(crate) struct NodeData { pub tag: Option, } -impl Hash for NodeData { +impl Hash for NodeData { fn hash(&self, state: &mut H) { self.node_type.hash(state); self.id.hash(state); @@ -159,7 +159,7 @@ impl Hash for NodeData { use cache::DomHash; -impl NodeData { +impl NodeData { pub fn calculate_node_data_hash(&self) -> DomHash { use std::hash::Hash; use twox_hash::XxHash; @@ -169,7 +169,7 @@ impl NodeData { } } -impl Clone for NodeData { +impl Clone for NodeData { fn clone(&self) -> Self { Self { node_type: self.node_type.clone(), @@ -181,7 +181,7 @@ impl Clone for NodeData { } } -impl fmt::Debug for NodeData { +impl fmt::Debug for NodeData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "NodeData {{ \ @@ -199,7 +199,7 @@ impl fmt::Debug for NodeData { } } -impl CallbackList { +impl CallbackList { fn special_clone(&self) -> Self { Self { callbacks: self.callbacks.clone(), @@ -207,7 +207,7 @@ impl CallbackList { } } -impl NodeData { +impl NodeData { /// Creates a new NodeData pub fn new(node_type: NodeType) -> Self { Self { @@ -234,14 +234,14 @@ impl NodeData { /// The document model, similar to HTML. This is a create-only structure, you don't actually read anything back #[derive(Clone, PartialEq, Eq)] -pub struct Dom { +pub struct Dom { pub(crate) arena: Rc>>>, pub(crate) root: NodeId, pub(crate) current_root: NodeId, pub(crate) last: NodeId, } -impl fmt::Debug for Dom { +impl fmt::Debug for Dom { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Dom {{ \ @@ -258,11 +258,11 @@ impl fmt::Debug for Dom { } #[derive(Clone, PartialEq, Eq)] -pub(crate) struct CallbackList { +pub(crate) struct CallbackList { pub(crate) callbacks: BTreeMap> } -impl Hash for CallbackList { +impl Hash for CallbackList { fn hash(&self, state: &mut H) { for callback in &self.callbacks { callback.hash(state); @@ -270,13 +270,13 @@ impl Hash for CallbackList { } } -impl fmt::Debug for CallbackList { +impl fmt::Debug for CallbackList { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "CallbackList (length: {:?})", self.callbacks.len()) } } -impl CallbackList { +impl CallbackList { pub fn new() -> Self { Self { callbacks: BTreeMap::new(), @@ -284,7 +284,7 @@ impl CallbackList { } } -impl Dom { +impl Dom { /// Creates an empty DOM #[inline] @@ -359,7 +359,7 @@ impl Dom { } } -impl Dom { +impl Dom { pub(crate) fn collect_callbacks( &self, diff --git a/src/lib.rs b/src/lib.rs index 28a9972b0..c388beffc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,7 +105,7 @@ pub mod prelude { pub use app_state::AppState; pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; - pub use traits::{LayoutScreen, ModifyAppState}; + pub use traits::{Layout, ModifyAppState}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, WindowMonitorTarget, RendererType, WindowEvent}; diff --git a/src/resources.rs b/src/resources.rs index 1f1b31de1..6b554e73f 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,4 +1,4 @@ -use traits::LayoutScreen; +use traits::Layout; use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey, FontInstanceKey}; use FastHashMap; @@ -24,7 +24,7 @@ use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; /// Images and fonts can be references across window contexts /// (not yet tested, but should work). #[derive(Clone)] -pub(crate) struct AppResources<'a, T: LayoutScreen> { +pub(crate) struct AppResources<'a, T: Layout> { /// Image cache pub(crate) images: FastHashMap, // Fonts are trickier to handle than images. @@ -42,7 +42,7 @@ pub(crate) struct AppResources<'a, T: LayoutScreen> { pub(crate) svg_registry: SvgRegistry, } -impl<'a, T: LayoutScreen> Default for AppResources<'a, T> { +impl<'a, T: Layout> Default for AppResources<'a, T> { fn default() -> Self { Self { svg_registry: SvgRegistry::default(), @@ -53,7 +53,7 @@ impl<'a, T: LayoutScreen> Default for AppResources<'a, T> { } } -impl<'a, T: LayoutScreen> AppResources<'a, T> { +impl<'a, T: Layout> AppResources<'a, T> { /// See `AppState::add_image()` pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) diff --git a/src/svg.rs b/src/svg.rs index 7bd950716..8022209cf 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -2,7 +2,7 @@ use std::io::Read; use lyon::path::default::Path; use webrender::api::ColorU; use dom::Callback; -use traits::LayoutScreen; +use traits::Layout; use std::sync::atomic::{Ordering, AtomicUsize}; use FastHashMap; use std::hash::{Hash, Hasher}; @@ -17,13 +17,13 @@ pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); pub struct SvgLayerId(usize); #[derive(Clone)] -pub(crate) struct SvgRegistry { +pub(crate) struct SvgRegistry { // note: one "layer" merely describes one or more polygons that have the same style layers: FastHashMap>, } -impl Default for SvgRegistry { +impl Default for SvgRegistry { fn default() -> Self { Self { layers: FastHashMap::default(), @@ -31,7 +31,7 @@ impl Default for SvgRegistry { } } -impl fmt::Debug for SvgRegistry { +impl fmt::Debug for SvgRegistry { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for layer in self.layers.keys() { write!(f, "{:?}", layer)?; @@ -40,7 +40,7 @@ impl fmt::Debug for SvgRegistry { } } -impl SvgRegistry { +impl SvgRegistry { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { let new_svg_id = SvgLayerId(unsafe { SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst) }); @@ -87,13 +87,13 @@ impl From for SvgParseError { } } -pub struct SvgLayer { +pub struct SvgLayer { pub data: SvgLayerType, pub callbacks: SvgCallbacks, pub style: SvgStyle, } -impl Clone for SvgLayer { +impl Clone for SvgLayer { fn clone(&self) -> Self { Self { data: self.data.clone(), @@ -103,7 +103,7 @@ impl Clone for SvgLayer { } } -pub enum SvgCallbacks { +pub enum SvgCallbacks { // No callbacks for this layer None, /// Call the callback on any of the items @@ -113,7 +113,7 @@ pub enum SvgCallbacks { Some(Vec<(usize, Callback)>), } -impl Clone for SvgCallbacks { +impl Clone for SvgCallbacks { fn clone(&self) -> Self { use self::SvgCallbacks::*; match self { @@ -124,7 +124,7 @@ impl Clone for SvgCallbacks { } } -impl Hash for SvgCallbacks { +impl Hash for SvgCallbacks { fn hash(&self, state: &mut H) { use self::SvgCallbacks::*; match self { @@ -141,13 +141,13 @@ impl Hash for SvgCallbacks { } } -impl PartialEq for SvgCallbacks { +impl PartialEq for SvgCallbacks { fn eq(&self, rhs: &Self) -> bool { self == rhs } } -impl Eq for SvgCallbacks { } +impl Eq for SvgCallbacks { } #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] pub struct SvgStyle { @@ -245,9 +245,9 @@ mod svg_to_lyon { use svg::{SvgCircle, SvgRect, SvgParseError, SvgLayer, SvgStyle}; use svg_crate::node::element::path::Parameters; use svg_crate::node::element::tag::Tag; - use traits::LayoutScreen; + use traits::Layout; - pub fn parse_from(svg_source: R) + pub fn parse_from(svg_source: R) -> Result>, SvgParseError> { use svg_crate::{read, parser::{Event, Error}}; diff --git a/src/task.rs b/src/task.rs index 4dd2ecfc7..ee8d214f5 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,7 +1,7 @@ //! Preliminary async IO / Task system use app_state::AppState; -use traits::LayoutScreen; +use traits::Layout; use std::sync::{Arc, Mutex, Weak}; use std::thread::{spawn, JoinHandle}; @@ -16,7 +16,7 @@ impl Task { app_state: &Arc>, callback: fn(Arc>, Arc<()>)) -> Self - where T: LayoutScreen + Send + 'static + where T: Layout + Send + 'static { let thread_check = Arc::new(()); let thread_weak = Arc::downgrade(&thread_check); diff --git a/src/traits.rs b/src/traits.rs index 8f63bdaf1..2bc1551d9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,7 +10,7 @@ use std::hash::Hash; use css_parser::{ParsedCssProperty, CssParsingError}; use std::sync::{Arc, Mutex}; -pub trait LayoutScreen { +pub trait Layout { /// Updates the DOM, must be provided by the final application. /// /// On each frame, a completely new DOM tree is generated. The final @@ -19,7 +19,7 @@ pub trait LayoutScreen { /// The `style_dom` looks through the given DOM rules, applies the style and /// recalculates the layout. This is done on each frame (except there are shortcuts /// when the DOM doesn't have to be recalculated). - fn get_dom(&self, window_id: WindowId) -> Dom where Self: Sized; + fn layout(&self, window_id: WindowId) -> Dom where Self: Sized; /// Applies the CSS styles to the nodes calculated from the `layout_screen` /// function and calculates the final display list that is submitted to the /// renderer. @@ -50,13 +50,13 @@ pub trait IntoParsedCssProperty<'a> { fn into_parsed_css_property(self) -> Result>; } -pub trait ModifyAppState { +pub trait ModifyAppState { /// Modifies the app state and then returns if the modification was successful /// Takes a FnMut that modifies the state fn modify(&self, closure: F) -> bool where F: FnMut(&mut T); } -impl ModifyAppState for Arc> { +impl ModifyAppState for Arc> { fn modify(&self, mut closure: F) -> bool where F: FnMut(&mut T) { match self.lock().as_mut() { Ok(lock) => { closure(&mut *lock); true }, @@ -138,7 +138,7 @@ impl<'a> ParsedCss<'a> { } } -fn match_dom_css_selectors<'a, T: LayoutScreen>( +fn match_dom_css_selectors<'a, T: Layout>( root: NodeId, arena: &Rc>>>, parsed_css: &ParsedCss<'a>, @@ -172,7 +172,7 @@ fn match_dom_css_selectors<'a, T: LayoutScreen>( } } -fn match_dom_css_selectors_inner<'a, T: LayoutScreen>( +fn match_dom_css_selectors_inner<'a, T: Layout>( root: NodeId, arena: &Arena>, parsed_css: &ParsedCss<'a>, @@ -202,7 +202,7 @@ fn match_dom_css_selectors_inner<'a, T: LayoutScreen>( /// Cascade the rules, put them into the list #[allow(unused_variables)] -fn cascade_constraints<'a, T: LayoutScreen>( +fn cascade_constraints<'a, T: Layout>( node: &NodeData, list: &mut CssConstraintList, parsed_css: &ParsedCss<'a>, diff --git a/src/ui_description.rs b/src/ui_description.rs index 8016543c3..199562010 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -1,7 +1,7 @@ use css_parser::ParsedCssProperty; use FastHashMap; use id_tree::{Arena, NodeId}; -use traits::LayoutScreen; +use traits::Layout; use ui_state::UiState; use css::Css; use dom::NodeData; @@ -10,7 +10,7 @@ use std::rc::Rc; use std::collections::BTreeMap; use css::CssDeclaration; -pub struct UiDescription { +pub struct UiDescription { pub(crate) ui_descr_arena: Rc>>>, /// ID of the root node of the arena (usually NodeId(0)) pub(crate) ui_descr_root: Option, @@ -26,7 +26,7 @@ pub struct UiDescription { pub(crate) dynamic_css_overrides: FastHashMap, } -impl Clone for UiDescription { +impl Clone for UiDescription { fn clone(&self) -> Self { Self { ui_descr_arena: self.ui_descr_arena.clone(), @@ -38,7 +38,7 @@ impl Clone for UiDescription { } } -impl Default for UiDescription { +impl Default for UiDescription { fn default() -> Self { Self { ui_descr_arena: Rc::new(RefCell::new(Arena::new())), @@ -50,7 +50,7 @@ impl Default for UiDescription { } } -impl UiDescription { +impl UiDescription { pub fn from_ui_state(ui_state: &UiState, style: &Css) -> Self { T::style_dom(&ui_state.dom, style) diff --git a/src/ui_state.rs b/src/ui_state.rs index 99916c88d..918d8a06e 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -1,17 +1,17 @@ -use traits::LayoutScreen; +use traits::Layout; use window::WindowId; use std::collections::BTreeMap; use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; use app_state::AppState; use std::fmt; -pub struct UiState { +pub struct UiState { pub dom: Dom, pub callback_list: BTreeMap>, pub node_ids_to_callbacks_list: BTreeMap>, } -impl fmt::Debug for UiState { +impl fmt::Debug for UiState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "UiState {{ \ @@ -25,7 +25,7 @@ impl fmt::Debug for UiState { } } -impl UiState { +impl UiState { pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId) -> Self { use dom::{Dom, On}; @@ -33,7 +33,7 @@ impl UiState { // Only shortly lock the data to get the dom out let dom: Dom = { let dom_lock = app_state.data.lock().unwrap(); - dom_lock.get_dom(window_id) + dom_lock.layout(window_id) }; unsafe { NODE_ID = 0 }; diff --git a/src/window.rs b/src/window.rs index 2ce0101e7..6c63dc5b0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -24,7 +24,7 @@ use cassowary::{ }; use display_list::SolvedLayout; -use traits::LayoutScreen; +use traits::Layout; use css::Css; use cache::{EditVariableCache, DomTreeCache}; use id_tree::NodeId; @@ -311,7 +311,7 @@ impl Default for WindowMonitorTarget { } /// Represents one graphical window to be rendered -pub struct Window { +pub struct Window { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, /// Current state of the window, stores the keyboard / mouse state, @@ -366,7 +366,7 @@ impl WindowDimensions { } /// Solver for solving the UI of the current window -pub(crate) struct UiSolver { +pub(crate) struct UiSolver { /// The actual solver pub(crate) solver: Solver, /// Dimensions of the root window @@ -380,7 +380,7 @@ pub(crate) struct UiSolver { pub(crate) dom_tree_cache: DomTreeCache, } -impl UiSolver { +impl UiSolver { pub(crate) fn query_bounds_of_rect(&self, rect_id: NodeId) { // TODO: After solving the UI, use this function to get the actual coordinates of an item in the UI. // This function should cache values accordingly @@ -398,7 +398,7 @@ pub(crate) struct WindowInternal { pub(crate) hidpi_factor: f32, } -impl Window { +impl Window { /// Creates a new window pub fn new(options: WindowCreateOptions, css: Css) -> Result { @@ -669,7 +669,7 @@ impl Window { } } -impl Drop for Window { +impl Drop for Window { fn drop(&mut self) { // self.background_thread.take().unwrap().join(); let renderer = self.renderer.take().unwrap(); From f15525371d53334ba0a1c0b0e1c72ea208f984c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 3 Jun 2018 00:54:18 +0200 Subject: [PATCH 081/868] Add question.md as a template for questions --- .github/ISSUE_TEMPLATE/question.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..da52d283b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,6 @@ +--- +name: Question +about: Ask a question / problem when using azul + +--- + From ad172e09f3f52a904e7a2c58e24339b4a054e53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 3 Jun 2018 18:32:56 +0200 Subject: [PATCH 082/868] Refacored the event loop into smaller functions --- examples/debug.rs | 2 +- src/app.rs | 308 ++++++++++++++++++++++++++-------------------- src/window.rs | 18 ++- 3 files changed, 192 insertions(+), 136 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7304cbd0a..125950f5e 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -58,5 +58,5 @@ fn main() { // TODO: Multi-window apps currently crash // Need to re-factor the event loop for that app.create_window(WindowCreateOptions::default(), css).unwrap(); - app.run(); + app.run().unwrap(); } diff --git a/src/app.rs b/src/app.rs index b2763f8b1..8769e44b1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,7 @@ use traits::Layout; use ui_state::UiState; use ui_description::UiDescription; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, PoisonError}; use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; use glium::glutin::Event; use euclid::TypedScale; @@ -16,6 +16,8 @@ use images::{ImageType}; use image::ImageError; use font::FontError; use webrender::api::{RenderApi, HitTestFlags}; +use glium::SwapBuffersError; +use std::fmt; /// Graphical application that maintains some kind of application state pub struct App<'a, T: Layout> { @@ -25,6 +27,35 @@ pub struct App<'a, T: Layout> { pub app_state: AppState<'a, T>, } +/// Error returned by the `.run()` function +/// +/// If the `.run()` function would panic, that would need `T` to +/// implement `Debug`, which is not necessary if we just return an error. +pub enum RuntimeError { + // Could not swap the display (drawing error) + GlSwapError(SwapBuffersError), + ArcUnlockError, + MutexPoisonError(PoisonError), +} + +impl From> for RuntimeError { + fn from(e: PoisonError) -> Self { + RuntimeError::MutexPoisonError(e) + } +} + +impl From for RuntimeError { + fn from(e: SwapBuffersError) -> Self { + RuntimeError::GlSwapError(e) + } +} + +impl fmt::Debug for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + pub(crate) struct FrameEventInfo { pub(crate) should_redraw_window: bool, pub(crate) should_swap_window: bool, @@ -73,45 +104,45 @@ impl<'a, T: Layout> App<'a, T> { /// This is the "main app loop", "main game loop" or whatever you want to call it. /// Usually this is the last function you call in your `main()` function, since exiting /// it means that the user has closed all windows and wants to close the app. - pub fn run(&mut self) + /// + /// When all windows are closed, this function returns the internal data again. + /// This is useful for ex. CLI application that run procedurally, but then want to + /// open a window temporarily, to ask for user input in a "nicer" way than a pure + /// CLI-way. + /// + /// This way you can do this: + /// + /// ```no_run,ignore + /// let app = App::new(MyData { username: None, password: None }); + /// app.create_window(WindowCreateOptions::default(), Css::native()); + /// + /// // pop open a window that asks the user for his username and password... + /// let MyData { username, password } = app.run(); + /// + /// // continue the rest of the program here... + /// println!("username: {:?}, password: {:?}", username, password); + /// ``` + pub fn run(mut self) -> Result> { - let mut ui_state_cache = Vec::with_capacity(self.windows.len()); - let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - - // first redraw, initialize cache - { - for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&self.app_state, WindowId { id: idx })); - } + self.run_inner()?; + let unique_arc = Arc::try_unwrap(self.app_state.data).map_err(|_| RuntimeError::ArcUnlockError)?; + unique_arc.into_inner().map_err(|e| e.into()) + } - // First repaint, otherwise the window would be black on startup - for (idx, window) in self.windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, - &ui_description_cache[idx], - &mut self.app_state.resources, - true); - window.display.swap_buffers().unwrap(); - } - } + fn run_inner(&mut self) -> Result<(), RuntimeError> { + use std::{thread, time::{Duration, Instant}}; - 'render_loop: loop { + let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); + let mut ui_description_cache = Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); - use webrender::api::{DeviceUintSize, WorldPoint, DeviceUintPoint, - DeviceUintRect, LayoutSize, Transaction}; - use dom::UpdateScreen; + while !self.windows.is_empty() { + let time_start = Instant::now(); let mut closed_windows = Vec::::new(); - let time_start = ::std::time::Instant::now(); - let mut debug_has_repainted = None; - - // TODO: Use threads on a per-window basis. - // Currently, events in one window will block all others for (idx, ref mut window) in self.windows.iter_mut().enumerate() { - let current_window_id = WindowId { id: idx }; - + let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); window.events_loop.poll_events(|event| { @@ -121,133 +152,142 @@ impl<'a, T: Layout> App<'a, T> { } }); - // update the state if frame_event_info.should_swap_window { - window.display.swap_buffers().unwrap(); + window.display.swap_buffers()?; } if frame_event_info.should_hittest { - - let cursor_x = frame_event_info.cur_cursor_pos.0 as f32; - let cursor_y = frame_event_info.cur_cursor_pos.1 as f32; - let point = WorldPoint::new(cursor_x, cursor_y); - let hit_test_results = window.internal.api.hit_test( - window.internal.document_id, - Some(window.internal.pipeline_id), - point, - HitTestFlags::FIND_ALL); - - let mut should_update_screen = UpdateScreen::DontRedraw; - - for item in hit_test_results.items { - let callback_list_opt = ui_state_cache[idx].node_ids_to_callbacks_list.get(&item.tag.0); - if let Some(callback_list) = callback_list_opt { - use window::WindowEvent; - // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) - // Currently, this just invoke all actions - let window_event = WindowEvent { - window: idx, - // TODO: currently we don't have information about what DOM node was hit - number_of_previous_siblings: None, - cursor_relative_to_item: (item.point_in_viewport.x, item.point_in_viewport.y), - cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), - }; - - for callback_id in callback_list.values() { - let update = (ui_state_cache[idx].callback_list[callback_id].0)(&mut self.app_state, window_event); - if update == UpdateScreen::Redraw { - should_update_screen = UpdateScreen::Redraw; - } - } - } - } - - if should_update_screen == UpdateScreen::Redraw { - frame_event_info.should_redraw_window = true; - // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! - // Copy the current fake CSS changes to the real CSS, then clear the fake CSS again - // TODO: .clone() and .clear() can be one operation - window.css.dynamic_css_overrides = self.app_state.windows[idx].css.dynamic_css_overrides.clone(); - self.app_state.windows[idx].css.clear(); - } + Self::do_hit_test_and_call_callbacks(window, window_id, &mut frame_event_info, &ui_state_cache, &mut self.app_state); } ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowId { id: idx }); - // Macro to avoid duplication between the new_window_size and the new_dpi_factor event - // TODO: refactor this into proper functions (when the WindowState is working) - macro_rules! update_display { - () => ( - let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); - - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); - window.internal.api.send_transaction(window.internal.document_id, txn); - render(window, - ¤t_window_id, - &ui_description_cache[idx], - &mut self.app_state.resources, - true); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); - ) - } - - if let Some((w, h)) = frame_event_info.new_window_size { - window.internal.layout_size = LayoutSize::new(w as f32, h as f32); - window.internal.framebuffer_size = DeviceUintSize::new(w, h); - update_display!(); - continue; - } - - if let Some(dpi) = frame_event_info.new_dpi_factor { - window.internal.hidpi_factor = dpi; - update_display!(); - continue; - } + // Update the window state that we got from the frame event (updates window dimensions and DPI) + window.update_from_external_window_state(&mut frame_event_info); + // Update the window state every frame that was set by the user + window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); if frame_event_info.should_redraw_window { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, - ¤t_window_id, - &ui_description_cache[idx], - &mut self.app_state.resources, - frame_event_info.new_window_size.is_some()); - - let time_end = ::std::time::Instant::now(); - debug_has_repainted = Some(time_end - time_start); + Self::update_display(&window); + render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); } - - // Update the window state every frame, no matter if the window has gotten an event or not - window.update_window_state(self.app_state.windows[idx].state.clone()); } - // close windows if necessary - for closed_window_id in closed_windows { - let closed_window_id = closed_window_id; + // Close windows if necessary + closed_windows.into_iter().for_each(|closed_window_id| { ui_state_cache.remove(closed_window_id); ui_description_cache.remove(closed_window_id); self.windows.remove(closed_window_id); + }); + + // Run deamons and remove them from the even queue if they are finished + self.app_state.run_all_deamons(); + + // Clean up finished tasks, remove them if possible + self.app_state.clean_up_finished_tasks(); + + // Wait until 16ms have passed + let time_end = Instant::now(); + let diff = time_end - time_start; + if diff < Duration::from_millis(16) { + thread::sleep(diff); } + } + + Ok(()) + } + + fn update_display(window: &Window) + { + use webrender::api::{Transaction, DeviceUintRect, DeviceUintPoint}; + + let mut txn = Transaction::new(); + let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); + + txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); + window.internal.api.send_transaction(window.internal.document_id, txn); + } - if self.windows.is_empty() { - break; - } else { - if let Some(restate_time) = debug_has_repainted { - println!("frame time: {:?} ms", restate_time.subsec_nanos() as f32 / 1_000_000.0); + fn do_hit_test_and_call_callbacks( + window: &mut Window, + window_id: WindowId, + info: &mut FrameEventInfo, + ui_state_cache: &[UiState], + app_state: &mut AppState) + { + use dom::UpdateScreen; + use webrender::api::WorldPoint; + + let cursor_x = info.cur_cursor_pos.0 as f32; + let cursor_y = info.cur_cursor_pos.1 as f32; + let point = WorldPoint::new(cursor_x, cursor_y); + let hit_test_results = window.internal.api.hit_test( + window.internal.document_id, + Some(window.internal.pipeline_id), + point, + HitTestFlags::FIND_ALL); + + let mut should_update_screen = UpdateScreen::DontRedraw; + + for item in hit_test_results.items { + let callback_list_opt = ui_state_cache[window_id.id].node_ids_to_callbacks_list.get(&item.tag.0); + if let Some(callback_list) = callback_list_opt { + use window::WindowEvent; + // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) + // Currently, this just invoke all actions + let window_event = WindowEvent { + window: window_id.id, + // TODO: currently we don't have information about what DOM node was hit + number_of_previous_siblings: None, + cursor_relative_to_item: (item.point_in_viewport.x, item.point_in_viewport.y), + cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), + }; + + for callback_id in callback_list.values() { + let update = (ui_state_cache[window_id.id].callback_list[callback_id].0)(app_state, window_event); + if update == UpdateScreen::Redraw { + should_update_screen = UpdateScreen::Redraw; + } } - ::std::thread::sleep(::std::time::Duration::from_millis(16)); } + } - // Run deamons and remove them from - if self.app_state.run_all_deamons() == UpdateScreen::Redraw { - // TODO: What to do? - } + if should_update_screen == UpdateScreen::Redraw { + info.should_redraw_window = true; + // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! + // Copy the current fake CSS changes to the real CSS, then clear the fake CSS again + // TODO: .clone() and .clear() can be one operation + window.css.dynamic_css_overrides = app_state.windows[window_id.id].css.dynamic_css_overrides.clone(); + // clear the dynamic CSS overrides + app_state.windows[window_id.id].css.clear(); + } + } - // Clean up finished tasks - self.app_state.clean_up_finished_tasks(); + fn initialize_ui_state(windows: &[Window], app_state: &AppState<'a, T>) + -> Vec> + { + windows.iter().enumerate().map(|(idx, _)| + UiState::from_app_state(app_state, WindowId { id: idx }) + ).collect() + } + + /// First repaint, otherwise the window would be black on startup + fn do_first_redraw( + windows: &mut [Window], + app_state: &mut AppState<'a, T>, + ui_state_cache: &[UiState]) + -> Vec> + { + let mut ui_description_cache = vec![UiDescription::default(); windows.len()]; + + for (idx, window) in windows.iter_mut().enumerate() { + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &mut app_state.resources, true); + window.display.swap_buffers().unwrap(); } + + ui_description_cache } /// Add an image to the internal resources diff --git a/src/window.rs b/src/window.rs index 6c63dc5b0..b9cb94d4b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -29,6 +29,7 @@ use css::Css; use cache::{EditVariableCache, DomTreeCache}; use id_tree::NodeId; use compositor::Compositor; +use app::FrameEventInfo; /// azul-internal ID for a window #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] @@ -617,7 +618,7 @@ impl Window { /// `window.position` has no effect on the platform window, since they are very /// frequently modified by the user (other properties are always set by the /// application developer) - pub fn update_window_state(&mut self, new_state: WindowState) { + pub(crate) fn update_from_user_window_state(&mut self, new_state: WindowState) { let gl_window = self.display.gl_window(); let window = gl_window.window(); @@ -667,6 +668,21 @@ impl Window { *old_state = new_state; } + + pub(crate) fn update_from_external_window_state(&mut self, frame_event_info: &mut FrameEventInfo) { + use webrender::api::{DeviceUintSize, WorldPoint, LayoutSize}; + + if let Some((w, h)) = frame_event_info.new_window_size { + self.internal.layout_size = LayoutSize::new(w as f32, h as f32); + self.internal.framebuffer_size = DeviceUintSize::new(w, h); + frame_event_info.should_redraw_window = true; + } + + if let Some(dpi) = frame_event_info.new_dpi_factor { + self.internal.hidpi_factor = dpi; + frame_event_info.should_redraw_window = true; + } + } } impl Drop for Window { From a4048a121049cadd8dcc938fff9d5aa251c3d48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 4 Jun 2018 02:43:46 +0200 Subject: [PATCH 083/868] Added button, text cache + SVG shader. The display currently only draws a black screen, need to investigate why that is. --- examples/debug.rs | 25 +++++++---- src/app.rs | 29 +++++++++++-- src/app_state.rs | 15 +++++++ src/compositor.rs | 1 + src/display_list.rs | 6 ++- src/dom.rs | 54 ++++++++++++++++++++---- src/images.rs | 15 +++++++ src/lib.rs | 22 ++++++---- src/resources.rs | 21 +++++++++- src/svg.rs | 100 +++++++++++++++++++++++++++++++++++++++++++- src/text_cache.rs | 38 +++++++++++++++++ src/traits.rs | 4 ++ src/widgets.rs | 45 ++++++++++++++++++++ 13 files changed, 342 insertions(+), 33 deletions(-) create mode 100644 src/text_cache.rs create mode 100644 src/widgets.rs diff --git a/examples/debug.rs b/examples/debug.rs index 125950f5e..c6f2587f3 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,6 +1,7 @@ extern crate azul; use azul::prelude::*; +use azul::widgets::*; const TEST_CSS: &str = include_str!("test_content.css"); const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); @@ -16,16 +17,21 @@ pub struct MyAppData { } impl Layout for MyAppData { - fn layout(&self, _window_id: WindowId) -> Dom { - let mut dom = Dom::new(NodeType::Label(format!("Load SVG file"))) - .with_class("__azul-native-button") - .with_event(On::MouseUp, Callback(my_button_click_handler)); - - for polygon in &self.my_svg_ids { - // Draw the cached polygon by its ID - dom.add_sibling(Dom::new(NodeType::SvgLayer(*polygon))); - } + fn layout(&self, _window_id: WindowId) + -> Dom + { + let mut dom = Dom::new(NodeType::Div); + dom.add_child( + Button::with_label("Load SVG file").dom() + .with_event(On::MouseUp, Callback(my_button_click_handler))); +/* + if !self.my_svg_ids.is_empty() { + dom.add_sibling( + Svg::new(self.my_svg_ids.clone()) + .dom()) + } +*/ dom } } @@ -33,6 +39,7 @@ impl Layout for MyAppData { fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { // Load and parse the SVG file, register polygon data as IDs let mut svg_ids = app_state.add_svg(TEST_SVG).unwrap(); + println!("adding svg ids: {:?}", svg_ids); app_state.data.modify(|data| data.my_svg_ids.append(&mut svg_ids)); UpdateScreen::Redraw } diff --git a/src/app.rs b/src/app.rs index 8769e44b1..687d73449 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use text_cache::TextId; use dom::UpdateScreen; use window::FakeWindow; use css::{Css, FakeCss}; @@ -142,6 +143,10 @@ impl<'a, T: Layout> App<'a, T> { for (idx, ref mut window) in self.windows.iter_mut().enumerate() { + // TODO: move this somewhere else + let svg_shader = &self.app_state.resources.svg_registry.init_shader(&window.display); + println!("shader: {:?}", svg_shader); + let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); @@ -166,12 +171,14 @@ impl<'a, T: Layout> App<'a, T> { window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); - +/* if frame_event_info.should_redraw_window { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - Self::update_display(&window); - render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); + } +*/ + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + Self::update_display(&window); + render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); } // Close windows if necessary @@ -405,6 +412,20 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.delete_deamon(id) } + pub fn add_text>(&mut self, text: S) + -> TextId + { + self.app_state.add_text(text) + } + + pub fn delete_text(&mut self, id: TextId) { + self.app_state.delete_text(id); + } + + pub fn clear_all_texts(&mut self) { + self.app_state.clear_all_texts(); + } + /// Mock rendering function, for creating a hidden window and rendering one frame /// Used in unit tests. You **have** to enable software rendering, otherwise, /// this function won't work in a headless environment. diff --git a/src/app_state.rs b/src/app_state.rs index 5f5d79b9f..d32db3b9f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use text_cache::TextId; use window::FakeWindow; use window_state::WindowState; use task::Task; @@ -239,6 +240,20 @@ impl<'a, T: Layout> AppState<'a, T> { { self.tasks.retain(|x| x.is_finished()); } + + pub fn add_text>(&mut self, text: S) + -> TextId + { + self.resources.add_text(text) + } + + pub fn delete_text(&mut self, id: TextId) { + self.resources.delete_text(id); + } + + pub fn clear_all_texts(&mut self) { + self.resources.clear_all_texts(); + } } impl<'a, T: Layout + Send + 'static> AppState<'a, T> { diff --git a/src/compositor.rs b/src/compositor.rs index 1a580bf3a..e665bc8e2 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -118,6 +118,7 @@ pub const VERTEXBUFFER_FOR_FULL_SCREEN_QUAD: [SimpleGpuVertex;3] = [ ]; impl Compositor { + pub fn new() -> Self { Self::default() } diff --git a/src/display_list.rs b/src/display_list.rs index 166b8340d..5e2e4b420 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -377,10 +377,14 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { #[inline] fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { + const TRANSPARENT: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; + let background = style.background_color.and_then(|col| Some(col.0)).unwrap_or(TRANSPARENT); + builder.push_rect(&info, background.into()); + /* match style.background_color { Some(bg) => builder.push_rect(&info, bg.0.into()), None => builder.push_clear_rect(&info), - } + }*/ } #[inline] diff --git a/src/dom.rs b/src/dom.rs index 9aa847563..66515f8a1 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -12,6 +12,9 @@ use std::hash::{Hash, Hasher}; use webrender::api::ColorU; use glium::Texture2d; use svg::SvgLayerId; +use images::ImageId; +use cache::DomHash; +use text_cache::TextId; /// This is only accessed from the main thread, so it's safe to use pub(crate) static mut NODE_ID: u64 = 0; @@ -78,21 +81,58 @@ impl Copy for Callback { } /// `Div` is shaped like a circle (for `Ul`). #[derive(Debug, Clone, PartialEq, Hash, Eq)] pub enum NodeType { - /// Regular div + /// Regular div with no particular type of data attached Div, - /// A label that can be (optionally) be selectable with the mouse + /// A small label that can be (optionally) be selectable with the mouse Label(String), - SvgLayer(SvgLayerId), - // GlTexture + /// Larger amount of text, that has to be cached + Text(TextId), + /// An image that is rendered by webrender. The id is aquired by the + /// `AppState::add_image()` function + Image(ImageId), + /// OpenGL texture. The `Svg` widget deserizalizes itself into a texture + /// Equality and Hash values are only checked by the OpenGl texture ID, + /// azul does not check that the contents of two textures are the same + GlTexture(Texture), +} + +#[derive(Debug, Clone)] +pub struct Texture { + inner: Rc, +} + +impl Texture { + fn new(tex: Texture2d) -> Self { + Self { + inner: Rc::new(tex), + } + } +} + +impl Hash for Texture { + fn hash(&self, state: &mut H) { + use glium::GlObject; + self.inner.get_id().hash(state); + } } +impl PartialEq for Texture { + fn eq(&self, other: &Texture) -> bool { + use glium::GlObject; + self.inner.get_id() == other.inner.get_id() + } +} + +impl Eq for Texture { } + impl GetCssId for NodeType { fn get_css_id(&self) -> &'static str { use self::NodeType::*; match *self { Div => "div", - Label(_) => "p", - SvgLayer(_) => "svg", + Label(_) | Text(_) => "p", + Image(_) => "image", + GlTexture(_) => "texture", } } } @@ -157,8 +197,6 @@ impl Hash for NodeData { } } -use cache::DomHash; - impl NodeData { pub fn calculate_node_data_hash(&self) -> DomHash { use std::hash::Hash; diff --git a/src/images.rs b/src/images.rs index b585c20c3..8ecd6dd3d 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,10 +1,25 @@ //! Module for loading and handling images +use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::ImageFormat as WebrenderImageFormat; use image::{ImageResult, ImageFormat, guess_format}; use image::{self, ImageError, DynamicImage, GenericImage}; use webrender::api::{ImageData, ImageDescriptor, ImageKey}; +static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ImageId { + id: usize, +} + +pub(crate) fn new_image_id() -> ImageId { + let unique_id =IMAGE_ID_COUNTER.fetch_add(1, Ordering::SeqCst); + ImageId { + id: unique_id, + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ImageType { Bmp, diff --git a/src/lib.rs b/src/lib.rs index c388beffc..d234791c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,14 +46,6 @@ extern crate app_units; extern crate unicode_normalization; extern crate harfbuzz_rs; -/// Global application (Initialization starts here) -mod app; -/// Wrapper for the application data & application state -mod app_state; -/// Styling & CSS parsing -mod css; -/// Text layout -mod text_layout; /// DOM / HTML node handling pub mod dom; /// The layout traits for creating a layout-able application @@ -64,6 +56,16 @@ pub mod window; pub mod task; /// SVG / path flattering module (lyon) pub mod svg; +/// Built-in widgets +pub mod widgets; +/// Global application (Initialization starts here) +mod app; +/// Wrapper for the application data & application state +mod app_state; +/// Styling & CSS parsing +mod css; +/// Text layout +mod text_layout; /// Font & image resource handling, lookup and caching mod resources; /// UI Description & display list handling (webrender) @@ -94,6 +96,8 @@ mod menu; mod compositor; /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) mod platform_ext; +/// Module for caching long texts (including their layout / character positions) across multiple frames +mod text_cache; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; @@ -105,7 +109,7 @@ pub mod prelude { pub use app_state::AppState; pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; - pub use traits::{Layout, ModifyAppState}; + pub use traits::{Layout, ModifyAppState, GetDom}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, WindowMonitorTarget, RendererType, WindowEvent}; diff --git a/src/resources.rs b/src/resources.rs index 6b554e73f..2875d6add 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,3 +1,4 @@ +use text_cache::TextRegistry; use traits::Layout; use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey, FontInstanceKey}; @@ -12,6 +13,7 @@ use app_units::Au; use css_parser; use css_parser::Font::ExternalFont; use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; +use text_cache::TextId; /// Font and image keys /// @@ -40,6 +42,8 @@ pub(crate) struct AppResources<'a, T: Layout> { /// Stores the polygon data for all SVGs. Polygons can be shared across windows /// without duplicating the data. This doesn't store any rendering-related data, only the polygons pub(crate) svg_registry: SvgRegistry, + /// Stores long texts across frames + pub(crate) text_registry: TextRegistry, } impl<'a, T: Layout> Default for AppResources<'a, T> { @@ -49,6 +53,7 @@ impl<'a, T: Layout> Default for AppResources<'a, T> { fonts: FastHashMap::default(), font_data: FastHashMap::default(), images: FastHashMap::default(), + text_registry: TextRegistry::default(), } } } @@ -166,10 +171,24 @@ impl<'a, T: Layout> AppResources<'a, T> { self.svg_registry.clear_all_layers(); } + pub(crate) fn add_text>(&mut self, text: S) + -> TextId + { + self.text_registry.add_text(text) + } + + pub(crate) fn delete_text(&mut self, id: TextId) { + self.text_registry.delete_text(id); + } + + pub(crate) fn clear_all_texts(&mut self) { + self.text_registry.clear_all_texts(); + } + /// Parses an input source, parses the SVG, adds the shapes as layers into /// the registry, returns the IDs of the added shapes, in the order that /// they appeared in the SVG text. - pub fn add_svg(&mut self, input: R) + pub(crate) fn add_svg(&mut self, input: R) -> Result, SvgParseError> { self.svg_registry.add_svg(input) diff --git a/src/svg.rs b/src/svg.rs index 8022209cf..c64d01ce8 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,3 +1,11 @@ +use std::rc::Rc; +use glium::DrawParameters; +use glium::IndexBuffer; +use glium::VertexBuffer; +use glium::Display; +use glium::Texture2d; +use glium::Program; +use webrender::api::ColorF; use std::io::Read; use lyon::path::default::Path; use webrender::api::ColorU; @@ -9,28 +17,75 @@ use std::hash::{Hash, Hasher}; use svg_crate::parser::Error as SvgError; use std::io::Error as IoError; use std::fmt; +use euclid::TypedRect; /// In order to store / compare SVG files, we have to pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); +const SVG_VERTEX_SHADER: &str = " + #version 130 + + in vec2 xy; + + uniform vec2 bbox_origin; + uniform vec2 bbox_size; + uniform float z_index; + + void main() { + gl_Position = vec4(vec2(-1.0) + ((xy - bbox_origin) / bbox_size), z_index, 1.0); + }"; + +const SVG_FRAGMENT_SHADER: &str = " + #version 130 + uniform vec4 color; + + void main() { + gl_FragColor = color; + } +"; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct SvgLayerId(usize); +#[derive(Debug, Clone)] +pub struct SvgShader { + program: Rc, +} + +impl SvgShader { + pub fn new(display: &Display) -> Self { + Self { + program: Rc::new(Program::from_source(display, SVG_VERTEX_SHADER, SVG_FRAGMENT_SHADER, None).unwrap()), + } + } +} + #[derive(Clone)] pub(crate) struct SvgRegistry { // note: one "layer" merely describes one or more polygons that have the same style layers: FastHashMap>, + shader: Option, } - impl Default for SvgRegistry { fn default() -> Self { Self { layers: FastHashMap::default(), + shader: None, } } } +impl SvgRegistry { + /// Builds and compiles the shader if the shader isn't already present + pub(crate) fn init_shader(&mut self, display: &Display) -> SvgShader { + if self.shader.is_none() { + self.shader = Some(SvgShader::new(display)); + } + self.shader.as_ref().and_then(|s| Some(s.clone())).unwrap() + } +} + impl fmt::Debug for SvgRegistry { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for layer in self.layers.keys() { @@ -103,6 +158,49 @@ impl Clone for SvgLayer { } } +// TODO: This must be implementable as a texture! + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Svg { + pub layers: Vec, +} + +#[derive(Debug, Copy, Clone)] +pub struct SvgWorldPixel; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct SvgVert { + pub(crate) xy: (f32, f32), +} + +implement_vertex!(SvgVert, xy); + +pub(crate) fn draw_polygons( + target: &mut Texture2d, + shader: &SvgShader, + color: &ColorF, + bbox: &TypedRect, + vbuf: &VertexBuffer, + ibuf: &IndexBuffer, + z_index: f32) +{ + use glium::Surface; + + let draw_options = DrawParameters { + primitive_restart_index: true, + .. Default::default() + }; + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: (color.r, color.g, color.b, color.a), + }; + + target.as_surface().draw(vbuf, ibuf, &*shader.program, &uniforms, &draw_options).unwrap(); +} + pub enum SvgCallbacks { // No callbacks for this layer None, diff --git a/src/text_cache.rs b/src/text_cache.rs new file mode 100644 index 000000000..22a62d215 --- /dev/null +++ b/src/text_cache.rs @@ -0,0 +1,38 @@ +use FastHashMap; +use std::sync::atomic::{Ordering, AtomicUsize}; + +static TEXT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +fn new_text_id() -> TextId { + let unique_id = TEXT_ID_COUNTER.fetch_add(1, Ordering::SeqCst); + TextId { + inner: unique_id + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct TextId { + inner: usize, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TextRegistry { + inner: FastHashMap +} + +impl TextRegistry { + + pub(crate) fn add_text>(&mut self, text: S) -> TextId { + let id = new_text_id(); + self.inner.insert(id, text.into()); + id + } + + pub(crate) fn delete_text(&mut self, id: TextId) { + self.inner.remove(&id); + } + + pub(crate) fn clear_all_texts(&mut self) { + self.inner.clear(); + } +} \ No newline at end of file diff --git a/src/traits.rs b/src/traits.rs index 2bc1551d9..d1053553c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,6 +10,10 @@ use std::hash::Hash; use css_parser::{ParsedCssProperty, CssParsingError}; use std::sync::{Arc, Mutex}; +pub trait GetDom { + fn dom(self) -> Dom; +} + pub trait Layout { /// Updates the DOM, must be provided by the final application. /// diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 000000000..28f216827 --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,45 @@ +#![allow(non_snake_case)] + +use traits::GetDom; +use traits::Layout; +use dom::{Dom, NodeType}; +use images::ImageId; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Button { + pub content: ButtonContent, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum ButtonContent { + Image(ImageId), + // Buttons should only contain short amounts of text + Text(String), +} + +impl Button { + pub fn with_label>(text: S) -> Self { + Self { + content: ButtonContent::Text(text.into()), + } + } + + pub fn with_image(image: ImageId) -> Self { + Self { + content: ButtonContent::Image(image), + } + } +} + +impl GetDom for Button { + fn dom(self) -> Dom { + use self::ButtonContent::*; + + let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); + match self.content { + Image(i) => button_root.add_child(Dom::new(NodeType::Image(i))), + Text(s) => button_root.add_child(Dom::new(NodeType::Label(s))) + } + button_root + } +} \ No newline at end of file From 343912c4ac0e44753db9350a11f463d571f5481d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 4 Jun 2018 05:25:55 +0200 Subject: [PATCH 084/868] Prepared ExternalImageHandler for compositor Removed immediate panic when creating multiple windows (by using `window.set_current()`), however, the example segfaults if the windows are closed in random order. --- examples/debug.rs | 11 ++++--- src/app.rs | 37 ++++++++++++++++------- src/compositor.rs | 75 +++++++++++++++++++++++++++++------------------ src/lib.rs | 2 +- src/svg.rs | 7 ----- src/traits.rs | 4 +-- src/ui_state.rs | 6 ++-- src/widgets.rs | 20 +++++++++++++ src/window.rs | 63 ++++++++++++++++++++++++++++++++------- 9 files changed, 157 insertions(+), 68 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index c6f2587f3..34293936d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -17,7 +17,7 @@ pub struct MyAppData { } impl Layout for MyAppData { - fn layout(&self, _window_id: WindowId) + fn layout(&self, info: WindowInfo) -> Dom { let mut dom = Dom::new(NodeType::Div); @@ -25,13 +25,13 @@ impl Layout for MyAppData { dom.add_child( Button::with_label("Load SVG file").dom() .with_event(On::MouseUp, Callback(my_button_click_handler))); -/* + /* if !self.my_svg_ids.is_empty() { dom.add_sibling( Svg::new(self.my_svg_ids.clone()) - .dom()) + .dom(&info.window)) } -*/ + */ dom } } @@ -62,8 +62,7 @@ fn main() { app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); // app.delete_image("Cat01"); - // TODO: Multi-window apps currently crash - // Need to re-factor the event loop for that + app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } diff --git a/src/app.rs b/src/app.rs index 687d73449..edbe6f997 100644 --- a/src/app.rs +++ b/src/app.rs @@ -96,6 +96,7 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.windows.push(FakeWindow { state: window.state.clone(), css: FakeCss::default(), + read_only_window: window.display.clone(), }); self.windows.push(window); Ok(()) @@ -132,6 +133,7 @@ impl<'a, T: Layout> App<'a, T> { fn run_inner(&mut self) -> Result<(), RuntimeError> { use std::{thread, time::{Duration, Instant}}; + use window::{ReadOnlyWindow, WindowInfo}; let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); let mut ui_description_cache = Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); @@ -143,9 +145,13 @@ impl<'a, T: Layout> App<'a, T> { for (idx, ref mut window) in self.windows.iter_mut().enumerate() { + unsafe { + use glium::glutin::GlContext; + window.display.gl_window().make_current().unwrap(); + } + // TODO: move this somewhere else let svg_shader = &self.app_state.resources.svg_registry.init_shader(&window.display); - println!("shader: {:?}", svg_shader); let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); @@ -165,20 +171,24 @@ impl<'a, T: Layout> App<'a, T> { Self::do_hit_test_and_call_callbacks(window, window_id, &mut frame_event_info, &ui_state_cache, &mut self.app_state); } - ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowId { id: idx }); + ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { + window_id: WindowId { id: idx }, + window: ReadOnlyWindow { + inner: window.display.clone(), + } + }); // Update the window state that we got from the frame event (updates window dimensions and DPI) window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); -/* - if frame_event_info.should_redraw_window { + if frame_event_info.should_redraw_window { + println!("updating window!"); + ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + Self::update_display(&window); + render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); } -*/ - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - Self::update_display(&window); - render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); } // Close windows if necessary @@ -274,8 +284,15 @@ impl<'a, T: Layout> App<'a, T> { fn initialize_ui_state(windows: &[Window], app_state: &AppState<'a, T>) -> Vec> { - windows.iter().enumerate().map(|(idx, _)| - UiState::from_app_state(app_state, WindowId { id: idx }) + use window::{ReadOnlyWindow, WindowInfo}; + + windows.iter().enumerate().map(|(idx, w)| + UiState::from_app_state(app_state, WindowInfo { + window_id: WindowId { id: idx }, + window: ReadOnlyWindow { + inner: w.display.clone(), + } + }) ).collect() } diff --git a/src/compositor.rs b/src/compositor.rs index e665bc8e2..cf84116f3 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -2,7 +2,10 @@ //! This makes it possible to use OpenGL images in the background and compose SVG elements //! into the UI. -use std::sync::{Arc, Mutex}; +use FastHashMap; +use webrender::{ExternalImageHandler, ExternalImageSource}; +use webrender::api::{ExternalImageId, TexelRect, DevicePixel}; +use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicBool}}; use glium::{ Program, VertexBuffer, Display, @@ -11,17 +14,13 @@ use glium::{ backend::Facade, }; use webrender::ExternalImage; - -#[derive(Default, Debug)] -pub struct Compositor { - textures: Vec, -} +use euclid::TypedPoint2D; // I'd wrap this in a `Arc>`, but this is only available on nightly // So, for now, this is completely thread-unsafe // -// However, this should be fine, as we initialize the program only from the main thread -// and never de-initialize it +// However, this should be fine, as we initialize the program only from the main +// thread and never de-initialize it static mut SHADER_FULL_SCREEN: Option = None; pub const INDICES_NO_INDICES_TRIANGLE_STRIP: NoIndices = NoIndices(TriangleStrip); @@ -117,12 +116,48 @@ pub const VERTEXBUFFER_FOR_FULL_SCREEN_QUAD: [SimpleGpuVertex;3] = [ }, ]; +#[derive(Debug)] +pub struct Compositor { + textures: FastHashMap, + locked: AtomicBool, +} + +impl Default for Compositor { + fn default() -> Self { + Self { + textures: FastHashMap::default(), + locked: AtomicBool::new(false), + } + } +} + +impl ExternalImageHandler for Compositor { + fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> ExternalImage { + use glium::GlObject; + + let texture = &self.textures[&key]; + self.locked.compare_and_swap(false, true, Ordering::SeqCst); + + ExternalImage { + uv: TexelRect { + uv0: TypedPoint2D::zero(), + uv1: TypedPoint2D::::new(texture.width() as f32, texture.height() as f32), + }, + source: ExternalImageSource::NativeTexture(texture.get_id()), + } + } + + fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { + self.locked.compare_and_swap(true, false, Ordering::SeqCst); + } +} + impl Compositor { pub fn new() -> Self { Self::default() } - +/* pub fn push_texture(&mut self, texture: Texture2d) { self.textures.push(texture); } @@ -155,6 +190,7 @@ impl Compositor { Some(initial_tex) } +*/ } #[derive(Debug)] @@ -235,23 +271,4 @@ impl CombineTwoTexturesProgram { target } -} - -/* -impl ExternalImageHandler for Compositor { - // Do not perform any actual locking since rendering happens on the main thread - fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> webrender::ExternalImage { - let descriptor = resources::resources().image_loader.texture_descriptors[&key.0]; - ExternalImage { - uv: TexelRect { - uv0: TypedPoint2D::zero(), - uv1: TypedPoint2D::::new(descriptor.width as f32, descriptor.height as f32), - }, - source: webrender::ExternalImageSource::NativeTexture(key.0 as _), - } - } - - fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { - } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d234791c7..13dcf1456 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,7 +112,7 @@ pub mod prelude { pub use traits::{Layout, ModifyAppState, GetDom}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, - WindowMonitorTarget, RendererType, WindowEvent}; + WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; pub use window_state::WindowState; pub use font::FontError; pub use images::ImageType; diff --git a/src/svg.rs b/src/svg.rs index c64d01ce8..fd3a0158b 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -158,13 +158,6 @@ impl Clone for SvgLayer { } } -// TODO: This must be implementable as a texture! - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Svg { - pub layers: Vec, -} - #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; diff --git a/src/traits.rs b/src/traits.rs index d1053553c..afdc4dcce 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use dom::{NodeData, Dom}; use ui_description::{StyledNode, CssConstraintList, UiDescription}; use css::{Css, CssRule}; -use window::WindowId; +use window::WindowInfo; use id_tree::{NodeId, Arena}; use std::rc::Rc; use std::cell::RefCell; @@ -23,7 +23,7 @@ pub trait Layout { /// The `style_dom` looks through the given DOM rules, applies the style and /// recalculates the layout. This is done on each frame (except there are shortcuts /// when the DOM doesn't have to be recalculated). - fn layout(&self, window_id: WindowId) -> Dom where Self: Sized; + fn layout(&self, window_id: WindowInfo) -> Dom where Self: Sized; /// Applies the CSS styles to the nodes calculated from the `layout_screen` /// function and calculates the final display list that is submitted to the /// renderer. diff --git a/src/ui_state.rs b/src/ui_state.rs index 918d8a06e..dd15310f7 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -1,5 +1,5 @@ use traits::Layout; -use window::WindowId; +use window::WindowInfo; use std::collections::BTreeMap; use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; use app_state::AppState; @@ -26,14 +26,14 @@ impl fmt::Debug for UiState { } impl UiState { - pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId) -> Self + pub(crate) fn from_app_state(app_state: &AppState, window_info: WindowInfo) -> Self { use dom::{Dom, On}; // Only shortly lock the data to get the dom out let dom: Dom = { let dom_lock = app_state.data.lock().unwrap(); - dom_lock.layout(window_id) + dom_lock.layout(window_info) }; unsafe { NODE_ID = 0 }; diff --git a/src/widgets.rs b/src/widgets.rs index 28f216827..b895760a6 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,7 @@ #![allow(non_snake_case)] +use svg::SvgLayerId; +use window::ReadOnlyWindow; use traits::GetDom; use traits::Layout; use dom::{Dom, NodeType}; @@ -42,4 +44,22 @@ impl GetDom for Button { } button_root } +} + +// TODO: This must be implementable as a widget, otherwise we can +// forget + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Svg { + pub layers: Vec, +} + +impl Svg { + fn dom(window: &ReadOnlyWindow) -> Dom { + let mut svg_root = Dom::new(NodeType::Div).with_class("__azul-native-svg"); + + // todo: implement window drawing + + svg_root + } } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index b9cb94d4b..efc3a091e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,10 @@ //! Window creation module +use glium::backend::Facade; +use glium::backend::Context; use css::FakeCss; use window_state::{WindowState, WindowPosition}; -use std::{time::Duration, fmt}; +use std::{time::Duration, fmt, rc::Rc}; use webrender::{ api::*, @@ -42,12 +44,57 @@ impl WindowId { } /// User-modifiable fake window -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct FakeWindow { /// The CSS (use this field to override dynamic CSS ids). pub css: FakeCss, /// The window state for the next frame pub state: WindowState, + /// An Rc to the original WindowContext - this is only so that + /// the user can create textures and other OpenGL content in the window + /// but not change any window properties from underneath - this would + /// lead to mismatch between the + pub(crate) read_only_window: Rc, +} + +impl FakeWindow { + pub fn get_window(&self) -> ReadOnlyWindow { + ReadOnlyWindow { + inner: self.read_only_window.clone() + } + } +} + +pub struct ReadOnlyWindow { + pub(crate) inner: Rc, +} + +impl Facade for ReadOnlyWindow { + fn get_context(&self) -> &Rc { + self.inner.get_context() + } +} + +impl ReadOnlyWindow { + // Since webrender is asynchronous, we can't let the user draw + // directly onto the frame or the texture since that has to be timed + // with webrender +} + +pub struct WindowInfo { + pub window_id: WindowId, + pub window: ReadOnlyWindow, +} + +impl fmt::Debug for FakeWindow { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "FakeWindow {{\ + css: {:?}, \ + state: {:?}, \ + read_only_window: Rc, \ + }}", self.css, self.state) + } } /// Window event that is passed to the user when a callback is invoked @@ -327,12 +374,9 @@ pub struct Window { /// The webrender renderer pub(crate) renderer: Option, /// The display, i.e. the window - pub(crate) display: Display, + pub(crate) display: Rc, /// The `WindowInternal` allows us to solve some borrowing issues pub(crate) internal: WindowInternal, - /// The compositor caches and stores OpenGL textures, so that we can - /// render custom elements behind the UI if needed. - pub(crate) compositor: Compositor, /// The solver for the UI, for caching the results of the computations pub(crate) solver: UiSolver, // The background thread that is running for this window. @@ -540,7 +584,7 @@ impl Window { let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); use self::RendererType::*; - let (renderer, sender) = match options.renderer_type { + let (mut renderer, sender) = match options.renderer_type { Hardware => { // force hardware renderer Renderer::new(gl, notifier, opts_native).unwrap() @@ -574,14 +618,13 @@ impl Window { solver.suggest_value(window_dim.width_var, window_dim.width() as f64).unwrap(); solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); - let compositor = Compositor::new(); + renderer.set_external_image_handler(Box::new(Compositor::new())); let window = Window { events_loop: events_loop, state: options.state, renderer: Some(renderer), - display: display, - compositor: compositor, + display: Rc::new(display), css: css, internal: WindowInternal { layout_size: layout_size, From cc6a8b21f3ec8f71ea999ad6c507873b3d000b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 4 Jun 2018 06:09:50 +0200 Subject: [PATCH 085/868] Fixed tests for new WindowInfo API --- src/app.rs | 19 +++---------------- src/app_state.rs | 2 +- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/app.rs b/src/app.rs index edbe6f997..227f3a2d2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -390,7 +390,7 @@ impl<'a, T: Layout> App<'a, T> { /// # struct MyAppData { } /// # /// # impl Layout for MyAppData { - /// # fn layout(&self, _window_id: WindowId) -> Dom { + /// # fn layout(&self, _window_id: WindowInfo) -> Dom { /// # Dom::new(NodeType::Div) /// # } /// # } @@ -465,21 +465,8 @@ impl<'a, T: Layout> App<'a, T> { .. Default::default() }; self.create_window(hidden_create_options, Css::native()).unwrap(); - let mut ui_state_cache = Vec::with_capacity(self.windows.len()); - let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; - - for (idx, _) in self.windows.iter().enumerate() { - ui_state_cache.push(UiState::from_app_state(&self.app_state, WindowId { id: idx })); - } - - for (idx, window) in self.windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, - &ui_description_cache[idx], - &mut self.app_state.resources, - true); - window.display.swap_buffers().unwrap(); - } + let ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); + Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); } } diff --git a/src/app_state.rs b/src/app_state.rs index d32db3b9f..c2aa77f73 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -121,7 +121,7 @@ impl<'a, T: Layout> AppState<'a, T> { /// struct MyAppData { } /// /// impl Layout for MyAppData { - /// fn layout(&self, _window_id: WindowId) -> Dom { + /// fn layout(&self, _window_id: WindowInfo) -> Dom { /// let mut dom = Dom::new(NodeType::Div); /// dom.event(On::MouseEnter, Callback(my_callback)); /// dom From 77e7761d7fa429e2e1d73196dc4d8fb4eae9c967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 4 Jun 2018 07:21:27 +0200 Subject: [PATCH 086/868] Updated Notifier to be Send + Sync (fixes wayland compile error) (?) --- src/window.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/window.rs b/src/window.rs index efc3a091e..8af1571f6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -296,6 +296,17 @@ struct Notifier { events_loop_proxy: EventsLoopProxy, } +// For some reason, the wayland implementation has problems with this (?) +// However, the glium documentation explicitly says that EventsLoopProxy can +// be shared across threads. +// +// This was working absolutely fine before #cc6a8b, so I don't really get why +// this code suddenly doesn't compile anymore on wayland - I didn't even touch +// the code related to the notifier in months and didn't change the glium version +// and now it suddenly doesn't want to compile anymore. +unsafe impl Send for Notifier { } +unsafe impl Sync for Notifier { } + impl Notifier { fn new(events_loop_proxy: EventsLoopProxy) -> Notifier { Notifier { From ab0b8a9aeae72422728be64ccdfdf6f8a1fc730d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 4 Jun 2018 23:27:49 +0200 Subject: [PATCH 087/868] Fixed black screen problem + resize event not handled Moved everything into the WindowState, currently debugging why redraw events don't trigger a screen update. --- examples/debug.rs | 13 +++++++------ src/app.rs | 25 ++++++++++++++++--------- src/display_list.rs | 33 +++++++++++++++++++-------------- src/widgets.rs | 16 ++++++---------- src/window.rs | 30 ++++++++++++++++-------------- src/window_state.rs | 5 ++++- 6 files changed, 68 insertions(+), 54 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 34293936d..bee3061de 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -20,11 +20,11 @@ impl Layout for MyAppData { fn layout(&self, info: WindowInfo) -> Dom { - let mut dom = Dom::new(NodeType::Div); + // let mut dom = Dom::new(NodeType::Div); - dom.add_child( + /*dom.add_child(*/ Button::with_label("Load SVG file").dom() - .with_event(On::MouseUp, Callback(my_button_click_handler))); + .with_event(On::MouseUp, Callback(my_button_click_handler))//); /* if !self.my_svg_ids.is_empty() { dom.add_sibling( @@ -32,15 +32,16 @@ impl Layout for MyAppData { .dom(&info.window)) } */ - dom + // dom } } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { // Load and parse the SVG file, register polygon data as IDs +/* let mut svg_ids = app_state.add_svg(TEST_SVG).unwrap(); - println!("adding svg ids: {:?}", svg_ids); app_state.data.modify(|data| data.my_svg_ids.append(&mut svg_ids)); +*/ UpdateScreen::Redraw } @@ -62,7 +63,7 @@ fn main() { app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); // app.delete_image("Cat01"); - app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); + // app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } diff --git a/src/app.rs b/src/app.rs index 227f3a2d2..b52ed1db6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -157,6 +157,7 @@ impl<'a, T: Layout> App<'a, T> { let mut frame_event_info = FrameEventInfo::default(); window.events_loop.poll_events(|event| { + println!("event! : {:?}", event); let should_close = process_event(event, &mut frame_event_info); if should_close { closed_windows.push(idx); @@ -165,6 +166,7 @@ impl<'a, T: Layout> App<'a, T> { if frame_event_info.should_swap_window { window.display.swap_buffers()?; + continue; } if frame_event_info.should_hittest { @@ -184,7 +186,6 @@ impl<'a, T: Layout> App<'a, T> { window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); if frame_event_info.should_redraw_window { - println!("updating window!"); ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); Self::update_display(&window); render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); @@ -217,12 +218,15 @@ impl<'a, T: Layout> App<'a, T> { fn update_display(window: &Window) { + use webrender::api::{Transaction, DeviceUintRect, DeviceUintPoint}; + use euclid::TypedSize2D; let mut txn = Transaction::new(); - let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), window.internal.framebuffer_size); + let framebuffer_size = TypedSize2D::new(window.state.size.width, window.state.size.height); + let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), framebuffer_size); - txn.set_window_parameters(window.internal.framebuffer_size, bounds, window.internal.hidpi_factor); + txn.set_window_parameters(framebuffer_size, bounds, window.state.size.hidpi_factor); window.internal.api.send_transaction(window.internal.document_id, txn); } @@ -529,6 +533,7 @@ fn render( { use webrender::api::*; use display_list::DisplayList; + use euclid::TypedSize2D; let display_list = DisplayList::new_from_ui_description(ui_description); let builder = display_list.into_display_list_builder( @@ -537,7 +542,8 @@ fn render( &mut window.css, app_resources, &window.internal.api, - has_window_size_changed); + has_window_size_changed, + &window.state.size); if let Some(new_builder) = builder { // only finalize the list if we actually need to. Otherwise just redraw the last display list @@ -546,13 +552,14 @@ fn render( let mut txn = Transaction::new(); + let framebuffer_size = TypedSize2D::new(window.state.size.width, window.state.size.height); + let layout_size = framebuffer_size.to_f32() / TypedScale::new(window.state.size.hidpi_factor); + txn.set_display_list( window.internal.epoch, None, - window.internal.layout_size, - (window.internal.pipeline_id, - window.solver.window_dimensions.layout_size, - window.internal.last_display_list_builder.clone()), + layout_size, + (window.internal.pipeline_id, layout_size, window.internal.last_display_list_builder.clone()), true, ); @@ -561,5 +568,5 @@ fn render( window.internal.api.send_transaction(window.internal.document_id, txn); window.renderer.as_mut().unwrap().update(); - window.renderer.as_mut().unwrap().render(window.internal.framebuffer_size).unwrap(); + window.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 5e2e4b420..301dd6c58 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -17,7 +17,8 @@ use { traits::Layout, constraints::{DisplayRect, CssConstraint}, ui_description::{UiDescription, StyledNode}, - window::{WindowDimensions, UiSolver}, + window::UiSolver, + window_state::WindowSize, id_tree::{Arena, NodeId}, css_parser::{self, *}, dom::NodeData, @@ -216,10 +217,14 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { css: &mut Css, app_resources: &mut AppResources, render_api: &RenderApi, - mut has_window_size_changed: bool) + mut has_window_size_changed: bool, + window_size: &WindowSize) -> Option { + use euclid::TypedScale; + let mut changeset = None; + if let Some(root) = self.ui_descr.ui_descr_root { let local_changeset = ui_solver.dom_tree_cache.update(root, &*(self.ui_descr.ui_descr_arena.borrow())); ui_solver.edit_variable_cache.initialize_new_rectangles(&mut ui_solver.solver, &local_changeset); @@ -235,7 +240,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let arena = &*self.ui_descr.ui_descr_arena.borrow(); let dom_hash = &ui_solver.dom_tree_cache.previous_layout.arena[rect_idx]; let display_rect = ui_solver.edit_variable_cache.map[&dom_hash.data]; - let layout_contraints = create_layout_constraints(rect, rect_idx, &self.rectangles, &ui_solver.window_dimensions); + let layout_contraints = create_layout_constraints(rect, rect_idx, &self.rectangles, window_size); let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, &layout_contraints); ui_solver.solver.add_constraints(&cassowary_constraints).unwrap(); } @@ -266,8 +271,10 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { css.needs_relayout = false; - let layout_size = ui_solver.window_dimensions.layout_size; - let mut builder = DisplayListBuilder::with_capacity(pipeline_id, layout_size, self.rectangles.nodes_len()); + let framebuffer_size = LayoutSize::new(window_size.width as f32, window_size.height as f32); + let hidpi_factor = TypedScale::new(window_size.hidpi_factor); + let whole_window_layout_size = framebuffer_size.to_f32() / hidpi_factor; + let mut builder = DisplayListBuilder::with_capacity(pipeline_id, whole_window_layout_size, self.rectangles.nodes_len()); let mut resource_updates = Vec::::new(); let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; @@ -275,13 +282,15 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { Self::update_resources(render_api, app_resources, &mut resource_updates); for rect_idx in self.rectangles.linear_iter() { + let rect = &self.rectangles[rect_idx].data; + // println!("encountered rect: {:#?}", rect); // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); - // debug rectangle - let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), ui_solver.window_dimensions.layout_size); + // temporary: fill the whole window + let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), whole_window_layout_size); let info = LayoutPrimitiveInfo { rect: bounds, @@ -377,14 +386,10 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { #[inline] fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { - const TRANSPARENT: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; - let background = style.background_color.and_then(|col| Some(col.0)).unwrap_or(TRANSPARENT); - builder.push_rect(&info, background.into()); - /* match style.background_color { Some(bg) => builder.push_rect(&info, bg.0.into()), None => builder.push_clear_rect(&info), - }*/ + } } #[inline] @@ -868,7 +873,7 @@ fn create_layout_constraints<'a>( rect: &DisplayRectangle, rect_id: NodeId, arena: &Arena>, - window_dimensions: &WindowDimensions) + window_size: &WindowSize) -> Vec { use css_parser; @@ -877,7 +882,7 @@ fn create_layout_constraints<'a>( let mut layout_constraints = Vec::::new(); let max_width = arena.get_wh_for_rectangle(rect_id, WidthOrHeight::Width) - .unwrap_or(window_dimensions.layout_size.width); + .unwrap_or(window_size.width as f32); println!("max width for rectangle with the ID {} is: {}", rect_id, max_width); diff --git a/src/widgets.rs b/src/widgets.rs index b895760a6..6692e48b7 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -36,19 +36,15 @@ impl Button { impl GetDom for Button { fn dom(self) -> Dom { use self::ButtonContent::*; - - let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); - match self.content { - Image(i) => button_root.add_child(Dom::new(NodeType::Image(i))), - Text(s) => button_root.add_child(Dom::new(NodeType::Label(s))) - } - button_root + /*let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); + button_root.add_child(*/match self.content { + Image(i) => Dom::new(NodeType::Image(i)).with_class("__azul-native-button"), + Text(s) => Dom::new(NodeType::Label(s)).with_class("__azul-native-button"), + }/*); + button_root*/ } } -// TODO: This must be implementable as a widget, otherwise we can -// forget - #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Svg { pub layers: Vec, diff --git a/src/window.rs b/src/window.rs index 8af1571f6..6f4bfd0a7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -425,8 +425,6 @@ impl WindowDimensions { pub(crate) struct UiSolver { /// The actual solver pub(crate) solver: Solver, - /// Dimensions of the root window - pub(crate) window_dimensions: WindowDimensions, /// Solved layout from the previous frame (empty by default) /// This is necessary for caching the constraints of the given layout pub(crate) solved_layout: SolvedLayout, @@ -445,15 +443,17 @@ impl UiSolver { pub(crate) struct WindowInternal { pub(crate) last_display_list_builder: BuiltDisplayList, - pub(crate) layout_size: LayoutSize, pub(crate) api: RenderApi, pub(crate) epoch: Epoch, - pub(crate) framebuffer_size: DeviceUintSize, pub(crate) pipeline_id: PipelineId, pub(crate) document_id: DocumentId, - pub(crate) hidpi_factor: f32, } +/* +pub(crate) layout_size: LayoutSize, +pub(crate) framebuffer_size: DeviceUintSize, +*/ + impl Window { /// Creates a new window @@ -638,18 +638,14 @@ impl Window { display: Rc::new(display), css: css, internal: WindowInternal { - layout_size: layout_size, api: api, epoch: epoch, - framebuffer_size: framebuffer_size, pipeline_id: pipeline_id, document_id: document_id, - hidpi_factor: hidpi_factor, last_display_list_builder: BuiltDisplayList::default(), }, solver: UiSolver { solver: solver, - window_dimensions: window_dim, solved_layout: SolvedLayout::empty(), edit_variable_cache: EditVariableCache::empty(), dom_tree_cache: DomTreeCache::empty(), @@ -682,14 +678,17 @@ impl Window { if old_state.title != new_state.title { window.set_title(&new_state.title); + old_state.title = new_state.title; } if old_state.mouse_state.mouse_cursor_type != new_state.mouse_state.mouse_cursor_type { window.set_cursor(new_state.mouse_state.mouse_cursor_type); + old_state.mouse_state.mouse_cursor_type = new_state.mouse_state.mouse_cursor_type; } if old_state.is_maximized != new_state.is_maximized { window.set_maximized(new_state.is_maximized); + old_state.is_maximized = new_state.is_maximized; } if old_state.is_fullscreen != new_state.is_fullscreen { @@ -698,10 +697,12 @@ impl Window { } else { window.set_fullscreen(None); } + old_state.is_fullscreen = new_state.is_fullscreen; } if old_state.has_decorations != new_state.has_decorations { window.set_decorations(new_state.has_decorations); + old_state.has_decorations = new_state.has_decorations; } if old_state.is_visible != new_state.is_visible { @@ -710,30 +711,31 @@ impl Window { } else { window.hide(); } + old_state.is_visible = new_state.is_visible; } if old_state.size.min_dimensions != new_state.size.min_dimensions { window.set_min_dimensions(new_state.size.min_dimensions); + old_state.size.min_dimensions = new_state.size.min_dimensions; } if old_state.size.max_dimensions != new_state.size.max_dimensions { window.set_max_dimensions(new_state.size.max_dimensions); + old_state.size.max_dimensions = new_state.size.max_dimensions; } - - *old_state = new_state; } pub(crate) fn update_from_external_window_state(&mut self, frame_event_info: &mut FrameEventInfo) { use webrender::api::{DeviceUintSize, WorldPoint, LayoutSize}; if let Some((w, h)) = frame_event_info.new_window_size { - self.internal.layout_size = LayoutSize::new(w as f32, h as f32); - self.internal.framebuffer_size = DeviceUintSize::new(w, h); + self.state.size.width = w; + self.state.size.height = h; frame_event_info.should_redraw_window = true; } if let Some(dpi) = frame_event_info.new_dpi_factor { - self.internal.hidpi_factor = dpi; + self.state.size.hidpi_factor = dpi; frame_event_info.should_redraw_window = true; } } diff --git a/src/window_state.rs b/src/window_state.rs index 048146de0..e2398e39f 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -95,12 +95,14 @@ pub struct WindowPosition { } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone)] pub struct WindowSize { /// Width of the window pub width: u32, /// Height of the window pub height: u32, + /// DPI factor of the window + pub hidpi_factor: f32, /// Minimum dimensions of the window pub min_dimensions: Option<(u32, u32)>, /// Maximum dimensions of the window @@ -112,6 +114,7 @@ impl Default for WindowSize { Self { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, + hidpi_factor: 1.0, min_dimensions: None, max_dimensions: None, } From 402f21d72e012530f07156044def701d79043321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 5 Jun 2018 00:37:04 +0200 Subject: [PATCH 088/868] Updated glium + webrender --- Cargo.toml | 5 ++--- src/app.rs | 5 +++-- src/display_list.rs | 2 -- src/lib.rs | 6 ++---- src/window.rs | 5 +++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 801b9133f..fcb98bd5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Felix Schütt "] cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" -glium = { git = "https://github.com/fschutt/glium", branch = "mapedit-winit-windows-ext" } +glium = "0.21.0" gleam = "0.5" euclid = "0.17" image = "0.19.0" @@ -17,11 +17,10 @@ unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" svg = "0.5.10" lyon = { version = "0.10.0", features = ["extra"] } -lazy_static = "1.0.0" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "5e4f257d9f3cdb5691ac62f3affe2e9189d66447" +rev = "952521658aaf331e7b7382fb18ca1d8b7bfc9dc8" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/app.rs b/src/app.rs index b52ed1db6..36c5cb9ba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -157,7 +157,6 @@ impl<'a, T: Layout> App<'a, T> { let mut frame_event_info = FrameEventInfo::default(); window.events_loop.poll_events(|event| { - println!("event! : {:?}", event); let should_close = process_event(event, &mut frame_event_info); if should_close { closed_windows.push(idx); @@ -509,13 +508,14 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { WindowEvent::HiDPIFactorChanged(dpi) => { frame_event_info.new_dpi_factor = Some(dpi); }, - WindowEvent::CloseRequested => { + WindowEvent::Closed => { return true; } _ => { }, } }, Event::Awakened => { + println!("received awakened event!"); frame_event_info.should_swap_window = true; }, _ => { }, @@ -564,6 +564,7 @@ fn render( ); txn.set_root_pipeline(window.internal.pipeline_id); + println!("generating frame!"); txn.generate_frame(); window.internal.api.send_transaction(window.internal.document_id, txn); diff --git a/src/display_list.rs b/src/display_list.rs index 301dd6c58..f5937f70e 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -314,9 +314,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { builder.push_stacking_context( &info, clip_region_id, - None, TransformStyle::Flat, - None, MixBlendMode::Normal, Vec::new(), GlyphRasterSpace::Screen, diff --git a/src/lib.rs b/src/lib.rs index 13dcf1456..a86dbc99d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,8 +32,6 @@ pub extern crate glium; pub extern crate gleam; pub extern crate image; -#[macro_use] -extern crate lazy_static; extern crate euclid; extern crate lyon; extern crate svg as svg_crate; @@ -94,8 +92,8 @@ mod menu; /// The compositor takes all textures (user-defined + the UI texture(s)) and draws them on /// top of each other mod compositor; -/// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) -mod platform_ext; +// /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) +// mod platform_ext; /// Module for caching long texts (including their layout / character positions) across multiple frames mod text_cache; diff --git a/src/window.rs b/src/window.rs index 6f4bfd0a7..6dd3521a0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -324,10 +324,11 @@ impl RenderNotifier for Notifier { fn wake_up(&self) { #[cfg(not(target_os = "android"))] - self.events_loop_proxy.wakeup().unwrap_or_else(|_| { }); + self.events_loop_proxy.wakeup().unwrap_or_else(|_| { eprintln!("couldn't wakeup event loop"); }); } - fn new_frame_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool) { + fn new_frame_ready(&self, id: DocumentId, _scrolled: bool, _composite_needed: bool) { + println!("sending wakeup event: {:?}", id); self.wake_up(); } } From 0c25d57ded3cb2282ab835e52a3771910b5369c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 5 Jun 2018 17:28:45 +0200 Subject: [PATCH 089/868] Refactored display_list, initial implementation for drawing OpenGL textures --- src/display_list.rs | 264 +++++++++++++++++++++++--------------------- src/dom.rs | 28 +++-- src/text_layout.rs | 8 +- src/widgets.rs | 29 ++++- 4 files changed, 184 insertions(+), 145 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index f5937f70e..019cbe3a2 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -21,7 +21,7 @@ use { window_state::WindowSize, id_tree::{Arena, NodeId}, css_parser::{self, *}, - dom::NodeData, + dom::{NodeData, NodeType::{self, *}}, css::Css, cache::DomChangeSet, ui_description::CssConstraintList, @@ -253,19 +253,13 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { None => true, Some(c) => c.is_empty() }; -/* - // early return if we have nothing - if !css.needs_relayout && changeset_is_useless && !has_window_size_changed { - return None; - } -*/ // recalculate the actual layout if css.needs_relayout || has_window_size_changed { /* - for change in solver.fetch_changes() { - println!("change: - {:?}", change); - } + for change in solver.fetch_changes() { + println!("change: - {:?}", change); + } */ } @@ -283,118 +277,161 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { for rect_idx in self.rectangles.linear_iter() { - let rect = &self.rectangles[rect_idx].data; - // println!("encountered rect: {:#?}", rect); + let display_rectangle = &self.rectangles[rect_idx].data; + let arena = self.ui_descr.ui_descr_arena.borrow(); + let node_type = &arena[rect_idx].data.node_type; // ask the solver what the bounds of the current rectangle is // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); - // temporary: fill the whole window - let bounds = LayoutRect::new(LayoutPoint::new(0.0, 0.0), whole_window_layout_size); - - let info = LayoutPrimitiveInfo { - rect: bounds, - clip_rect: bounds, - is_backface_visible: true, - tag: rect.tag.and_then(|tag| Some((tag, 0))), - }; - - let clip_region_id = rect.style.border_radius.and_then(|border_radius| { - let region = ComplexClipRegion { - rect: bounds, - radii: border_radius, - mode: ClipMode::Clip, - }; - Some(builder.define_clip(bounds, vec![region], None)) - }); + // temporary: fill the whole window with each rectangle + displaylist_handle_rect( + &mut builder, + display_rectangle, + node_type, + full_screen_rect, /* replace this with the real bounds */ + full_screen_rect, + app_resources, + render_api, + &mut resource_updates); + } - // TODO: expose 3D-transform in CSS - // TODO: expose blend-modes in CSS - // TODO: expose filters (blur, hue, etc.) in CSS - builder.push_stacking_context( - &info, - clip_region_id, - TransformStyle::Flat, - MixBlendMode::Normal, - Vec::new(), - GlyphRasterSpace::Screen, - ); + render_api.update_resources(resource_updates); - // Push the "outset" box shadow, before the clip is active - push_box_shadow( - &mut builder, - &rect.style, - &bounds, - &full_screen_rect, - BoxShadowClipMode::Outset); + Some(builder) + } +} - // Push clip - if let Some(id) = clip_region_id { - builder.push_clip_id(id); - } +fn displaylist_handle_rect( + builder: &mut DisplayListBuilder, + rect: &DisplayRectangle, + html_node: &NodeType, + bounds: TypedRect, + full_screen_rect: TypedRect, + app_resources: &mut AppResources, + render_api: &RenderApi, + resource_updates: &mut Vec) +{ + let info = LayoutPrimitiveInfo { + rect: bounds, + clip_rect: bounds, + is_backface_visible: false, + tag: rect.tag.and_then(|tag| Some((tag, 0))), + }; - push_rect( - &info, - &mut builder, - &rect.style); + let clip_region_id = rect.style.border_radius.and_then(|border_radius| { + let region = ComplexClipRegion { + rect: bounds, + radii: border_radius, + mode: ClipMode::Clip, + }; + Some(builder.define_clip(bounds, vec![region], None)) + }); - push_background( - &info, - &bounds, - &mut builder, - &rect.style, - &app_resources); + // Push the "outset" box shadow, before the clip is active + push_box_shadow( + builder, + &rect.style, + &bounds, + &full_screen_rect, + BoxShadowClipMode::Outset); + + if let Some(id) = clip_region_id { + builder.push_clip_id(id); + } + + if let Some(ref bg_col) = rect.style.background_color { + push_rect(&info, builder, bg_col); + } - // push the inset shadow (if any) - push_box_shadow(&mut builder, - &rect.style, - &bounds, - &full_screen_rect, - BoxShadowClipMode::Inset); + if let Some(ref bg) = rect.style.background { + push_background( + &info, + &bounds, + builder, + bg, + &app_resources); + }; + // Push the inset shadow (if any) + push_box_shadow(builder, + &rect.style, + &bounds, + &full_screen_rect, + BoxShadowClipMode::Inset); + + push_border( + &info, + builder, + &rect.style); + + // handle the special content of the node + match html_node { + Div => { /* nothing special to do */ }, + Label(text) => { + // println!("encountered text with style: {:#?}", rect.style); push_text( &info, - &self, - rect_idx, - &mut builder, + text, + builder, &rect.style, app_resources, &render_api, &bounds, - &mut resource_updates); + resource_updates); + }, + Text(text_id) => { - push_border( - &info, - &mut builder, - &rect.style); + }, + Image(image_id) => { - // Pop clip - if clip_region_id.is_some() { - builder.pop_clip_id(); - } + }, + GlTexture(texture) => { + // This is probably going to destroy the texture too early, and not + // going to work properly. So this is simply an attempt at getting something going + use glium::GlObject; + let opaque = true; + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); + let key = render_api.generate_image_key(); + let data = ImageData::External(ExternalImageData { + id: ExternalImageId(texture.inner.get_id() as u64), // todo: is this how you pass a texture handle? + channel_index: 0, + image_type: ExternalImageType::TextureHandle(TextureTarget::Default), + }); - builder.pop_stacking_context(); - } + resource_updates.push(ResourceUpdate::AddImage( + AddImage { key, descriptor, data, tiling: None } + )); - render_api.update_resources(resource_updates); + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + key); + }, + } - Some(builder) + if clip_region_id.is_some() { + builder.pop_clip_id(); } } #[inline] -fn push_rect(info: &PrimitiveInfo, builder: &mut DisplayListBuilder, style: &RectStyle) { - match style.background_color { - Some(bg) => builder.push_rect(&info, bg.0.into()), - None => builder.push_clear_rect(&info), - } +fn push_rect( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + color: &BackgroundColor) +{ + builder.push_rect(&info, color.0.into()); } #[inline] fn push_text( info: &PrimitiveInfo, - display_list: &DisplayList, - rect_idx: NodeId, + text: &str, builder: &mut DisplayListBuilder, style: &RectStyle, app_resources: &mut AppResources, @@ -407,14 +444,6 @@ fn push_text( use text_layout; use css_parser::{TextAlignmentHorz, TextOverflowBehaviour}; - // NOTE: If the text is outside the current bounds, webrender will not display the text, i.e. clip it - let arena = display_list.ui_descr.ui_descr_arena.borrow(); - - let text = match arena[rect_idx].data.node_type { - Label(ref text) => text, - _ => return, - }; - if text.is_empty() { return; } @@ -443,21 +472,12 @@ fn push_text( let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); - let mut scrollbar_bar_style = RectStyle::default(); - scrollbar_bar_style.background_color = Some(BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 })); - - let mut scrollbar_background_style = RectStyle::default(); - scrollbar_background_style.background_color = Some(BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 })); - - let mut scrollbar_triangle_style = RectStyle::default(); - scrollbar_triangle_style.background_color = Some(BackgroundColor(ColorU { r: 163, g: 163, b: 163, a: 255 })); - let scrollbar_style = ScrollbarInfo { width: 17, padding: 2, - background_style: scrollbar_background_style, - triangle_style: scrollbar_triangle_style, - bar_style: scrollbar_bar_style, + background_color: BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 }), + triangle_color: BackgroundColor(ColorU { r: 163, g: 163, b: 163, a: 255 }), + bar_color: BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 }), }; let (positioned_glyphs, scrollbar_info) = text_layout::put_text_in_bounds( @@ -519,7 +539,7 @@ fn push_scrollbar( tag: None, // TODO: for hit testing }; - push_rect(&scrollbar_vertical_background_info, builder, &scrollbar_style.background_style); + push_rect(&scrollbar_vertical_background_info, builder, &scrollbar_style.background_color); } { @@ -540,7 +560,7 @@ fn push_scrollbar( tag: None, // TODO: for hit testing }; - push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_style); + push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_color); } { @@ -559,11 +579,11 @@ fn push_scrollbar( scrollbar_triangle_rect.size.width /= 2.0; scrollbar_triangle_rect.size.height /= 2.0; - push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_style, TriangleDirection::PointUp); + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointUp); // Triangle bottom scrollbar_triangle_rect.origin.y += bounds.size.height - scrollbar_style.width as f32 + scrollbar_style.padding as f32; - push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_style, TriangleDirection::PointDown); + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointDown); } } @@ -577,7 +597,7 @@ enum TriangleDirection { fn push_triangle( bounds: &TypedRect, builder: &mut DisplayListBuilder, - style: &RectStyle, + background_color: &BackgroundColor, direction: TriangleDirection) { use euclid::TypedPoint2D; @@ -586,11 +606,6 @@ fn push_triangle( // see: https://css-tricks.com/snippets/css/css-triangle/ // uses the "3d effect" for making a triangle - let background_color = match style.background_color { - None => return, - Some(s) => s, - }; - let triangle_rect_info = PrimitiveInfo { rect: *bounds, clip_rect: *bounds, @@ -685,14 +700,9 @@ fn push_background( info: &PrimitiveInfo, bounds: &TypedRect, builder: &mut DisplayListBuilder, - style: &RectStyle, + background: &Background, app_resources: &AppResources) { - let background = match style.background { - Some(ref bg) => bg, - None => return, - }; - match background { Background::RadialGradient(gradient) => { let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| diff --git a/src/dom.rs b/src/dom.rs index 66515f8a1..f03e86fe3 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -98,7 +98,7 @@ pub enum NodeType { #[derive(Debug, Clone)] pub struct Texture { - inner: Rc, + pub(crate) inner: Rc, } impl Texture { @@ -361,36 +361,48 @@ impl Dom { /// Same as `id`, but easier to use for method chaining in a builder-style pattern #[inline] pub fn with_id>(mut self, id: S) -> Self { - self.id(id); + self.set_id(id); self } /// Same as `id`, but easier to use for method chaining in a builder-style pattern #[inline] pub fn with_class>(mut self, class: S) -> Self { - self.class(class); + self.set_class(class); self } /// Same as `event`, but easier to use for method chaining in a builder-style pattern #[inline] - pub fn with_event(mut self, on: On, callback: Callback) -> Self { - self.event(on, callback); + pub fn with_callback(mut self, on: On, callback: Callback) -> Self { + self.set_callback(on, callback); self } #[inline] - pub fn id>(&mut self, id: S) { + pub fn with_child(mut self, child: Self) -> Self { + self.add_child(child); + self + } + + #[inline] + pub fn with_sibling(mut self, sibling: Self) -> Self { + self.add_sibling(sibling); + self + } + + #[inline] + pub fn set_id>(&mut self, id: S) { self.arena.borrow_mut()[self.last].data.id = Some(id.into()); } #[inline] - pub fn class>(&mut self, class: S) { + pub fn set_class>(&mut self, class: S) { self.arena.borrow_mut()[self.last].data.classes.push(class.into()); } #[inline] - pub fn event(&mut self, on: On, callback: Callback) { + pub fn set_callback(&mut self, on: On, callback: Callback) { self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); self.arena.borrow_mut()[self.last].data.tag = Some(unsafe { NODE_ID }); unsafe { NODE_ID += 1; }; diff --git a/src/text_layout.rs b/src/text_layout.rs index 9a6e5d2f6..6e345f28d 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -3,7 +3,7 @@ use webrender::api::*; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; -use css_parser::{TextAlignmentHorz, RectStyle, TextAlignmentVert, LineHeight, LayoutOverflow}; +use css_parser::{TextAlignmentHorz, BackgroundColor, TextAlignmentVert, LineHeight, LayoutOverflow}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing @@ -101,11 +101,11 @@ pub(crate) struct ScrollbarInfo { /// Padding of the scrollbar, in pixels. The inner bar is `width - padding` pixels wide. pub(crate) padding: usize, /// Style of the scrollbar (how to draw it) - pub(crate) bar_style: RectStyle, + pub(crate) bar_color: BackgroundColor, /// How to draw the "up / down" arrows - pub(crate) triangle_style: RectStyle, + pub(crate) triangle_color: BackgroundColor, /// Style of the scrollbar background - pub(crate) background_style: RectStyle, + pub(crate) background_color: BackgroundColor, } /// Temporary struct so I don't have to pass the three parameters around seperately all the time diff --git a/src/widgets.rs b/src/widgets.rs index 6692e48b7..57894ca68 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -36,12 +36,12 @@ impl Button { impl GetDom for Button { fn dom(self) -> Dom { use self::ButtonContent::*; - /*let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); - button_root.add_child(*/match self.content { - Image(i) => Dom::new(NodeType::Image(i)).with_class("__azul-native-button"), - Text(s) => Dom::new(NodeType::Label(s)).with_class("__azul-native-button"), - }/*); - button_root*/ + let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); + button_root.add_child(match self.content { + Image(i) => Dom::new(NodeType::Image(i)), + Text(s) => Dom::new(NodeType::Label(s)), + }); + button_root } } @@ -58,4 +58,21 @@ impl Svg { svg_root } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Label { + pub text: String, +} + +impl Label { + pub fn new>(text: S) -> Self { + Self { text: text.into() } + } +} + +impl GetDom for Label { + fn dom(self) -> Dom { + Dom::new(NodeType::Label(self.text)) + } } \ No newline at end of file From cbaded45a2d39cab3c7fd14e70bccb0247c9e939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 5 Jun 2018 23:03:02 +0200 Subject: [PATCH 090/868] Added functions to load and draw textures, currently not working correctly Also updated documentation after DOM changes --- examples/debug.rs | 14 +------------- src/app_state.rs | 5 ++--- src/display_list.rs | 2 ++ src/dom.rs | 7 ++++++- src/widgets.rs | 17 +++++++++++++---- src/window.rs | 6 ++++++ 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index bee3061de..7e09e216c 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -20,19 +20,7 @@ impl Layout for MyAppData { fn layout(&self, info: WindowInfo) -> Dom { - // let mut dom = Dom::new(NodeType::Div); - - /*dom.add_child(*/ - Button::with_label("Load SVG file").dom() - .with_event(On::MouseUp, Callback(my_button_click_handler))//); - /* - if !self.my_svg_ids.is_empty() { - dom.add_sibling( - Svg::new(self.my_svg_ids.clone()) - .dom(&info.window)) - } - */ - // dom + Svg::empty().dom(&info.window) } } diff --git a/src/app_state.rs b/src/app_state.rs index c2aa77f73..10f15ba11 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -122,9 +122,8 @@ impl<'a, T: Layout> AppState<'a, T> { /// /// impl Layout for MyAppData { /// fn layout(&self, _window_id: WindowInfo) -> Dom { - /// let mut dom = Dom::new(NodeType::Div); - /// dom.event(On::MouseEnter, Callback(my_callback)); - /// dom + /// Dom::new(NodeType::Div) + /// .with_callback(On::MouseEnter, Callback(my_callback)) /// } /// } /// diff --git a/src/display_list.rs b/src/display_list.rs index 019cbe3a2..8e08773fc 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -390,10 +390,12 @@ fn displaylist_handle_rect( // This is probably going to destroy the texture too early, and not // going to work properly. So this is simply an attempt at getting something going use glium::GlObject; + let opaque = true; let allow_mipmaps = true; let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); let key = render_api.generate_image_key(); + let data = ImageData::External(ExternalImageData { id: ExternalImageId(texture.inner.get_id() as u64), // todo: is this how you pass a texture handle? channel_index: 0, diff --git a/src/dom.rs b/src/dom.rs index f03e86fe3..0a6d499ac 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -15,6 +15,7 @@ use svg::SvgLayerId; use images::ImageId; use cache::DomHash; use text_cache::TextId; +use glium::framebuffer::SimpleFrameBuffer; /// This is only accessed from the main thread, so it's safe to use pub(crate) static mut NODE_ID: u64 = 0; @@ -102,11 +103,15 @@ pub struct Texture { } impl Texture { - fn new(tex: Texture2d) -> Self { + pub(crate) fn new(tex: Texture2d) -> Self { Self { inner: Rc::new(tex), } } + + pub fn as_surface<'a>(&'a self) -> SimpleFrameBuffer<'a> { + self.inner.as_surface() + } } impl Hash for Texture { diff --git a/src/widgets.rs b/src/widgets.rs index 57894ca68..f2bc0d964 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -51,12 +51,21 @@ pub struct Svg { } impl Svg { - fn dom(window: &ReadOnlyWindow) -> Dom { - let mut svg_root = Dom::new(NodeType::Div).with_class("__azul-native-svg"); + // todo: remove this later + pub fn empty() -> Self { + Self { layers: Vec::new() } + } + + pub fn dom(&self, window: &ReadOnlyWindow) -> Dom { + use glium::Surface; - // todo: implement window drawing + let tex = window.create_texture(800, 800); + tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); - svg_root + Dom::new(NodeType::Div) + .with_class("__azul-native-svg") + .with_child(Dom::new(NodeType::GlTexture(tex))) + .with_id("my_opengl_id") } } diff --git a/src/window.rs b/src/window.rs index 6dd3521a0..63fe10c3f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,6 @@ //! Window creation module +use dom::Texture; use glium::backend::Facade; use glium::backend::Context; use css::FakeCss; @@ -79,6 +80,11 @@ impl ReadOnlyWindow { // Since webrender is asynchronous, we can't let the user draw // directly onto the frame or the texture since that has to be timed // with webrender + pub fn create_texture(&self, width: u32, height: u32) -> Texture { + use glium::texture::texture2d::Texture2d; + let tex = Texture2d::empty(&*self.inner, width, height).unwrap(); + Texture::new(tex) + } } pub struct WindowInfo { From d0026234fbd42bbfb924ab62544f88b836e58f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 6 Jun 2018 02:52:00 +0200 Subject: [PATCH 091/868] Push temporary not_freed_gl_textures implementation (currently crashing) --- src/app.rs | 3 +++ src/display_list.rs | 10 ++++++++-- src/resources.rs | 24 +++++++++++++++++++++++- src/widgets.rs | 4 +++- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 36c5cb9ba..565e102ee 100644 --- a/src/app.rs +++ b/src/app.rs @@ -535,9 +535,12 @@ fn render( use display_list::DisplayList; use euclid::TypedSize2D; + println!("epoch: {:?}", window.internal.epoch); + let display_list = DisplayList::new_from_ui_description(ui_description); let builder = display_list.into_display_list_builder( window.internal.pipeline_id, + window.internal.epoch, &mut window.solver, &mut window.css, app_resources, diff --git a/src/display_list.rs b/src/display_list.rs index 8e08773fc..e777891b1 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -213,6 +213,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { pub fn into_display_list_builder( &self, pipeline_id: PipelineId, + current_epoch: Epoch, ui_solver: &mut UiSolver, css: &mut Css, app_resources: &mut AppResources, @@ -293,7 +294,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { full_screen_rect, app_resources, render_api, - &mut resource_updates); + &mut resource_updates, + current_epoch); } render_api.update_resources(resource_updates); @@ -310,7 +312,8 @@ fn displaylist_handle_rect( full_screen_rect: TypedRect, app_resources: &mut AppResources, render_api: &RenderApi, - resource_updates: &mut Vec) + resource_updates: &mut Vec, + current_epoch: Epoch) { let info = LayoutPrimitiveInfo { rect: bounds, @@ -413,6 +416,9 @@ fn displaylist_handle_rect( ImageRendering::Auto, AlphaType::Alpha, key); + + app_resources.not_freed_gl_textures.entry(current_epoch).or_insert(Vec::new()) + .push(texture.clone()); }, } diff --git a/src/resources.rs b/src/resources.rs index 2875d6add..df62eb947 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,3 +1,5 @@ +use webrender::api::Epoch; +use dom::Texture; use text_cache::TextRegistry; use traits::Layout; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -40,10 +42,19 @@ pub(crate) struct AppResources<'a, T: Layout> { // we first need to create one. pub(crate) fonts: FastHashMap>, /// Stores the polygon data for all SVGs. Polygons can be shared across windows - /// without duplicating the data. This doesn't store any rendering-related data, only the polygons + /// without duplicating the data. This doesn't store any rendering-related data, + /// only the polygons pub(crate) svg_registry: SvgRegistry, /// Stores long texts across frames pub(crate) text_registry: TextRegistry, + /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long + /// as webrender needs it for drawing. To transparently do this, we store the epoch that the + /// texture was originally created with, and check, **after we have drawn the frame**, + /// if there are any textures that need cleanup. + /// + /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL + /// texture) does not run until we remove the textures + pub(crate) not_freed_gl_textures: FastHashMap> } impl<'a, T: Layout> Default for AppResources<'a, T> { @@ -54,6 +65,7 @@ impl<'a, T: Layout> Default for AppResources<'a, T> { font_data: FastHashMap::default(), images: FastHashMap::default(), text_registry: TextRegistry::default(), + not_freed_gl_textures: FastHashMap::default(), } } } @@ -185,6 +197,16 @@ impl<'a, T: Layout> AppResources<'a, T> { self.text_registry.clear_all_texts(); } + /// This function should be called after webrender is done with drawing + /// all the textures for a certain frame ID (i.e. "epoch") - meaning after + /// `display.swap_buffers()` is done. Since the `Texture` is simply a + /// wrapper around `Rc`, invoking this function will destruct + /// the last `Rc` that holds a reference to the handle and execute the + /// destructor for all OpenGL textures that are currently in use. + pub(crate) fn clean_up_gl_textures_for_epoch(&mut self, epoch: &Epoch) { + self.not_freed_gl_textures.remove(epoch); + } + /// Parses an input source, parses the SVG, adds the shapes as layers into /// the registry, returns the IDs of the added shapes, in the order that /// they appeared in the SVG text. diff --git a/src/widgets.rs b/src/widgets.rs index f2bc0d964..ccd725153 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -61,11 +61,13 @@ impl Svg { let tex = window.create_texture(800, 800); tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); - +/* Dom::new(NodeType::Div) .with_class("__azul-native-svg") .with_child(Dom::new(NodeType::GlTexture(tex))) .with_id("my_opengl_id") +*/ + Dom::new(NodeType::GlTexture(tex)) } } From befde8df235f1a53ba2200124422e674d380d8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 6 Jun 2018 04:21:23 +0200 Subject: [PATCH 092/868] Implemented GlTexture drawing more correctly... does not yet clear the surface properly, though. --- Cargo.toml | 1 + src/app.rs | 6 +- src/compositor.rs | 261 +++++--------------------------------------- src/display_list.rs | 16 +-- src/lib.rs | 2 + src/resources.rs | 19 ---- src/widgets.rs | 2 +- src/window.rs | 5 +- 8 files changed, 45 insertions(+), 267 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fcb98bd5e..86159488f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" svg = "0.5.10" lyon = { version = "0.10.0", features = ["extra"] } +lazy_static = "1.0.1" [dependencies.webrender] git = "https://github.com/servo/webrender" diff --git a/src/app.rs b/src/app.rs index 565e102ee..df2561c42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -312,6 +312,7 @@ impl<'a, T: Layout> App<'a, T> { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &mut app_state.resources, true); window.display.swap_buffers().unwrap(); + } ui_description_cache @@ -515,7 +516,6 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { } }, Event::Awakened => { - println!("received awakened event!"); frame_event_info.should_swap_window = true; }, _ => { }, @@ -535,12 +535,9 @@ fn render( use display_list::DisplayList; use euclid::TypedSize2D; - println!("epoch: {:?}", window.internal.epoch); - let display_list = DisplayList::new_from_ui_description(ui_description); let builder = display_list.into_display_list_builder( window.internal.pipeline_id, - window.internal.epoch, &mut window.solver, &mut window.css, app_resources, @@ -567,7 +564,6 @@ fn render( ); txn.set_root_pipeline(window.internal.pipeline_id); - println!("generating frame!"); txn.generate_frame(); window.internal.api.send_transaction(window.internal.document_id, txn); diff --git a/src/compositor.rs b/src/compositor.rs index cf84116f3..dc258d4dd 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -2,10 +2,11 @@ //! This makes it possible to use OpenGL images in the background and compose SVG elements //! into the UI. +use dom::Texture; use FastHashMap; use webrender::{ExternalImageHandler, ExternalImageSource}; use webrender::api::{ExternalImageId, TexelRect, DevicePixel}; -use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicBool}}; +use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}; use glium::{ Program, VertexBuffer, Display, @@ -16,118 +17,37 @@ use glium::{ use webrender::ExternalImage; use euclid::TypedPoint2D; -// I'd wrap this in a `Arc>`, but this is only available on nightly -// So, for now, this is completely thread-unsafe -// -// However, this should be fine, as we initialize the program only from the main -// thread and never de-initialize it -static mut SHADER_FULL_SCREEN: Option = None; - -pub const INDICES_NO_INDICES_TRIANGLE_STRIP: NoIndices = NoIndices(TriangleStrip); - -const SIMPLE_FRAGMENT_SHADER: &'static str = "\ - #version 130 - - in vec4 v_color; - - void main() { - gl_FragColor = v_color; - } -"; - -/// Simple fragment shader that combines two textures `tex1` and `tex2`, -/// drawing `tex2` over `tex1` -/// -/// ## Inputs -/// -/// - `vec2 v_tex_coords`: The texture coordinates of the vertices -/// (see `VERTEX_SHADER_FULL_SCREEN`) -/// - `uniform sampler2d tex1`: The lower texture to be drawn (RGBA) -/// - `uniform sampler2d tex2`: The texture to draw on top (RGBA) -/// -/// ## Outputs -/// -/// - `vec4 gl_FragColor`: The color on the screen / to a different texture -const TWO_TEXTURES_FRAGMENT_SHADER: &'static str = "\ - #version 130 - - in vec2 v_tex_coords; - uniform sampler2D tex1; - uniform sampler2D tex2; - - void main() { - vec4 tex1_color = texture(tex1, v_tex_coords); - vec4 tex2_color = texture(tex2, v_tex_coords); - gl_FragColor = mix(tex1_color, tex2_color, tex2_color.a); - } -"; +lazy_static! { + /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long + /// as webrender needs it for drawing. To transparently do this, we store the epoch that the + /// texture was originally created with, and check, **after we have drawn the frame**, + /// if there are any textures that need cleanup. + /// + /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL + /// texture) does not run until we remove the textures + pub(crate) static ref ACTIVE_GL_TEXTURES: Mutex> = Mutex::new(FastHashMap::default()); +} -/// This is a vertex shader that should be called with a glDrawArrays(3) and no data -/// What it does is to generate a triangle that stretches over the whole screen: -/// -/// ```no_run,ignore -/// + -/// | - -/// | - -/// | - -/// +-----------+ -/// | | - -/// | screen | - -/// | | - -/// +-----------+-----------+ -/// ``` +/// The Texture struct is public to the user /// -/// It also sets up the texture coordinates. So if you pair it with the -/// `TWO_TEXTURES_FRAGMENT_SHADER`, you can draw two textures on top of each other -const VERTEX_SHADER_FULL_SCREEN: &str = " - #version 140 - out vec2 v_tex_coords; - void main() { - float x = -1.0 + float((gl_VertexID & 1) << 2); - float y = -1.0 + float((gl_VertexID & 2) << 1); - v_tex_coords = vec2((x+1.0)*0.5, (y+1.0)*0.5); - gl_Position = vec4(x, y, 0, 1); - } -"; - -#[derive(Debug, Copy, Clone)] -pub struct SimpleGpuVertex { - pub coordinate: [f32; 2], - pub tex_coords: [f32; 2], +/// With this wrapper struct we can implement Send + Sync, but we don't want to do that +/// on the Texture itself +#[derive(Debug)] +pub(crate) struct ActiveTexture { + pub(crate) texture: Texture, } -implement_vertex!(SimpleGpuVertex, coordinate, tex_coords); - -pub const VERTEXBUFFER_FOR_FULL_SCREEN_QUAD: [SimpleGpuVertex;3] = [ - // top left - SimpleGpuVertex { - coordinate: [-1.0, 1.0], - tex_coords: [-1.0, 1.0], - }, - // bottom left - SimpleGpuVertex { - coordinate: [-1.0, -1.0], - tex_coords: [-1.0, -1.0], - }, - // top right - SimpleGpuVertex { - coordinate: [1.0, 1.0], - tex_coords: [1.0, 1.0], - }, -]; +// necessary because of lazy_static rules - theoretically unsafe, +// but we do addition / removal of textures on the main thread +unsafe impl Send for ActiveTexture { } +unsafe impl Sync for ActiveTexture { } #[derive(Debug)] -pub struct Compositor { - textures: FastHashMap, - locked: AtomicBool, -} +pub(crate) struct Compositor { } impl Default for Compositor { fn default() -> Self { - Self { - textures: FastHashMap::default(), - locked: AtomicBool::new(false), - } + Self { } } } @@ -135,140 +55,19 @@ impl ExternalImageHandler for Compositor { fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> ExternalImage { use glium::GlObject; - let texture = &self.textures[&key]; - self.locked.compare_and_swap(false, true, Ordering::SeqCst); + let gl_tex_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); + let tex = &gl_tex_lock[&key]; ExternalImage { uv: TexelRect { uv0: TypedPoint2D::zero(), - uv1: TypedPoint2D::::new(texture.width() as f32, texture.height() as f32), - }, - source: ExternalImageSource::NativeTexture(texture.get_id()), - } - } - - fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { - self.locked.compare_and_swap(true, false, Ordering::SeqCst); - } -} - -impl Compositor { - - pub fn new() -> Self { - Self::default() - } -/* - pub fn push_texture(&mut self, texture: Texture2d) { - self.textures.push(texture); - } - - /// Combine all texture together. Returns None if there are no textures in `self.textures`. - pub fn combine_all_textures(&self, display: &T, sample_behaviour: SampleBehaviour) - -> Option - { - // lazily initialize shader - if unsafe { SHADER_FULL_SCREEN.is_none() } { - unsafe { SHADER_FULL_SCREEN = Some(CombineTwoTexturesProgram::new(display)) }; - } - - let shader = unsafe { SHADER_FULL_SCREEN.as_ref().unwrap() }; - let mut iter = self.textures.iter().skip(1); - - let mut initial_tex: Texture2d = match self.textures.get(0) { - Some(tex) => { - // TODO: this could be optimized - let (w, h) = (tex.width(), tex.height()); - Texture2d::empty(display, w, h).unwrap() + uv1: TypedPoint2D::::new(tex.texture.inner.width() as f32, tex.texture.inner.height() as f32), }, - None => return None, - }; - - while let Some(tex2) = iter.next() { - let combined = shader.draw(display, &initial_tex, tex2, sample_behaviour); - initial_tex = combined; - } - - Some(initial_tex) - } -*/ -} - -#[derive(Debug)] -pub struct CombineTwoTexturesProgram { - program: Program, - vertex_buffer: VertexBuffer, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum SampleBehaviour { - GlNearest, - GlLinear, -} - -impl CombineTwoTexturesProgram { - - /// Uses `VERTEX_SHADER_FULL_SCREEN`, `TWO_TEXTURES_FRAGMENT_SHADER` - /// and `VERTEXBUFFER_FOR_FULL_SCREEN_QUAD`. - pub fn new(display: &T) -> Self { - let program = Program::from_source(display, VERTEX_SHADER_FULL_SCREEN, TWO_TEXTURES_FRAGMENT_SHADER, None).unwrap(); - let vertex_buf = VertexBuffer::new(display, &VERTEXBUFFER_FOR_FULL_SCREEN_QUAD).unwrap(); - Self { - program: program, - vertex_buffer: vertex_buf, + source: ExternalImageSource::NativeTexture(tex.texture.inner.get_id()), } } - /// Draw tex2 over tex1, returns a new texture with the combined result - /// - /// NOTE: `sample_behaviour`: specify if using `GL_NEAREST` or `GL_LINEAR` - /// for blending - pub fn draw( - &self, - display: &T, - tex1: &Texture2d, - tex2: &Texture2d, - sample_behaviour: SampleBehaviour) - -> Texture2d - { - use self::SampleBehaviour::*; - use glium::{ - Surface, - uniforms::{MagnifySamplerFilter, MinifySamplerFilter}, - texture::{CompressedSrgbFormat, CompressedMipmapsOption}, - }; - - let max_width = tex1.width().max(tex2.width()); - let max_height = tex1.height().max(tex2.height()); - - let (tex1, tex2) = match sample_behaviour { - GlNearest => { - (tex1.sampled() - .magnify_filter(MagnifySamplerFilter::Nearest) - .minify_filter(MinifySamplerFilter::LinearMipmapNearest), - tex2.sampled() - .magnify_filter(MagnifySamplerFilter::Nearest) - .minify_filter(MinifySamplerFilter::LinearMipmapNearest)) - }, - GlLinear => { - (tex1.sampled(), - tex2.sampled()) - } - }; - - let uniforms = uniform! { - tex1: tex1, - tex2: tex2, - }; - - let target = Texture2d::empty(display, max_width, max_height).unwrap(); - - target.as_surface().draw( - &self.vertex_buffer, - &INDICES_NO_INDICES_TRIANGLE_STRIP, - &self.program, - &uniforms, - &Default::default()).unwrap(); - - target + fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) { + ACTIVE_GL_TEXTURES.lock().unwrap().remove(&key); } } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index e777891b1..c6fc865a1 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -213,7 +213,6 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { pub fn into_display_list_builder( &self, pipeline_id: PipelineId, - current_epoch: Epoch, ui_solver: &mut UiSolver, css: &mut Css, app_resources: &mut AppResources, @@ -294,8 +293,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { full_screen_rect, app_resources, render_api, - &mut resource_updates, - current_epoch); + &mut resource_updates); } render_api.update_resources(resource_updates); @@ -312,8 +310,7 @@ fn displaylist_handle_rect( full_screen_rect: TypedRect, app_resources: &mut AppResources, render_api: &RenderApi, - resource_updates: &mut Vec, - current_epoch: Epoch) + resource_updates: &mut Vec) { let info = LayoutPrimitiveInfo { rect: bounds, @@ -393,14 +390,18 @@ fn displaylist_handle_rect( // This is probably going to destroy the texture too early, and not // going to work properly. So this is simply an attempt at getting something going use glium::GlObject; + use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; let opaque = true; let allow_mipmaps = true; let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); let key = render_api.generate_image_key(); + let external_image_id = ExternalImageId(texture.inner.get_id() as u64); + + println!("pushing texture with ID: {:?}", external_image_id); let data = ImageData::External(ExternalImageData { - id: ExternalImageId(texture.inner.get_id() as u64), // todo: is this how you pass a texture handle? + id: external_image_id, channel_index: 0, image_type: ExternalImageType::TextureHandle(TextureTarget::Default), }); @@ -417,8 +418,7 @@ fn displaylist_handle_rect( AlphaType::Alpha, key); - app_resources.not_freed_gl_textures.entry(current_epoch).or_insert(Vec::new()) - .push(texture.clone()); + ACTIVE_GL_TEXTURES.lock().unwrap().insert(external_image_id, ActiveTexture { texture: texture.clone() }); }, } diff --git a/src/lib.rs b/src/lib.rs index a86dbc99d..05c2097ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,8 @@ pub extern crate glium; pub extern crate gleam; pub extern crate image; +#[macro_use] +extern crate lazy_static; extern crate euclid; extern crate lyon; extern crate svg as svg_crate; diff --git a/src/resources.rs b/src/resources.rs index df62eb947..62f3d73a6 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -47,14 +47,6 @@ pub(crate) struct AppResources<'a, T: Layout> { pub(crate) svg_registry: SvgRegistry, /// Stores long texts across frames pub(crate) text_registry: TextRegistry, - /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long - /// as webrender needs it for drawing. To transparently do this, we store the epoch that the - /// texture was originally created with, and check, **after we have drawn the frame**, - /// if there are any textures that need cleanup. - /// - /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL - /// texture) does not run until we remove the textures - pub(crate) not_freed_gl_textures: FastHashMap> } impl<'a, T: Layout> Default for AppResources<'a, T> { @@ -65,7 +57,6 @@ impl<'a, T: Layout> Default for AppResources<'a, T> { font_data: FastHashMap::default(), images: FastHashMap::default(), text_registry: TextRegistry::default(), - not_freed_gl_textures: FastHashMap::default(), } } } @@ -197,16 +188,6 @@ impl<'a, T: Layout> AppResources<'a, T> { self.text_registry.clear_all_texts(); } - /// This function should be called after webrender is done with drawing - /// all the textures for a certain frame ID (i.e. "epoch") - meaning after - /// `display.swap_buffers()` is done. Since the `Texture` is simply a - /// wrapper around `Rc`, invoking this function will destruct - /// the last `Rc` that holds a reference to the handle and execute the - /// destructor for all OpenGL textures that are currently in use. - pub(crate) fn clean_up_gl_textures_for_epoch(&mut self, epoch: &Epoch) { - self.not_freed_gl_textures.remove(epoch); - } - /// Parses an input source, parses the SVG, adds the shapes as layers into /// the registry, returns the IDs of the added shapes, in the order that /// they appeared in the SVG text. diff --git a/src/widgets.rs b/src/widgets.rs index ccd725153..3d30bdbd1 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -59,7 +59,7 @@ impl Svg { pub fn dom(&self, window: &ReadOnlyWindow) -> Dom { use glium::Surface; - let tex = window.create_texture(800, 800); + let tex = window.create_texture(800, 600); tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); /* Dom::new(NodeType::Div) diff --git a/src/window.rs b/src/window.rs index 63fe10c3f..f6cb23bb5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -333,8 +333,7 @@ impl RenderNotifier for Notifier { self.events_loop_proxy.wakeup().unwrap_or_else(|_| { eprintln!("couldn't wakeup event loop"); }); } - fn new_frame_ready(&self, id: DocumentId, _scrolled: bool, _composite_needed: bool) { - println!("sending wakeup event: {:?}", id); + fn new_frame_ready(&self, _id: DocumentId, _scrolled: bool, _composite_needed: bool) { self.wake_up(); } } @@ -636,7 +635,7 @@ impl Window { solver.suggest_value(window_dim.width_var, window_dim.width() as f64).unwrap(); solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); - renderer.set_external_image_handler(Box::new(Compositor::new())); + renderer.set_external_image_handler(Box::new(Compositor::default())); let window = Window { events_loop: events_loop, From 0ae009d87fdb570e1607655b2fd151d3cd27da6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 06:09:41 +0200 Subject: [PATCH 093/868] Fixed resizing / updating bug Because winit "eats" the "awakened" event during a resize (at least on Windows), we need to relayout-and-redraw the screen twice (since the first "redraw" event gets eaten). --- Cargo.toml | 2 +- src/app.rs | 54 ++++++++++++++++++++++++++++------------- src/display_list.rs | 7 ++---- src/widgets.rs | 13 +++++++--- src/window.rs | 59 +++++++++++++++++++++------------------------ 5 files changed, 77 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 86159488f..da99e6a97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ lazy_static = "1.0.1" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "952521658aaf331e7b7382fb18ca1d8b7bfc9dc8" +rev = "2e381e0325f367429810099d5abf76e674f697ed" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/app.rs b/src/app.rs index df2561c42..351ef9b4d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -64,6 +64,7 @@ pub(crate) struct FrameEventInfo { pub(crate) cur_cursor_pos: (f64, f64), pub(crate) new_window_size: Option<(u32, u32)>, pub(crate) new_dpi_factor: Option, + pub(crate) is_resize_event: bool, } impl Default for FrameEventInfo { @@ -75,6 +76,7 @@ impl Default for FrameEventInfo { cur_cursor_pos: (0.0, 0.0), new_window_size: None, new_dpi_factor: None, + is_resize_event: false, } } } @@ -138,20 +140,22 @@ impl<'a, T: Layout> App<'a, T> { let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); let mut ui_description_cache = Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); + let mut force_redraw_cache = vec![0_usize; self.windows.len()]; + while !self.windows.is_empty() { let time_start = Instant::now(); let mut closed_windows = Vec::::new(); for (idx, ref mut window) in self.windows.iter_mut().enumerate() { - +/* unsafe { use glium::glutin::GlContext; window.display.gl_window().make_current().unwrap(); } - +*/ // TODO: move this somewhere else - let svg_shader = &self.app_state.resources.svg_registry.init_shader(&window.display); + // let svg_shader = &self.app_state.resources.svg_registry.init_shader(&window.display); let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); @@ -163,30 +167,45 @@ impl<'a, T: Layout> App<'a, T> { } }); - if frame_event_info.should_swap_window { + if frame_event_info.is_resize_event { + // This is a hack because during a resize event, winit eats the "awakened" + // event. So what we do is that we call the layout-and-render again, to + // trigger a second "awakened" event. So when the window is resized, the + // layout function is called twice (the first event will be eaten by winit) + // + // This is a reported bug and should be fixed somewhere in July + force_redraw_cache[idx] = 3; + } + + if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; - continue; + if let Some(i) = force_redraw_cache.get_mut(idx) { + *i -= 1; + } } if frame_event_info.should_hittest { Self::do_hit_test_and_call_callbacks(window, window_id, &mut frame_event_info, &ui_state_cache, &mut self.app_state); } - ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { - window_id: WindowId { id: idx }, - window: ReadOnlyWindow { - inner: window.display.clone(), - } - }); - // Update the window state that we got from the frame event (updates window dimensions and DPI) window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); - if frame_event_info.should_redraw_window { + if frame_event_info.should_redraw_window || force_redraw_cache[idx] > 0 { + // Call the Layout::layout() fn, get the DOM + ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { + window_id: WindowId { id: idx }, + window: ReadOnlyWindow { + inner: window.display.clone(), + } + }); + // Style the DOM ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + // send webrender the size and buffer of the display Self::update_display(&window); + // render the window (webrender will send an Awakened event when the frame is done) render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); } } @@ -195,6 +214,7 @@ impl<'a, T: Layout> App<'a, T> { closed_windows.into_iter().for_each(|closed_window_id| { ui_state_cache.remove(closed_window_id); ui_description_cache.remove(closed_window_id); + force_redraw_cache.remove(closed_window_id); self.windows.remove(closed_window_id); }); @@ -217,7 +237,6 @@ impl<'a, T: Layout> App<'a, T> { fn update_display(window: &Window) { - use webrender::api::{Transaction, DeviceUintRect, DeviceUintPoint}; use euclid::TypedSize2D; @@ -311,8 +330,6 @@ impl<'a, T: Layout> App<'a, T> { for (idx, window) in windows.iter_mut().enumerate() { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &mut app_state.resources, true); - window.display.swap_buffers().unwrap(); - } ui_description_cache @@ -502,12 +519,15 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { }, WindowEvent::Resized(w, h) => { frame_event_info.new_window_size = Some((w, h)); + frame_event_info.is_resize_event = true; + frame_event_info.should_redraw_window = true; }, WindowEvent::Refresh => { frame_event_info.should_redraw_window = true; }, WindowEvent::HiDPIFactorChanged(dpi) => { frame_event_info.new_dpi_factor = Some(dpi); + frame_event_info.should_redraw_window = true; }, WindowEvent::Closed => { return true; @@ -565,8 +585,8 @@ fn render( txn.set_root_pipeline(window.internal.pipeline_id); txn.generate_frame(); - window.internal.api.send_transaction(window.internal.document_id, txn); + window.internal.api.send_transaction(window.internal.document_id, txn); window.renderer.as_mut().unwrap().update(); window.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index c6fc865a1..6d5b9a0e0 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -398,8 +398,6 @@ fn displaylist_handle_rect( let key = render_api.generate_image_key(); let external_image_id = ExternalImageId(texture.inner.get_id() as u64); - println!("pushing texture with ID: {:?}", external_image_id); - let data = ImageData::External(ExternalImageData { id: external_image_id, channel_index: 0, @@ -897,11 +895,10 @@ fn create_layout_constraints<'a>( use constraints::{SizeConstraint, Strength}; let mut layout_constraints = Vec::::new(); + /* let max_width = arena.get_wh_for_rectangle(rect_id, WidthOrHeight::Width) .unwrap_or(window_size.width as f32); - - println!("max width for rectangle with the ID {} is: {}", rect_id, max_width); - + */ layout_constraints.push(CssConstraint::Size((SizeConstraint::Width(200.0), Strength(STRONG)))); layout_constraints.push(CssConstraint::Size((SizeConstraint::Height(200.0), Strength(STRONG)))); diff --git a/src/widgets.rs b/src/widgets.rs index 3d30bdbd1..664828aa8 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -57,17 +57,22 @@ impl Svg { } pub fn dom(&self, window: &ReadOnlyWindow) -> Dom { +/* use glium::Surface; - + window.make_current(); let tex = window.create_texture(800, 600); - tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); -/* + { + let mut surface = tex.as_surface(); + surface.clear_color_and_depth((0.0, 1.0, 0.0, 1.0), 1.0); + } Dom::new(NodeType::Div) .with_class("__azul-native-svg") .with_child(Dom::new(NodeType::GlTexture(tex))) .with_id("my_opengl_id") */ - Dom::new(NodeType::GlTexture(tex)) + // Dom::new(NodeType::GlTexture(tex)) + Dom::new(NodeType::Div) + .with_class("__azul-native-button") } } diff --git a/src/window.rs b/src/window.rs index f6cb23bb5..ae059a863 100644 --- a/src/window.rs +++ b/src/window.rs @@ -85,6 +85,19 @@ impl ReadOnlyWindow { let tex = Texture2d::empty(&*self.inner, width, height).unwrap(); Texture::new(tex) } + + pub fn make_current(&self) { + unsafe { + use glium::glutin::GlContext; + self.inner.gl_window().make_current().unwrap(); + } + } +/* + pub fn undbind_fb(&self) { + let gl = self.inner. + gl.bind_framebuffer(gl::FRAMEBUFFER, 0); + } +*/ } pub struct WindowInfo { @@ -455,11 +468,6 @@ pub(crate) struct WindowInternal { pub(crate) document_id: DocumentId, } -/* -pub(crate) layout_size: LayoutSize, -pub(crate) framebuffer_size: DeviceUintSize, -*/ - impl Window { /// Creates a new window @@ -507,8 +515,16 @@ impl Window { opengl_version: (3, 2), opengles_version: (3, 0), }) - .with_gl_profile(GlProfile::Core) - .with_gl_debug_flag(false); + .with_gl_profile(GlProfile::Core); + + #[cfg(debug_assertions)] { + builder = builder.with_gl_debug_flag(true); + } + + #[cfg(not(debug_assertions))] { + builder = builder.with_gl_debug_flag(false); + } + if vsync { builder = builder.with_vsync(true); } @@ -518,41 +534,21 @@ impl Window { builder } - // For some reason, there is GL_INVALID_OPERATION stuff going on, - // but the display works fine. TODO: report this to glium - // Only create a context with VSync and SRGB if the context creation works let gl_window = GlWindow::new(window.clone(), create_context_builder(true, true), &events_loop) .or_else(|_| GlWindow::new(window.clone(), create_context_builder(true, false), &events_loop)) .or_else(|_| GlWindow::new(window.clone(), create_context_builder(false, true), &events_loop)) .or_else(|_| GlWindow::new(window, create_context_builder(false, false), &events_loop))?; - let hidpi_factor = gl_window.hidpi_factor(); - if let Some(WindowPosition { x, y }) = options.state.position { gl_window.window().set_position(x as i32, y as i32); } + #[cfg(debug_assertions)] + let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; + #[cfg(not(debug_assertions))] let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; - unsafe { - display.gl_window().make_current()?; - } - - // draw the first frame in the background color - use glium::Surface; - let mut frame = display.draw(); - if let Some(depth) = options.clear_depth { - if let Some(stencil) = options.clear_stencil { - frame.clear_all_srgb((options.background.r, options.background.g, options.background.b, options.background.a), depth, stencil); - } - frame.clear_color_srgb_and_depth((options.background.r, options.background.g, options.background.b, options.background.a), depth); - } else if let Some(stencil) = options.clear_stencil { - frame.clear_color_srgb_and_stencil((options.background.r, options.background.g, options.background.b, options.background.a), stencil); - } - frame.clear_color_srgb(options.background.r, options.background.g, options.background.b, options.background.a); - frame.finish()?; - let device_pixel_ratio = display.gl_window().hidpi_factor(); // this exists because RendererOptions isn't Clone-able @@ -567,7 +563,8 @@ impl Window { enable_subpixel_aa: true, enable_aa: true, clear_color: clear_color, - enable_render_on_scroll: false, + enable_render_on_scroll: true, + enable_scrollbars: true, cached_programs: Some(ProgramCache::new(None)), renderer_kind: if native { RendererKind::Native From 66b849ad45c2eec050db84faf4c2f2e6b9d29100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 06:25:31 +0200 Subject: [PATCH 094/868] Initial OpenGL drawing working, added window.unbind_framebuffer() --- src/widgets.rs | 19 ++++++------------- src/window.rs | 21 ++++++++++++++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/widgets.rs b/src/widgets.rs index 664828aa8..a1e76fcb5 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -57,22 +57,15 @@ impl Svg { } pub fn dom(&self, window: &ReadOnlyWindow) -> Dom { -/* + use glium::Surface; + window.make_current(); let tex = window.create_texture(800, 600); - { - let mut surface = tex.as_surface(); - surface.clear_color_and_depth((0.0, 1.0, 0.0, 1.0), 1.0); - } - Dom::new(NodeType::Div) - .with_class("__azul-native-svg") - .with_child(Dom::new(NodeType::GlTexture(tex))) - .with_id("my_opengl_id") -*/ - // Dom::new(NodeType::GlTexture(tex)) - Dom::new(NodeType::Div) - .with_class("__azul-native-button") + tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); + window.unbind_framebuffer(); + + Dom::new(NodeType::GlTexture(tex)) } } diff --git a/src/window.rs b/src/window.rs index ae059a863..3833602ce 100644 --- a/src/window.rs +++ b/src/window.rs @@ -92,12 +92,23 @@ impl ReadOnlyWindow { self.inner.gl_window().make_current().unwrap(); } } -/* - pub fn undbind_fb(&self) { - let gl = self.inner. - gl.bind_framebuffer(gl::FRAMEBUFFER, 0); + + pub fn unbind_framebuffer(&self) { + let gl = match self.inner.gl_window().get_api() { + glutin::Api::OpenGl => unsafe { + gl::GlFns::load_with(|symbol| + self.inner.gl_window().get_proc_address(symbol) as *const _) + }, + glutin::Api::OpenGlEs => unsafe { + gl::GlesFns::load_with(|symbol| + self.inner.gl_window().get_proc_address(symbol) as *const _) + }, + glutin::Api::WebGl => unreachable!(), + }; + + gl.bind_framebuffer(gl::FRAMEBUFFER, 0); } -*/ + } pub struct WindowInfo { From 79182c091b337b39e7778ca180d4f0ec34a3a2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 20:23:43 +0200 Subject: [PATCH 095/868] Test if GitHub issue templates support auto-labeling --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + .github/ISSUE_TEMPLATE/question.md | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a9be897e4..5ef1be640 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,7 @@ --- name: Bug report about: Report a problem encountered while using azul +label: bug --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e98d3d041..0b09efb40 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,7 @@ --- name: Feature request about: Suggest an idea to improve azul +label: feature-request --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index da52d283b..04153cca1 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,6 +1,7 @@ --- name: Question about: Ask a question / problem when using azul +label: question --- From 32a3622d8a0430ef5136b6baa7adfb2c3b006257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 23:11:16 +0200 Subject: [PATCH 096/868] Implemented the SVG cache as a widget, prepared svg drawing --- examples/debug.rs | 24 ++++++++------- src/app.rs | 17 +++++----- src/app_state.rs | 35 ++------------------- src/display_list.rs | 24 +++++++-------- src/lib.rs | 2 +- src/resources.rs | 43 +++----------------------- src/svg.rs | 75 ++++++++++++++------------------------------- src/traits.rs | 4 +-- src/widgets.rs | 65 +++++++++++++++++++++++++++++++++++++-- src/window.rs | 26 ++++++++++++++++ 10 files changed, 154 insertions(+), 161 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7e09e216c..f6760fc8a 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -10,26 +10,29 @@ const TEST_SVG: &[u8] = include_bytes!("../assets/svg/test.svg"); #[derive(Debug)] pub struct MyAppData { - // Your app data goes here - pub my_data: u32, - // SVG IDs - pub my_svg_ids: Vec, + pub svg: Option<(SvgCache, Vec)>, } impl Layout for MyAppData { fn layout(&self, info: WindowInfo) -> Dom { - Svg::empty().dom(&info.window) + if let Some((svg_cache, svg_layers)) = &self.svg { + Svg::with_layers(svg_layers).dom(&info.window, &svg_cache) + } else { + // TODO: This currently crashes because of the double-redraw bug + Dom::new(NodeType::Div) + .with_class("__azul-native-button") + .with_callback(On::MouseUp, Callback(my_button_click_handler)) + } } } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { // Load and parse the SVG file, register polygon data as IDs -/* - let mut svg_ids = app_state.add_svg(TEST_SVG).unwrap(); - app_state.data.modify(|data| data.my_svg_ids.append(&mut svg_ids)); -*/ + let mut svg_cache = SvgCache::empty(); + let svg_layers= svg_cache.add_svg(TEST_SVG).unwrap(); + app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); UpdateScreen::Redraw } @@ -39,8 +42,7 @@ fn main() { let css = Css::new_from_string(TEST_CSS).unwrap(); let my_app_data = MyAppData { - my_data: 0, - my_svg_ids: Vec::new(), + svg: None, }; let mut app = App::new(my_app_data); diff --git a/src/app.rs b/src/app.rs index 351ef9b4d..c2f5be1b8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -147,25 +147,26 @@ impl<'a, T: Layout> App<'a, T> { let time_start = Instant::now(); let mut closed_windows = Vec::::new(); - for (idx, ref mut window) in self.windows.iter_mut().enumerate() { + 'window_loop: for (idx, ref mut window) in self.windows.iter_mut().enumerate() { /* unsafe { use glium::glutin::GlContext; window.display.gl_window().make_current().unwrap(); } */ - // TODO: move this somewhere else - // let svg_shader = &self.app_state.resources.svg_registry.init_shader(&window.display); - let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); - window.events_loop.poll_events(|event| { + let mut events = Vec::new(); + window.events_loop.poll_events(|e| events.push(e)); + + for event in events { let should_close = process_event(event, &mut frame_event_info); if should_close { closed_windows.push(idx); + continue 'window_loop; } - }); + } if frame_event_info.is_resize_event { // This is a hack because during a resize event, winit eats the "awakened" @@ -180,7 +181,7 @@ impl<'a, T: Layout> App<'a, T> { if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; if let Some(i) = force_redraw_cache.get_mut(idx) { - *i -= 1; + i.saturating_sub(1); } } @@ -548,7 +549,7 @@ fn render( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, - app_resources: &mut AppResources, + app_resources: &mut AppResources, has_window_size_changed: bool) { use webrender::api::*; diff --git a/src/app_state.rs b/src/app_state.rs index 10f15ba11..27ad1848c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -12,7 +12,7 @@ use font::FontError; use std::collections::hash_map::Entry::*; use FastHashMap; use std::sync::{Arc, Mutex}; -use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; +use svg::{SvgLayerId, SvgLayer, SvgParseError}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application @@ -30,7 +30,7 @@ pub struct AppState<'a, T: Layout> { /// ``` pub windows: Vec, /// Fonts and images that are currently loaded into the app - pub(crate) resources: AppResources<'a, T>, + pub(crate) resources: AppResources<'a>, /// Currently running deamons (polling functions) pub(crate) deamons: FastHashMap UpdateScreen>, /// Currently running tasks (asynchronous functions running on a different thread) @@ -189,37 +189,6 @@ impl<'a, T: Layout> AppState<'a, T> { self.deamons.remove(id.as_ref()).is_some() } - /// A "SvgLayer" represents one or more shapes that get drawn using the same style (necessary for batching). - /// Adds the SVG layer as a resource to the internal resources, the returns the ID, which you can use in the - /// `NodeType::SvgLayer` to draw the SVG layer. - pub fn add_svg_layer(&mut self, layer: SvgLayer) - -> SvgLayerId - { - self.resources.add_svg_layer(layer) - } - - /// Deletes a specific shape from the app-internal resources. When drawing with an invalid ID, the app will crash - /// (in debug mode) or simply not draw the shape (in release mode) - pub fn delete_svg_layer(&mut self, svg_id: SvgLayerId) - { - self.resources.delete_svg_layer(svg_id); - } - - /// Clears all crate-internal resources and shapes. Use with care. - pub fn clear_all_svg_layers(&mut self) - { - self.resources.clear_all_svg_layers(); - } - - /// Parses an input source, parses the SVG, adds the shapes as layers into - /// the registry, returns the IDs of the added shapes, in the order that - /// they appeared in the SVG text. - pub fn add_svg(&mut self, input: R) - -> Result, SvgParseError> - { - self.resources.add_svg(input) - } - /// Run all currently registered deamons pub(crate) fn run_all_deamons(&self) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; diff --git a/src/display_list.rs b/src/display_list.rs index 6d5b9a0e0..2317dcb16 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -106,7 +106,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { /// Looks if any new images need to be uploaded and stores the in the image resources fn update_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { Self::update_image_resources(api, app_resources, resource_updates); @@ -115,7 +115,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { fn update_image_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { use images::{ImageState, ImageInfo}; @@ -165,7 +165,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // have two HashMaps that need to be updated fn update_font_resources( api: &RenderApi, - app_resources: &mut AppResources, + app_resources: &mut AppResources, resource_updates: &mut Vec) { use font::FontState; @@ -215,7 +215,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi, mut has_window_size_changed: bool, window_size: &WindowSize) @@ -302,13 +302,13 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } -fn displaylist_handle_rect( +fn displaylist_handle_rect( builder: &mut DisplayListBuilder, rect: &DisplayRectangle, html_node: &NodeType, bounds: TypedRect, full_screen_rect: TypedRect, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi, resource_updates: &mut Vec) { @@ -435,12 +435,12 @@ fn push_rect( } #[inline] -fn push_text( +fn push_text( info: &PrimitiveInfo, text: &str, builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, resource_updates: &mut Vec) @@ -702,12 +702,12 @@ fn push_box_shadow( } #[inline] -fn push_background( +fn push_background( info: &PrimitiveInfo, bounds: &TypedRect, builder: &mut DisplayListBuilder, background: &Background, - app_resources: &AppResources) + app_resources: &AppResources) { match background { Background::RadialGradient(gradient) => { @@ -768,11 +768,11 @@ fn push_border( } #[inline] -fn push_font( +fn push_font( font_id: &css_parser::Font, font_size_app_units: Au, resource_updates: &mut Vec, - app_resources: &mut AppResources, + app_resources: &mut AppResources, render_api: &RenderApi) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 05c2097ae..b6c96fd97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub mod prelude { ExtendMode, PixelValue, PercentageValue, }; - pub use svg::{SvgLayerId, SvgLayer}; + pub use svg::{SvgLayerId, SvgLayer, SvgCache}; // from the extern crate image pub use image::ImageError; diff --git a/src/resources.rs b/src/resources.rs index 62f3d73a6..0e3b4a5a3 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -14,7 +14,7 @@ use std::collections::hash_map::Entry::*; use app_units::Au; use css_parser; use css_parser::Font::ExternalFont; -use svg::{SvgLayerId, SvgLayer, SvgParseError, SvgRegistry}; +use svg::{SvgLayerId, SvgLayer, SvgParseError}; use text_cache::TextId; /// Font and image keys @@ -28,7 +28,7 @@ use text_cache::TextId; /// Images and fonts can be references across window contexts /// (not yet tested, but should work). #[derive(Clone)] -pub(crate) struct AppResources<'a, T: Layout> { +pub(crate) struct AppResources<'a> { /// Image cache pub(crate) images: FastHashMap, // Fonts are trickier to handle than images. @@ -41,18 +41,13 @@ pub(crate) struct AppResources<'a, T: Layout> { // the font instance key (if there is any). If there is no font instance key, // we first need to create one. pub(crate) fonts: FastHashMap>, - /// Stores the polygon data for all SVGs. Polygons can be shared across windows - /// without duplicating the data. This doesn't store any rendering-related data, - /// only the polygons - pub(crate) svg_registry: SvgRegistry, /// Stores long texts across frames pub(crate) text_registry: TextRegistry, } -impl<'a, T: Layout> Default for AppResources<'a, T> { +impl<'a> Default for AppResources<'a> { fn default() -> Self { Self { - svg_registry: SvgRegistry::default(), fonts: FastHashMap::default(), font_data: FastHashMap::default(), images: FastHashMap::default(), @@ -61,7 +56,7 @@ impl<'a, T: Layout> Default for AppResources<'a, T> { } } -impl<'a, T: Layout> AppResources<'a, T> { +impl<'a> AppResources<'a> { /// See `AppState::add_image()` pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) @@ -153,27 +148,6 @@ impl<'a, T: Layout> AppResources<'a, T> { } } - /// A "SvgLayer" represents one or more shapes that get drawn using the same style (necessary for batching). - /// Adds the SVG layer as a resource to the internal resources, the returns the ID, which you can use in the - /// `NodeType::SvgLayer` to draw the SVG layer. - pub(crate) fn add_svg_layer(&mut self, layer: SvgLayer) - -> SvgLayerId - { - self.svg_registry.add_layer(layer) - } - - /// See `AppState::` - pub(crate) fn delete_svg_layer(&mut self, svg_id: SvgLayerId) - { - self.svg_registry.delete_layer(svg_id); - } - - /// Clears all crate-internal resources and shapes. Use with care. - pub(crate) fn clear_all_svg_layers(&mut self) - { - self.svg_registry.clear_all_layers(); - } - pub(crate) fn add_text>(&mut self, text: S) -> TextId { @@ -187,13 +161,4 @@ impl<'a, T: Layout> AppResources<'a, T> { pub(crate) fn clear_all_texts(&mut self) { self.text_registry.clear_all_texts(); } - - /// Parses an input source, parses the SVG, adds the shapes as layers into - /// the registry, returns the IDs of the added shapes, in the order that - /// they appeared in the SVG text. - pub(crate) fn add_svg(&mut self, input: R) - -> Result, SvgParseError> - { - self.svg_registry.add_svg(input) - } } \ No newline at end of file diff --git a/src/svg.rs b/src/svg.rs index fd3a0158b..42e556d24 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; +use glium::backend::Facade; use std::rc::Rc; use glium::DrawParameters; use glium::IndexBuffer; @@ -49,44 +51,49 @@ pub struct SvgLayerId(usize); #[derive(Debug, Clone)] pub struct SvgShader { - program: Rc, + pub program: Rc, } impl SvgShader { - pub fn new(display: &Display) -> Self { + pub fn new(display: &F) -> Self { Self { program: Rc::new(Program::from_source(display, SVG_VERTEX_SHADER, SVG_FRAGMENT_SHADER, None).unwrap()), } } } -#[derive(Clone)] -pub(crate) struct SvgRegistry { +pub struct SvgCache { // note: one "layer" merely describes one or more polygons that have the same style - layers: FastHashMap>, - shader: Option, + pub layers: FastHashMap>, + shader: Mutex>, } -impl Default for SvgRegistry { +impl Default for SvgCache { fn default() -> Self { Self { layers: FastHashMap::default(), - shader: None, + shader: Mutex::new(None), } } } -impl SvgRegistry { - /// Builds and compiles the shader if the shader isn't already present - pub(crate) fn init_shader(&mut self, display: &Display) -> SvgShader { - if self.shader.is_none() { - self.shader = Some(SvgShader::new(display)); +impl SvgCache { + + pub fn empty() -> Self { + Self::default() + } + + /// Builds and compiles the SVG shader if the shader isn't already present + pub fn init_shader(&self, display: &F) -> SvgShader { + let mut shader_lock = self.shader.lock().unwrap(); + if shader_lock.is_none() { + *shader_lock = Some(SvgShader::new(display)); } - self.shader.as_ref().and_then(|s| Some(s.clone())).unwrap() + shader_lock.as_ref().and_then(|s| Some(s.clone())).unwrap() } } -impl fmt::Debug for SvgRegistry { +impl fmt::Debug for SvgCache { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for layer in self.layers.keys() { write!(f, "{:?}", layer)?; @@ -95,7 +102,7 @@ impl fmt::Debug for SvgRegistry { } } -impl SvgRegistry { +impl SvgCache { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { let new_svg_id = SvgLayerId(unsafe { SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst) }); @@ -158,42 +165,6 @@ impl Clone for SvgLayer { } } -#[derive(Debug, Copy, Clone)] -pub struct SvgWorldPixel; - -#[derive(Debug, Copy, Clone)] -pub(crate) struct SvgVert { - pub(crate) xy: (f32, f32), -} - -implement_vertex!(SvgVert, xy); - -pub(crate) fn draw_polygons( - target: &mut Texture2d, - shader: &SvgShader, - color: &ColorF, - bbox: &TypedRect, - vbuf: &VertexBuffer, - ibuf: &IndexBuffer, - z_index: f32) -{ - use glium::Surface; - - let draw_options = DrawParameters { - primitive_restart_index: true, - .. Default::default() - }; - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: (color.r, color.g, color.b, color.a), - }; - - target.as_surface().draw(vbuf, ibuf, &*shader.program, &uniforms, &draw_options).unwrap(); -} - pub enum SvgCallbacks { // No callbacks for this layer None, diff --git a/src/traits.rs b/src/traits.rs index afdc4dcce..ea2cd372e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -57,11 +57,11 @@ pub trait IntoParsedCssProperty<'a> { pub trait ModifyAppState { /// Modifies the app state and then returns if the modification was successful /// Takes a FnMut that modifies the state - fn modify(&self, closure: F) -> bool where F: FnMut(&mut T); + fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T); } impl ModifyAppState for Arc> { - fn modify(&self, mut closure: F) -> bool where F: FnMut(&mut T) { + fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T) { match self.lock().as_mut() { Ok(lock) => { closure(&mut *lock); true }, Err(_) => false, diff --git a/src/widgets.rs b/src/widgets.rs index a1e76fcb5..07569df7a 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,5 +1,6 @@ #![allow(non_snake_case)] +use svg::SvgCache; use svg::SvgLayerId; use window::ReadOnlyWindow; use traits::GetDom; @@ -7,6 +8,8 @@ use traits::Layout; use dom::{Dom, NodeType}; use images::ImageId; +// --- button + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Button { pub content: ButtonContent, @@ -45,30 +48,86 @@ impl GetDom for Button { } } +// --- svg + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Svg { pub layers: Vec, } +#[derive(Debug, Copy, Clone)] +pub(crate) struct SvgVert { + pub(crate) xy: (f32, f32), +} + +implement_vertex!(SvgVert, xy); + +#[derive(Debug, Copy, Clone)] +pub struct SvgWorldPixel; + +use glium::{Texture2d, draw_parameters::DrawParameters, + index::PrimitiveType, IndexBuffer, Surface}; +use std::sync::Mutex; +use svg::SvgShader; +use webrender::api::ColorF; +use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; + impl Svg { + // todo: remove this later pub fn empty() -> Self { Self { layers: Vec::new() } } - pub fn dom(&self, window: &ReadOnlyWindow) -> Dom { + pub fn with_layers(layers: &Vec) -> Self { + Self { layers: layers.clone() } + } - use glium::Surface; + pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom { window.make_current(); let tex = window.create_texture(800, 600); - tex.as_surface().clear_color(1.0, 0.0, 0.0, 1.0); + tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); + + // TODO: cache the vertex buffers / index buffers + let vertex_buffer = window.make_vertex_buffer(&[ + SvgVert { xy: (500.0, 400.0) }, + SvgVert { xy: (500.0, 0.0) }, + SvgVert { xy: (0.0, 300.0) }, + ]).unwrap(); + + let index_buffer = window.make_index_buffer(PrimitiveType::TrianglesList, &[0_u32, 1, 2]).unwrap(); + + let draw_options = DrawParameters { + primitive_restart_index: true, + .. Default::default() + }; + + let z_index: f32 = 0.5; + let bbox= Svg::make_bbox((0.0, 0.0), (800.0, 600.0)); + let color = ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }; + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: (color.r, color.g, color.b, color.a), + }; + + tex.as_surface().draw(&vertex_buffer, &index_buffer, &svg_cache.init_shader(window).program, &uniforms, &draw_options).unwrap(); + window.unbind_framebuffer(); Dom::new(NodeType::GlTexture(tex)) } + + pub fn make_bbox((origin_x, origin_y): (f32, f32), (size_x, size_y): (f32, f32)) -> TypedRect { + TypedRect::::new(TypedPoint2D::new(origin_x, origin_y), TypedSize2D::new(size_x, size_y)) + } } +// --- label + #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Label { pub text: String, diff --git a/src/window.rs b/src/window.rs index 3833602ce..a40a2327a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -76,6 +76,10 @@ impl Facade for ReadOnlyWindow { } } +use glium::{Vertex, VertexBuffer, IndexBuffer, index::PrimitiveType}; +use glium::vertex::BufferCreationError as VertexBufferCreationError; +use glium::index::BufferCreationError as IndexBufferCreationError; + impl ReadOnlyWindow { // Since webrender is asynchronous, we can't let the user draw // directly onto the frame or the texture since that has to be timed @@ -109,6 +113,16 @@ impl ReadOnlyWindow { gl.bind_framebuffer(gl::FRAMEBUFFER, 0); } + pub fn make_vertex_buffer(&self, data: &[T]) + -> Result, VertexBufferCreationError> + { + VertexBuffer::new(self, data) + } + pub fn make_index_buffer(&self, prim_type: PrimitiveType, data: &[u32]) + -> Result, IndexBufferCreationError> + { + IndexBuffer::new(self, prim_type, data) + } } pub struct WindowInfo { @@ -145,6 +159,18 @@ pub struct WindowEvent { pub cursor_in_viewport: (f32, f32), } +impl WindowEvent { + // Mock window event, used for testing / calling callbacks without a window + pub fn mock() -> Self { + Self { + window: 0, + number_of_previous_siblings: None, + cursor_relative_to_item: (0.0, 0.0), + cursor_in_viewport: (0.0, 0.0), + } + } +} + /// Options on how to initially create the window #[derive(Debug, Clone)] pub struct WindowCreateOptions { From e5f0aad3b1fab92c47baef2145e783809c386c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 23:19:26 +0200 Subject: [PATCH 097/868] Try fixing codecov again --- .travis.yml | 51 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index a26956816..0a4f2413d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,37 @@ language: rust -sudo: false - cache: cargo +rust: + - stable + - beta + - nightly + os: - linux - osx -rust: - - stable - matrix: fast_finish: true -# Required for kcov +# We can't test OpenGL 3.2 on Travis, the shader compilation fails +# because glium does a check first if it has a OGL 3.2 context +script: + - cargo clean + - cargo build --verbose --examples + - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" + +matrix: + allow_failures: + - rust: nightly + - rust: beta + +# before_install: +# - sudo apt-get update + +install: + - PATH=$PATH:/home/travis/.cargo/bin + addons: apt: update: true @@ -28,23 +45,21 @@ addons: - libiberty-dev - zlib1g-dev -# We can't test OpenGL 3.2 on Travis, the shader compilation fails -# because glium does a check first if it has a OGL 3.2 context -script: - - cargo build --verbose --examples - - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" - after_success: | - wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && - tar xzf master.tar.gz && - cd kcov-master && + wget https://github.com/SimonKagstrom/kcov/archive/v34.tar.gz && + tar xzf v34.tar.gz && + cd kcov-34 && mkdir build && cd build && cmake .. && make && sudo make install && cd ../.. && - rm -rf kcov-master && - kcov –-coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo,/usr/lib --verify target/debug/azul-* && + rm -rf kcov-34 && + find target/debug -maxdepth 1 -name 'azul-*' -type f | while read file; do + [ -x $file ] || continue; + mkdir -p "target/cov/$(basename $file)"; + kcov --exclude-pattern=/.cargo,/usr/lib --include-path="$(pwd)" --verify "target/cov/$(basename $file)" "$file"; + done && bash <(curl -s https://codecov.io/bash) && - echo "Uploaded code coverage" \ No newline at end of file + echo "Uploaded code coverage" From 12fe975af0431f3cea420b2b6c414cde993cefd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 7 Jun 2018 23:44:03 +0200 Subject: [PATCH 098/868] Fixed underflow bug (with redrawing window not updating itself) --- src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index c2f5be1b8..8dd18310a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -168,7 +168,7 @@ impl<'a, T: Layout> App<'a, T> { } } - if frame_event_info.is_resize_event { + if frame_event_info.is_resize_event || frame_event_info.should_redraw_window { // This is a hack because during a resize event, winit eats the "awakened" // event. So what we do is that we call the layout-and-render again, to // trigger a second "awakened" event. So when the window is resized, the @@ -181,7 +181,7 @@ impl<'a, T: Layout> App<'a, T> { if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; if let Some(i) = force_redraw_cache.get_mut(idx) { - i.saturating_sub(1); + if *i > 0 { *i -= 1 }; } } From 442d8ac3dd26cab303f74915f494b6cadd1f4a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 8 Jun 2018 03:48:18 +0200 Subject: [PATCH 099/868] Initial SVG display working --- assets/svg/test.svg | 74 ++++++++------------ examples/debug.rs | 1 - src/app.rs | 20 +++--- src/svg.rs | 160 ++++++++++++++++++++++++++++++++++++++++++-- src/widgets.rs | 77 +++++++++++---------- src/window.rs | 23 ++----- 6 files changed, 240 insertions(+), 115 deletions(-) diff --git a/assets/svg/test.svg b/assets/svg/test.svg index ce83f3fa3..c355c65ae 100644 --- a/assets/svg/test.svg +++ b/assets/svg/test.svg @@ -9,9 +9,9 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="437.6221" - height="405.85663" - viewBox="0 0 115.78751 107.3829" + width="800" + height="600" + viewBox="0 0 211.66666 158.75" version="1.1" id="svg8" inkscape:version="0.92.3 (2405546, 2018-03-11)" @@ -25,9 +25,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="2" - inkscape:cx="92.84402" - inkscape:cy="212.12355" + inkscape:zoom="1.4142136" + inkscape:cx="298.27505" + inkscape:cy="496.31106" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" @@ -57,69 +57,49 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" - transform="translate(-47.020246,-94.435636)"> + transform="translate(-47.020246,-43.068536)"> + x="106.3247" + y="52.933861" + rx="9.9604597" + ry="9.9604597" /> + inkscape:transform-center-y="-2.0813023" + d="M 59.49538,150.22576 66.09703,82.596705 121.36468,122.12843 Z" + id="path956" + inkscape:connector-curvature="0" /> + inkscape:transform-center-y="-1.8924511" + d="M 146.34711,142.81301 132.37314,128.51798 135.42458,148.2742 126.55196,130.36063 123.34904,150.09286 120.44627,130.31425 111.30253,148.09096 114.65375,128.38338 100.46423,142.46446 109.74141,124.75703 91.895077,133.76413 106.1901,119.79016 86.433883,122.8416 104.34745,113.96898 84.615224,110.76606 104.39383,107.86329 86.617123,98.719547 106.32471,102.07077 92.243621,87.88125 109.95106,97.158431 100.94396,79.3121 114.91793,93.607122 111.86648,73.850905 120.73911,91.764473 123.94202,72.032246 126.8448,91.810855 135.98854,74.034146 132.63732,93.741726 146.82684,79.660644 137.54966,97.368081 155.39599,88.360979 141.10096,102.33495 160.85718,99.283503 142.94361,108.15613 162.67584,111.35904 142.89723,114.26182 160.67394,123.40556 140.96636,120.05434 155.04744,134.24386 137.34001,124.96668 Z" + id="path1556" + inkscape:connector-curvature="0" /> Hello
diff --git a/examples/debug.rs b/examples/debug.rs index f6760fc8a..4b335a0a7 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -20,7 +20,6 @@ impl Layout for MyAppData { if let Some((svg_cache, svg_layers)) = &self.svg { Svg::with_layers(svg_layers).dom(&info.window, &svg_cache) } else { - // TODO: This currently crashes because of the double-redraw bug Dom::new(NodeType::Div) .with_class("__azul-native-button") .with_callback(On::MouseUp, Callback(my_button_click_handler)) diff --git a/src/app.rs b/src/app.rs index 8dd18310a..f68c5aeab 100644 --- a/src/app.rs +++ b/src/app.rs @@ -168,16 +168,6 @@ impl<'a, T: Layout> App<'a, T> { } } - if frame_event_info.is_resize_event || frame_event_info.should_redraw_window { - // This is a hack because during a resize event, winit eats the "awakened" - // event. So what we do is that we call the layout-and-render again, to - // trigger a second "awakened" event. So when the window is resized, the - // layout function is called twice (the first event will be eaten by winit) - // - // This is a reported bug and should be fixed somewhere in July - force_redraw_cache[idx] = 3; - } - if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; if let Some(i) = force_redraw_cache.get_mut(idx) { @@ -189,6 +179,16 @@ impl<'a, T: Layout> App<'a, T> { Self::do_hit_test_and_call_callbacks(window, window_id, &mut frame_event_info, &ui_state_cache, &mut self.app_state); } + if frame_event_info.is_resize_event || frame_event_info.should_redraw_window { + // This is a hack because during a resize event, winit eats the "awakened" + // event. So what we do is that we call the layout-and-render again, to + // trigger a second "awakened" event. So when the window is resized, the + // layout function is called twice (the first event will be eaten by winit) + // + // This is a reported bug and should be fixed somewhere in July + force_redraw_cache[idx] = 2; + } + // Update the window state that we got from the frame event (updates window dimensions and DPI) window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user diff --git a/src/svg.rs b/src/svg.rs index 42e556d24..35a33ce56 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -20,14 +20,17 @@ use svg_crate::parser::Error as SvgError; use std::io::Error as IoError; use std::fmt; use euclid::TypedRect; +use lyon::tessellation::VertexBuffers; +use std::cell::UnsafeCell; /// In order to store / compare SVG files, we have to -pub(crate) static mut SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); +pub(crate) static SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); const SVG_VERTEX_SHADER: &str = " #version 130 in vec2 xy; + in vec2 normal; uniform vec2 bbox_origin; uniform vec2 bbox_size; @@ -41,8 +44,10 @@ const SVG_FRAGMENT_SHADER: &str = " #version 130 uniform vec4 color; + out vec4 out_color; + void main() { - gl_FragColor = color; + out_color = color; } "; @@ -64,7 +69,10 @@ impl SvgShader { pub struct SvgCache { // note: one "layer" merely describes one or more polygons that have the same style - pub layers: FastHashMap>, + layers: FastHashMap>, + // Stores the vertices and indices necessary for drawing. Must be synchronized with the `layers` + gpu_ready_to_upload_cache: FastHashMap, Vec)>, + vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, shader: Mutex>, } @@ -72,6 +80,8 @@ impl Default for SvgCache { fn default() -> Self { Self { layers: FastHashMap::default(), + gpu_ready_to_upload_cache: FastHashMap::default(), + vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), shader: Mutex::new(None), } } @@ -91,6 +101,44 @@ impl SvgCache { } shader_lock.as_ref().and_then(|s| Some(s.clone())).unwrap() } + + /// Note: panics if the ID isn't found. + /// + /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` + /// in sync, a panic should never happen + pub fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + -> &'a (VertexBuffer, IndexBuffer) + { + use std::collections::hash_map::Entry::*; + use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; + + // First, we need the SvgCache to call this function immutably, otherwise we can't + // use it from the Layout::layout() function + // + // Rust does also not "understand" that we want to return a reference into + // self.vertex_index_buffer_cache, so the reference that we are returning lives as + // long as the self.gpu_ready_to_upload_cache (at least until it's removed) + + // We need to use UnsafeCell here - when using a regular RefCell, Rust thinks we + // are destroying the reference after the borrow, but that isn't true. + + let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; + let rnotmut = &self.gpu_ready_to_upload_cache; + + rmut.entry(*id).or_insert_with(|| { + let (vbuf, ibuf) = rnotmut.get(id).as_ref().unwrap(); + let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); + (vertex_buffer, index_buffer) + }) + } + + pub fn get_style(&self, id: &SvgLayerId) + -> SvgStyle + { + self.layers.get(id).as_ref().unwrap().style + } + } impl fmt::Debug for SvgCache { @@ -105,17 +153,25 @@ impl fmt::Debug for SvgCache { impl SvgCache { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { - let new_svg_id = SvgLayerId(unsafe { SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst) }); + let new_svg_id = SvgLayerId(SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst)); + let (vertex_buf, index_buf) = tesselate_layer_data(&layer.data); self.layers.insert(new_svg_id, layer); + self.gpu_ready_to_upload_cache.insert(new_svg_id, (vertex_buf, index_buf)); new_svg_id } pub fn delete_layer(&mut self, svg_id: SvgLayerId) { self.layers.remove(&svg_id); + self.gpu_ready_to_upload_cache.remove(&svg_id); + let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; + rmut.remove(&svg_id); } pub fn clear_all_layers(&mut self) { self.layers.clear(); + self.gpu_ready_to_upload_cache.clear(); + let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; + rmut.clear(); } /// Parses an input source, parses the SVG, adds the shapes as layers into @@ -129,6 +185,25 @@ impl SvgCache { } } +fn tesselate_layer_data(layer_data: &[SvgLayerType]) -> (Vec, Vec) { + const GL_RESTART_INDEX: u32 = ::std::u32::MAX; + + let mut last_index = 0; + let mut vertex_buf = Vec::::new(); + let mut index_buf = Vec::::new(); + + for layer in layer_data { + let VertexBuffers { vertices, indices } = layer.tesselate(); + let vertices_len = vertices.len(); + vertex_buf.extend(vertices.into_iter()); + index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); + index_buf.push(GL_RESTART_INDEX); + last_index += vertices_len; + } + + (vertex_buf, index_buf) +} + #[derive(Debug)] pub enum SvgParseError { /// Syntax error in the Svg @@ -150,7 +225,7 @@ impl From for SvgParseError { } pub struct SvgLayer { - pub data: SvgLayerType, + pub data: Vec, pub callbacks: SvgCallbacks, pub style: SvgStyle, } @@ -274,6 +349,79 @@ pub enum SvgLayerType { Text(String), } +#[derive(Debug, Copy, Clone)] +pub struct SvgVert { + pub xy: (f32, f32), + pub normal: (f32, f32), +} + +implement_vertex!(SvgVert, xy, normal); + +#[derive(Debug, Copy, Clone)] +pub struct SvgWorldPixel; + +impl SvgLayerType { + pub fn tesselate(&self) + -> VertexBuffers + { + use self::SvgLayerType::*; + use lyon::tessellation::{VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator}; + use lyon::tessellation::basic_shapes::{fill_circle, fill_rounded_rectangle}; + use lyon::geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}; + use lyon::tessellation::basic_shapes::BorderRadii; + + let mut geometry = VertexBuffers::new(); + + match self { + Polygon(p) => { + let mut tessellator = FillTessellator::new(); + tessellator.tessellate_path( + p.path_iter(), + &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + }), + ).unwrap(); + }, + Circle(c) => { + fill_circle( + TypedPoint2D::new(c.center_x, c.center_y), c.radius, &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + }, + Rect(r) => { + fill_rounded_rectangle( + &TypedRect::new(TypedPoint2D::new(r.x, r.y), TypedSize2D::new(r.width, r.height)), + &BorderRadii { + top_left: r.rx, + top_right: r.rx, + bottom_left: r.rx, + bottom_right: r.rx, + }, + &FillOptions::default(), + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + }, + Text(_t) => { }, + } + + geometry + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub struct SvgCircle { pub center_x: f32, @@ -366,7 +514,7 @@ mod svg_to_lyon { }) .map(|(data, style)| { SvgLayer { - data: data, + data: vec![data], callbacks: SvgCallbacks::None, style: style, } diff --git a/src/widgets.rs b/src/widgets.rs index 07569df7a..7de592ee1 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -41,8 +41,8 @@ impl GetDom for Button { use self::ButtonContent::*; let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); button_root.add_child(match self.content { - Image(i) => Dom::new(NodeType::Image(i)), Text(s) => Dom::new(NodeType::Label(s)), + Image(i) => Dom::new(NodeType::Image(i)), }); button_root } @@ -53,68 +53,77 @@ impl GetDom for Button { #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Svg { pub layers: Vec, + pub enable_fxaa: bool, } -#[derive(Debug, Copy, Clone)] -pub(crate) struct SvgVert { - pub(crate) xy: (f32, f32), -} - -implement_vertex!(SvgVert, xy); - -#[derive(Debug, Copy, Clone)] -pub struct SvgWorldPixel; - use glium::{Texture2d, draw_parameters::DrawParameters, index::PrimitiveType, IndexBuffer, Surface}; use std::sync::Mutex; -use svg::SvgShader; -use webrender::api::ColorF; +use svg::{SvgVert, SvgWorldPixel, SvgShader}; +use webrender::api::{ColorU, ColorF}; use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; impl Svg { // todo: remove this later + #[inline] pub fn empty() -> Self { - Self { layers: Vec::new() } + Self { layers: Vec::new(), enable_fxaa: true } } + #[inline] pub fn with_layers(layers: &Vec) -> Self { - Self { layers: layers.clone() } + Self { layers: layers.clone(), enable_fxaa: true } + } + + #[inline] + pub fn with_fxaa(mut self, enable_fxaa: bool) -> Self { + self.enable_fxaa = enable_fxaa; + self } pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom { + const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; + window.make_current(); + let tex = window.create_texture(800, 600); tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); - // TODO: cache the vertex buffers / index buffers - let vertex_buffer = window.make_vertex_buffer(&[ - SvgVert { xy: (500.0, 400.0) }, - SvgVert { xy: (500.0, 0.0) }, - SvgVert { xy: (0.0, 300.0) }, - ]).unwrap(); - - let index_buffer = window.make_index_buffer(PrimitiveType::TrianglesList, &[0_u32, 1, 2]).unwrap(); - let draw_options = DrawParameters { primitive_restart_index: true, .. Default::default() }; let z_index: f32 = 0.5; - let bbox= Svg::make_bbox((0.0, 0.0), (800.0, 600.0)); - let color = ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 }; - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: (color.r, color.g, color.b, color.a), - }; + let bbox = Svg::make_bbox((0.0, 0.0), (800.0, 600.0)); + let shader = svg_cache.init_shader(window); + + { + let mut surface = tex.as_surface(); + + // TODO: cache the vertex buffers / index buffers + for layer_id in &self.layers { + let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); - tex.as_surface().draw(&vertex_buffer, &index_buffer, &svg_cache.init_shader(window).program, &uniforms, &draw_options).unwrap(); + let style = svg_cache.get_style(layer_id); + let color: ColorF = style.fill.unwrap_or(DEFAULT_COLOR).into(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: (color.r as f32, color.g as f32, color.b as f32, color.a as f32), + }; + + surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + } + } + + if self.enable_fxaa { + // TODO: apply FXAA shader + } window.unbind_framebuffer(); diff --git a/src/window.rs b/src/window.rs index a40a2327a..211b9d84e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -112,17 +112,6 @@ impl ReadOnlyWindow { gl.bind_framebuffer(gl::FRAMEBUFFER, 0); } - - pub fn make_vertex_buffer(&self, data: &[T]) - -> Result, VertexBufferCreationError> - { - VertexBuffer::new(self, data) - } - pub fn make_index_buffer(&self, prim_type: PrimitiveType, data: &[u32]) - -> Result, IndexBufferCreationError> - { - IndexBuffer::new(self, prim_type, data) - } } pub struct WindowInfo { @@ -553,12 +542,12 @@ impl Window { opengles_version: (3, 0), }) .with_gl_profile(GlProfile::Core); - + /* #[cfg(debug_assertions)] { builder = builder.with_gl_debug_flag(true); } - - #[cfg(not(debug_assertions))] { + */ + /*#[cfg(not(debug_assertions))] */ { builder = builder.with_gl_debug_flag(false); } @@ -581,9 +570,9 @@ impl Window { gl_window.window().set_position(x as i32, y as i32); } - #[cfg(debug_assertions)] - let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; - #[cfg(not(debug_assertions))] + // #[cfg(debug_assertions)] + // let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; + // #[cfg(not(debug_assertions))] let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; let device_pixel_ratio = display.gl_window().hidpi_factor(); From f141ce17c501c3fe8edd1db8a07428ae722a5c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 8 Jun 2018 04:06:25 +0200 Subject: [PATCH 100/868] Fix crash when texture is removed too early (due to winit resize bug, again) --- src/app.rs | 8 +++++++- src/compositor.rs | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index f68c5aeab..a5ff36c18 100644 --- a/src/app.rs +++ b/src/app.rs @@ -171,7 +171,13 @@ impl<'a, T: Layout> App<'a, T> { if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; if let Some(i) = force_redraw_cache.get_mut(idx) { - if *i > 0 { *i -= 1 }; + if *i > 0 { *i -= 1 }; + if *i == 0 { + use compositor::{TO_DELETE_TEXTURES, ACTIVE_GL_TEXTURES}; + let mut to_delete_lock = TO_DELETE_TEXTURES.lock().unwrap(); + let mut active_textures_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); + to_delete_lock.drain().for_each(|tex| { active_textures_lock.remove(&tex); }); + } } } diff --git a/src/compositor.rs b/src/compositor.rs index dc258d4dd..0f8a9a82d 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -3,7 +3,7 @@ //! into the UI. use dom::Texture; -use FastHashMap; +use {FastHashMap, FastHashSet}; use webrender::{ExternalImageHandler, ExternalImageSource}; use webrender::api::{ExternalImageId, TexelRect, DevicePixel}; use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}; @@ -26,6 +26,7 @@ lazy_static! { /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL /// texture) does not run until we remove the textures pub(crate) static ref ACTIVE_GL_TEXTURES: Mutex> = Mutex::new(FastHashMap::default()); + pub(crate) static ref TO_DELETE_TEXTURES: Mutex> = Mutex::new(FastHashSet::default()); } /// The Texture struct is public to the user @@ -68,6 +69,7 @@ impl ExternalImageHandler for Compositor { } fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) { - ACTIVE_GL_TEXTURES.lock().unwrap().remove(&key); + TO_DELETE_TEXTURES.lock().unwrap().insert(key); + // ACTIVE_GL_TEXTURES.lock().unwrap().remove(&key); } } \ No newline at end of file From 5862d11715490f25b3054adb5b470df2605700ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 27 Jun 2018 07:31:01 +0200 Subject: [PATCH 101/868] Initial implementation of text caching implemented This version of text caching is absolutely awful, since it simply clones large strings every time the text is resized or scrolled or whatever. However it's just a first implementation and there are bigger issues to worry about right now. Another danger is that you could lose a text ID, thereby "leaking" the text string. This should be revisited, so that a proper error is returned. In general, the text string itself should not be stored with the layout of the glyphs together - because every time you'd want to change the glyphs, this would mean that you'd have to change the text, too, which isn't the case. This commit also adds image caching (so that you can either push images via an image ID or a CSS identifier (aka a String)). Doing it via the String requires 1 extra lookup, but is more convenient. --- examples/debug.rs | 2 + src/app.rs | 12 +++- src/app_state.rs | 12 +++- src/css_parser.rs | 25 +++++++- src/display_list.rs | 122 ++++++++++++++++++++++++----------- src/dom.rs | 11 ++-- src/id_tree.rs | 16 +++-- src/resources.rs | 70 ++++++++++++++++---- src/text_cache.rs | 31 ++++++--- src/text_layout.rs | 151 +++++++++++++++++++++++++++++--------------- src/ui_state.rs | 5 +- 11 files changed, 330 insertions(+), 127 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 4b335a0a7..8af2a1cdb 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + extern crate azul; use azul::prelude::*; diff --git a/src/app.rs b/src/app.rs index a5ff36c18..dfc073774 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use css_parser::{Font as FontId, PixelValue, FontSize}; use text_cache::TextId; use dom::UpdateScreen; use window::FakeWindow; @@ -7,7 +8,6 @@ use app_state::AppState; use traits::Layout; use ui_state::UiState; use ui_description::UiDescription; - use std::sync::{Arc, Mutex, PoisonError}; use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; use glium::glutin::Event; @@ -457,10 +457,16 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.delete_deamon(id) } - pub fn add_text>(&mut self, text: S) + pub fn add_text_uncached>(&mut self, text: S) + -> TextId + { + self.app_state.add_text_uncached(text) + } + + pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) -> TextId { - self.app_state.add_text(text) + self.app_state.add_text_cached(text, font_id, font_size) } pub fn delete_text(&mut self, id: TextId) { diff --git a/src/app_state.rs b/src/app_state.rs index 27ad1848c..101834553 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -13,6 +13,7 @@ use std::collections::hash_map::Entry::*; use FastHashMap; use std::sync::{Arc, Mutex}; use svg::{SvgLayerId, SvgLayer, SvgParseError}; +use css_parser::{Font as FontId, FontSize, PixelValue}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application @@ -209,10 +210,17 @@ impl<'a, T: Layout> AppState<'a, T> { self.tasks.retain(|x| x.is_finished()); } - pub fn add_text>(&mut self, text: S) + pub fn add_text_uncached>(&mut self, text: S) -> TextId { - self.resources.add_text(text) + self.resources.add_text_uncached(text) + } + + pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) + -> TextId + { + let font_size = FontSize(font_size); + self.resources.add_text_cached(text, font_id, font_size) } pub fn delete_text(&mut self, id: TextId) { diff --git a/src/css_parser.rs b/src/css_parser.rs index 3dbf69b85..400b986d2 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -275,12 +275,20 @@ impl<'a> From for CssParsingError<'a> { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct InvalidValueErr<'a>(pub &'a str); -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct PixelValue { pub metric: CssMetric, pub number: f32, } +impl PartialEq for PixelValue { + fn eq(&self, other: &Self) -> bool { + self.compare_equality_2digits(other) + } +} + +impl Eq for PixelValue { } + /// "100%" or "1.0" value #[derive(Debug, PartialEq, Copy, Clone)] pub struct PercentageValue { @@ -303,6 +311,19 @@ impl PixelValue { CssMetric::Em => { self.number * EM_HEIGHT }, } } + + /// Compare the equality of two font sizes up to the 4th digit + /// + /// i.e. `1.234` == `1.235` because `123` == `123` + /// + /// Usually this precision is enough to determine if two font sizes are + /// "equal" since you can't really compare floating-point values + /// + /// Used for the `PartialEq` implementation + pub fn compare_equality_2digits(&self, other: &Self) -> bool { + (self.to_pixels() * 100.0) as usize == + (other.to_pixels() * 100.0) as usize + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1652,7 +1673,7 @@ fn parse_line_height(input: &str) parse_percentage_value(input).and_then(|e| Ok(LineHeight(e))) } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct FontSize(pub PixelValue); typed_pixel_value_parser!(parse_css_font_size, FontSize); diff --git a/src/display_list.rs b/src/display_list.rs index 2317dcb16..6b028dac4 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -26,6 +26,8 @@ use { cache::DomChangeSet, ui_description::CssConstraintList, text_layout::{TextOverflowPass2, ScrollbarInfo}, + images::ImageId, + text_cache::TextId, }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); @@ -61,6 +63,36 @@ pub(crate) struct SolvedLayout { pub(crate) solved_constraints: FastHashMap>, } +/// This is used for caching large strings (in the `push_text` function) +/// In the cached version, you can lookup the text as well as the dimensions of +/// the words in the `AppResources`. For the `Uncached` version, you'll have to re- +/// calculate it on every frame. +pub(crate) enum TextInfo<'a> { + Cached(TextId), + Uncached(&'a str), +} + +impl<'a> TextInfo<'a> { + /// Returns if the inner text is empty. Returns false if the ID does not exist + fn is_empty_text(&self, app_resources: &AppResources) + -> bool + { + use self::TextInfo::*; + use text_cache::LargeString; + + match self { + Cached(text_id) => { + match app_resources.text_cache.cached_strings.get(text_id) { + Some(LargeString::Raw(r)) => r.is_empty(), + Some(LargeString::Cached { words, .. }) => words.is_empty(), + None => false, + } + } + Uncached(s) => s.is_empty(), + } + } +} + impl SolvedLayout { pub fn empty() -> Self { Self { @@ -120,8 +152,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { { use images::{ImageState, ImageInfo}; - let mut updated_images = Vec::<(String, (ImageData, ImageDescriptor))>::new(); - let mut to_delete_images = Vec::<(String, Option)>::new(); + let mut updated_images = Vec::<(ImageId, (ImageData, ImageDescriptor))>::new(); + let mut to_delete_images = Vec::<(ImageId, Option)>::new(); // possible performance bottleneck (duplicated cloning) !! for (key, value) in app_resources.images.iter() { @@ -369,10 +401,9 @@ fn displaylist_handle_rect( match html_node { Div => { /* nothing special to do */ }, Label(text) => { - // println!("encountered text with style: {:#?}", rect.style); push_text( &info, - text, + &TextInfo::Uncached(text), builder, &rect.style, app_resources, @@ -381,14 +412,21 @@ fn displaylist_handle_rect( resource_updates); }, Text(text_id) => { - + push_text( + &info, + &TextInfo::Cached(*text_id), + builder, + &rect.style, + app_resources, + &render_api, + &bounds, + resource_updates); }, Image(image_id) => { - + push_image(&info, builder, &bounds, app_resources, image_id); }, GlTexture(texture) => { - // This is probably going to destroy the texture too early, and not - // going to work properly. So this is simply an attempt at getting something going + use glium::GlObject; use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; @@ -435,9 +473,9 @@ fn push_rect( } #[inline] -fn push_text( +fn push_text<'a>( info: &PrimitiveInfo, - text: &str, + text: &TextInfo<'a>, builder: &mut DisplayListBuilder, style: &RectStyle, app_resources: &mut AppResources, @@ -450,7 +488,7 @@ fn push_text( use text_layout; use css_parser::{TextAlignmentHorz, TextOverflowBehaviour}; - if text.is_empty() { + if text.is_empty_text(&*app_resources) { return; } @@ -460,10 +498,8 @@ fn push_text( }; let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); - let font_size = font_size.0.to_pixels(); - let font_size_app_units = (font_size as i32) * AU_PER_PX; + let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX as i32); let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); - let font_size_app_units = Au(font_size_app_units as i32); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { @@ -474,7 +510,6 @@ fn push_text( let vert_alignment = TextAlignmentVert::Center; // TODO let line_height = style.line_height; - let font = &app_resources.font_data[font_id].0; let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); @@ -486,16 +521,17 @@ fn push_text( bar_color: BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 }), }; - let (positioned_glyphs, scrollbar_info) = text_layout::put_text_in_bounds( - text, - font, - font_size, - line_height, + let (positioned_glyphs, scrollbar_info) = text_layout::get_glyphs( + app_resources, + bounds, horz_alignment, vert_alignment, + &font_id, + &font_size, + line_height, + text, &overflow_behaviour, - &scrollbar_style, - bounds + &scrollbar_style ); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); @@ -731,26 +767,38 @@ fn push_background( let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, - Background::Image(image_id) => { - if let Some(image_info) = app_resources.images.get(&image_id.0) { - use images::ImageState::*; - match image_info { - Uploaded(image_info) => { - builder.push_image( - &info, - bounds.size, - LayoutSize::zero(), - ImageRendering::Auto, - AlphaType::Alpha, - image_info.key); - }, - _ => { }, - } + Background::Image(css_image_id) => { + if let Some(image_id) = app_resources.css_ids_to_image_ids.get(&css_image_id.0) { + push_image(info, builder, bounds, app_resources, image_id); } } } } +fn push_image( + info: &PrimitiveInfo, + builder: &mut DisplayListBuilder, + bounds: &TypedRect, + app_resources: &AppResources, + image_id: &ImageId) +{ + if let Some(image_info) = app_resources.images.get(image_id) { + use images::ImageState::*; + match image_info { + Uploaded(image_info) => { + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + image_info.key); + }, + _ => { }, + } + } +} + #[inline] fn push_border( info: &PrimitiveInfo, diff --git a/src/dom.rs b/src/dom.rs index 0a6d499ac..d6decd112 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -16,10 +16,11 @@ use images::ImageId; use cache::DomHash; use text_cache::TextId; use glium::framebuffer::SimpleFrameBuffer; +use std::sync::atomic::{AtomicUsize, Ordering}; /// This is only accessed from the main thread, so it's safe to use -pub(crate) static mut NODE_ID: u64 = 0; -pub(crate) static mut CALLBACK_ID: u64 = 0; +pub(crate) static NODE_ID: AtomicUsize = AtomicUsize::new(0); +pub(crate) static CALLBACK_ID: AtomicUsize = AtomicUsize::new(0); /// A callback function has to return if the screen should /// be updated after the function has run.PartialEq @@ -409,8 +410,7 @@ impl Dom { #[inline] pub fn set_callback(&mut self, on: On, callback: Callback) { self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); - self.arena.borrow_mut()[self.last].data.tag = Some(unsafe { NODE_ID }); - unsafe { NODE_ID += 1; }; + self.arena.borrow_mut()[self.last].data.tag = Some(NODE_ID.fetch_add(1, Ordering::SeqCst) as u64); } } @@ -425,8 +425,7 @@ impl Dom { let mut cb_id_list = BTreeMap::::new(); let item = &self.arena.borrow()[item.inner_value()]; for (on, callback) in item.data.events.callbacks.iter() { - let callback_id = unsafe { CALLBACK_ID }; - unsafe { CALLBACK_ID += 1; } + let callback_id = CALLBACK_ID.fetch_add(1, Ordering::SeqCst) as u64; callback_list.insert(callback_id, *callback); cb_id_list.insert(*on, callback_id); } diff --git a/src/id_tree.rs b/src/id_tree.rs index 113b924d0..c085c7f82 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -29,18 +29,24 @@ use std::{ pub struct NonZeroUsizeHack(&'static ()); impl NonZeroUsizeHack { - #[inline] + /// **NOTE**: Panics on overflow, since having a pointer that is zero is + /// undefined behaviour (it would bascially be casted to a `None`, + /// which is incorrect, so we rather panic on overflow to prevent that. + #[inline(always)] pub fn new(value: usize) -> Self { // Add 1 on insertion - let value = value + 1; - unsafe { NonZeroUsizeHack(&*(value as *const ())) } + let (new_value, has_overflown) = value.overflowing_add(1); + if has_overflown { + panic!("Overflow when creating DOM Node with ID {}", value); + } else { + unsafe { NonZeroUsizeHack(&*(new_value as *const ())) } + } } - #[inline] + #[inline(always)] pub fn get(self) -> usize { // Remove 1 on retrieval let value = self.0 as *const () as usize; - assert!(value != 0); // can never happen, since we add 1 it in the new() fn value - 1 } } diff --git a/src/resources.rs b/src/resources.rs index 0e3b4a5a3..172a3575c 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,6 +1,11 @@ +use images::ImageId; +use css_parser::FontSize; +use text_layout::RUSTTYPE_SIZE_HACK; +use text_layout::PX_TO_PT; +use text_layout::split_text_into_words; use webrender::api::Epoch; use dom::Texture; -use text_cache::TextRegistry; +use text_cache::TextCache; use traits::Layout; use std::sync::atomic::{AtomicUsize, Ordering}; use webrender::api::{ImageKey, FontKey, FontInstanceKey}; @@ -29,8 +34,14 @@ use text_cache::TextId; /// (not yet tested, but should work). #[derive(Clone)] pub(crate) struct AppResources<'a> { - /// Image cache - pub(crate) images: FastHashMap, + /// When looking up images, there are two sources: Either the indirect way via using a + /// CssId (which is a String) or a direct ImageId. The indirect way requires one extra + /// lookup (to map from the stringified ID to the actual image ID). This is what this + /// HashMap is for + pub(crate) css_ids_to_image_ids: FastHashMap, + /// The actual image cache, does NOT store the image data, only stores it temporarily + /// while it is being uploaded to the GPU via webrender. + pub(crate) images: FastHashMap, // Fonts are trickier to handle than images. // First, we duplicate the font - webrender wants the raw font data, // but we also need access to the font metrics. So we first parse the font @@ -42,16 +53,17 @@ pub(crate) struct AppResources<'a> { // we first need to create one. pub(crate) fonts: FastHashMap>, /// Stores long texts across frames - pub(crate) text_registry: TextRegistry, + pub(crate) text_cache: TextCache, } impl<'a> Default for AppResources<'a> { fn default() -> Self { Self { + css_ids_to_image_ids: FastHashMap::default(), fonts: FastHashMap::default(), font_data: FastHashMap::default(), images: FastHashMap::default(), - text_registry: TextRegistry::default(), + text_cache: TextCache::default(), } } } @@ -64,7 +76,17 @@ impl<'a> AppResources<'a> { { use images; // the module, not the crate! - match self.images.entry(id.into()) { + // TODO: Handle image decoding failure better! + + let image_id = match self.css_ids_to_image_ids.entry(id.into()) { + Occupied(_) => return Ok(None), + Vacant(v) => { + let new_id = images::new_image_id(); + v.insert(new_id) + }, + }; + + match self.images.entry(*image_id) { Occupied(_) => Ok(None), Vacant(v) => { let mut image_data = Vec::::new(); @@ -81,7 +103,9 @@ impl<'a> AppResources<'a> { pub(crate) fn delete_image>(&mut self, id: S) -> Option<()> { - match self.images.get_mut(id.as_ref()) { + let image_id = self.css_ids_to_image_ids.remove(id.as_ref())?; + + match self.images.get_mut(&image_id) { None => None, Some(v) => { let to_delete_image_key = match *v { @@ -100,7 +124,12 @@ impl<'a> AppResources<'a> { pub(crate) fn has_image>(&mut self, id: S) -> bool { - self.images.get(id.as_ref()).is_some() + let image_id = match self.css_ids_to_image_ids.get(id.as_ref()) { + None => return false, + Some(s) => s, + }; + + self.images.get(image_id).is_some() } /// See `AppState::add_font()` @@ -148,17 +177,34 @@ impl<'a> AppResources<'a> { } } - pub(crate) fn add_text>(&mut self, text: S) + pub(crate) fn add_text_uncached>(&mut self, text: S) -> TextId { - self.text_registry.add_text(text) + use text_cache::LargeString; + self.text_cache.add_text(LargeString::Raw(text.into())) + } + + /// Calculates the widths for the words, then stores the widths of the words + the actual words + /// + /// This leads to a faster layout cycle, but has an upfront performance cost + pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &css_parser::Font, font_size: FontSize) + -> TextId + { + use rusttype::Scale; + use text_cache::LargeString; + use std::rc::Rc; + + let font_size_no_line_height = Scale::uniform(font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT); + let rusttype_font = self.font_data.get(font_id).expect("in resources.add_text_cached(): could not get font for caching text"); + let words = split_text_into_words(text.as_ref(), &rusttype_font.0, font_size_no_line_height); + self.text_cache.add_text(LargeString::Cached { font: font_id.clone(), size: font_size, words: Rc::new(words) }) } pub(crate) fn delete_text(&mut self, id: TextId) { - self.text_registry.delete_text(id); + self.text_cache.delete_text(id); } pub(crate) fn clear_all_texts(&mut self) { - self.text_registry.clear_all_texts(); + self.text_cache.clear_all_texts(); } } \ No newline at end of file diff --git a/src/text_cache.rs b/src/text_cache.rs index 22a62d215..606e011c2 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -1,5 +1,9 @@ +use css_parser::FontSize; +use text_layout::SemanticWordItem; use FastHashMap; use std::sync::atomic::{Ordering, AtomicUsize}; +use css_parser::Font; +use std::rc::Rc; static TEXT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -10,29 +14,40 @@ fn new_text_id() -> TextId { } } +/// A unique ID by which a large block of text can be uniquely identified #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct TextId { inner: usize, } -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct TextRegistry { - inner: FastHashMap +#[derive(Debug, Clone)] +pub(crate) enum LargeString { + Raw(String), + /// The `Vec` stores the individual word, so we don't need + /// to store it again. The `words` is stored in an Rc, so that we don't need to + /// duplicate it for every font size. + Cached { font: Font, size: FontSize, words: Rc> }, } -impl TextRegistry { +/// Cache for accessing large amounts of text +#[derive(Debug, Default, Clone)] +pub(crate) struct TextCache { + /// Gives you the mapping from the TextID to the actual, UTF-8 String + pub(crate) cached_strings: FastHashMap, +} - pub(crate) fn add_text>(&mut self, text: S) -> TextId { +impl TextCache { + pub(crate) fn add_text(&mut self, text: LargeString) -> TextId { let id = new_text_id(); - self.inner.insert(id, text.into()); + self.cached_strings.insert(id, text); id } pub(crate) fn delete_text(&mut self, id: TextId) { - self.inner.remove(&id); + self.cached_strings.remove(&id); } pub(crate) fn clear_all_texts(&mut self) { - self.inner.clear(); + self.cached_strings.clear(); } } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index 6e345f28d..2d9d3a7b2 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,28 +1,31 @@ #![allow(unused_variables, dead_code)] +use resources::AppResources; +use display_list::TextInfo; use webrender::api::*; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; -use css_parser::{TextAlignmentHorz, BackgroundColor, TextAlignmentVert, LineHeight, LayoutOverflow}; +use css_parser::{TextAlignmentHorz, FontSize, BackgroundColor, Font as FontId, TextAlignmentVert, LineHeight, LayoutOverflow}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing -const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; +pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; -const PX_TO_PT: f32 = 72.0 / 96.0; +pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; -#[derive(Debug)] -struct Word { - // the original text +#[derive(Debug, Clone)] +pub(crate) struct Word { + /// The original text. TODO: Move this out of here, + /// this field gets unnecessarily cloned pub text: String, - // glyphs, positions are relative to the first character of the word + /// Glyphs, positions are relative to the first character of the word pub glyphs: Vec, - // the sum of the width of all the characters + /// The sum of the width of all the characters pub total_width: f32, } -#[derive(Debug)] -enum SemanticWordItem { +#[derive(Debug, Clone)] +pub(crate) enum SemanticWordItem { /// Encountered a word (delimited by spaces) Word(Word), // `\t` or `x09` @@ -121,13 +124,30 @@ struct FontMetrics { offset_top: f32, } +// TODO: hacky hacky shit. Seperate the text itself from the representation +// so we don't have to clone the strings when we change or zoom the font +fn get_string_from_words(words: &[SemanticWordItem]) -> String { + use self::SemanticWordItem::*; + let mut target = String::with_capacity(words.len()); + for word in words { + match word { + Word(w) => target += &w.text, + Tab => target.push('\t'), + Return => target.push('\n'), + } + } + target +} + /// ## Inputs /// +/// - `app_resources`: This is only used for caching - if you already have a `LargeString`, which +/// stores the word boundaries for the given font, we don't have to re-calculate the font metrics again. /// - `bounds`: The bounds of the rectangle containing the text /// - `horiz_alignment`: Usually parsed from the `text-align` attribute: horizontal alignment of the text /// - `vert_alignment`: Usually parsed from the `align-items` attribute on the parent node /// or the `align-self` on the child node: horizontal alignment of the text -/// - `font`: The font to use for layouting +/// - `font`: The font to use for layouting (only the ID) /// - `font_size`: The font size (without line height) /// - `line_height`: The line height (100% = 1.0). I.e. `line-height = 1.2;` scales the text vertically by 1.2x /// - `text`: The actual text to layout. Will be unicode-normalized after the Unicode Normalization Form C @@ -149,26 +169,32 @@ struct FontMetrics { /// allocations. This should be cleaned up in the future by caching `BlobStrings` and only re-layouting /// when it's absolutely necessary. pub(crate) fn get_glyphs<'a>( + app_resources: &AppResources<'a>, bounds: &TypedRect, horiz_alignment: TextAlignmentHorz, vert_alignment: TextAlignmentVert, - font: &'a Font<'a>, - font_size: f32, + target_font_id: &FontId, + target_font_size: &FontSize, line_height: Option, - text: &str, + text: &TextInfo<'a>, overflow: &LayoutOverflow, scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) { use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; + use text_cache::LargeString; + + let target_font = app_resources.font_data.get(target_font_id) + .expect("Drawing with invalid font!"); + let target_font_size_f32 = target_font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT; let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; - let font_size_with_line_height = Scale::uniform(font_size * line_height); - let font_size_no_line_height = Scale::uniform(font_size); - let space_width = font.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; + let font_size_with_line_height = Scale::uniform(target_font_size_f32 * line_height); + let font_size_no_line_height = Scale::uniform(target_font_size_f32); + let space_width = target_font.0.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; let tab_width = 4.0 * space_width; // TODO: make this configurable - let v_metrics_scaled = font.v_metrics(font_size_with_line_height); + let v_metrics_scaled = target_font.0.v_metrics(font_size_with_line_height); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; let offset_top = v_metrics_scaled.ascent; @@ -179,14 +205,61 @@ pub(crate) fn get_glyphs<'a>( offset_top: offset_top, }; - // (1) Split the text into semantic items (word, tab or newline) + // (1) Split the text into semantic items (word, tab or newline) OR get the cached + // text and scale it accordingly. + // // This function also normalizes the unicode characters and calculates kerning. // - // TODO: cache the words somewhere - let words = split_text_into_words(text, font, font_size_no_line_height); + // NOTE: This should be revisited, the caching does unnecessary cloning. + let (word_scale_factor, mut words) = match text { + TextInfo::Cached(text_id) => { + match app_resources.text_cache.cached_strings.get(text_id) { + Some(LargeString::Cached { font, size, words }) => { + if font == target_font_id { + use std::rc::Rc; + // If the target font is the same as the initial font, but the font size differs, + // all we have to do is to scale the widths of the words on the words + let cloned_words: Vec = (&*(words.clone())).clone(); + if size == target_font_size { + (None, cloned_words) + } else { + (Some(target_font_size.0.to_pixels() / size.0.to_pixels()), cloned_words) + } + } else { + // generate new words struct based on the previous words + let new_words = split_text_into_words(&get_string_from_words(words), &target_font.0, font_size_no_line_height); + (None, new_words) + } + }, + Some(LargeString::Raw(s)) => { + (None, split_text_into_words(s, &target_font.0, font_size_no_line_height)) + }, + None => panic!("Invalid TextId \"{:?}\" encountered in text_layout::get_glyphs", text_id), + } + }, + TextInfo::Uncached(s) => (None, split_text_into_words(s, &target_font.0, font_size_no_line_height)), + }; + + // Scale the horizontal width of the words to match the new font size + // Since each word has a local origin (i.e. the first character of each word + // is at (0, 0)), we can simply scale the X position of each glyph by a + // certain factor. + // + // So if we previously had a 12pt font and now a 13pt font, + // we simply scale each glyph position by 13 / 12. This is faster than + // re-calculating the font metrics (from Rusttype) each time we scale a + // large amount of text. + if let Some(scale_factor) = word_scale_factor { + for word in words.iter_mut() { + if let SemanticWordItem::Word(ref mut w) = word { + w.glyphs.iter_mut().for_each(|g| g.point.x *= scale_factor); + w.total_width *= scale_factor; + } + } + } // (2) Calculate the additions / subtractions that have to be take into account - let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, font); + // let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, &target_font.0); // (3) Determine if the words will overflow the bounding rectangle let overflow_pass_1 = estimate_overflow_pass_1(&words, &bounds.size, &font_metrics, &overflow); @@ -200,10 +273,10 @@ pub(crate) fn get_glyphs<'a>( // (5) Align text to the left, initial layout of glyphs let (mut positioned_glyphs, line_break_offsets) = - words_to_left_aligned_glyphs(words, font, max_horizontal_text_width, &font_metrics); + words_to_left_aligned_glyphs(words, &target_font.0, max_horizontal_text_width, &font_metrics); // (6) Add the harfbuzz adjustments to the positioned glyphs - apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); + // apply_harfbuzz_adjustments(&mut positioned_glyphs, harfbuzz_adjustments); // (7) Calculate the Knuth-Plass adjustments for the (now layouted) glyphs let knuth_plass_adjustments = calculate_knuth_plass_adjustments(&positioned_glyphs, &line_break_offsets); @@ -223,8 +296,10 @@ pub(crate) fn get_glyphs<'a>( (positioned_glyphs, overflow_pass_2) } -#[inline(always)] -fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: Scale) +/// This function is also used in the `text_cache` module for caching large strings. +/// +/// It is one of the most expensive functions, use with care. +pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: Scale) -> Vec { use unicode_normalization::UnicodeNormalization; @@ -734,28 +809,4 @@ fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) c.point.x += x; c.point.y += y; } -} - -pub(crate) fn put_text_in_bounds<'a>( - text: &str, - font: &Font<'a>, - font_size: f32, - line_height: Option, - horz_align: TextAlignmentHorz, - vert_align: TextAlignmentVert, - overflow: &LayoutOverflow, - scrollbar_info: &ScrollbarInfo, - bounds: &TypedRect) --> (Vec, TextOverflowPass2) -{ - get_glyphs( - bounds, - horz_align, - vert_align, - font, - font_size * RUSTTYPE_SIZE_HACK * PX_TO_PT, - line_height, - text, - overflow, - scrollbar_info) } \ No newline at end of file diff --git a/src/ui_state.rs b/src/ui_state.rs index dd15310f7..7273b8c31 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -29,6 +29,7 @@ impl UiState { pub(crate) fn from_app_state(app_state: &AppState, window_info: WindowInfo) -> Self { use dom::{Dom, On}; + use std::sync::atomic::Ordering; // Only shortly lock the data to get the dom out let dom: Dom = { @@ -36,8 +37,8 @@ impl UiState { dom_lock.layout(window_info) }; - unsafe { NODE_ID = 0 }; - unsafe { CALLBACK_ID = 0 }; + NODE_ID.swap(0, Ordering::SeqCst); + CALLBACK_ID.swap(0, Ordering::SeqCst); let mut callback_list = BTreeMap::>::new(); let mut node_ids_to_callbacks_list = BTreeMap::>::new(); From 5ea52bbfb4c815dee54405f9ff10d24fcd2a6428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 27 Jun 2018 08:17:12 +0200 Subject: [PATCH 102/868] Added empty test for codecov --- src/app.rs | 8 ++++++++ src/app_state.rs | 8 ++++++++ src/cache.rs | 8 ++++++++ src/compositor.rs | 8 ++++++++ src/constraints.rs | 8 ++++++++ src/display_list.rs | 10 +++++++++- src/dom.rs | 8 ++++++++ src/font.rs | 8 ++++++++ src/images.rs | 7 +++++++ src/menu.rs | 8 ++++++++ src/resources.rs | 8 ++++++++ src/svg.rs | 8 ++++++++ src/task.rs | 8 ++++++++ src/text_cache.rs | 8 ++++++++ src/text_layout.rs | 21 +++++++++++++++++++++ src/traits.rs | 8 ++++++++ src/ui_description.rs | 8 ++++++++ src/ui_state.rs | 8 ++++++++ src/widgets.rs | 8 ++++++++ src/window.rs | 8 ++++++++ src/window_state.rs | 10 +++++++++- 21 files changed, 182 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index dfc073774..ed4c21614 100644 --- a/src/app.rs +++ b/src/app.rs @@ -602,4 +602,12 @@ fn render( window.internal.api.send_transaction(window.internal.document_id, txn); window.renderer.as_mut().unwrap().update(); window.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_app_file() { + } \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index 101834553..9f4a0a3eb 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -239,4 +239,12 @@ impl<'a, T: Layout + Send + 'static> AppState<'a, T> { let task = Task::new(&self.data, callback); self.tasks.push(task); } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_app_state_file() { + } \ No newline at end of file diff --git a/src/cache.rs b/src/cache.rs index a621345d3..b946dc2e7 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -306,4 +306,12 @@ impl EditVariableCache { self.map.remove(hash); } } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_cache_file() { + } \ No newline at end of file diff --git a/src/compositor.rs b/src/compositor.rs index 0f8a9a82d..dce8423d3 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -72,4 +72,12 @@ impl ExternalImageHandler for Compositor { TO_DELETE_TEXTURES.lock().unwrap().insert(key); // ACTIVE_GL_TEXTURES.lock().unwrap().remove(&key); } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_compositor_file() { + } \ No newline at end of file diff --git a/src/constraints.rs b/src/constraints.rs index ac3866ba5..0576ef39c 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -251,4 +251,12 @@ impl PaddingConstraint { }, } } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_constraints_file() { + } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 6b028dac4..5fa14c204 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1055,4 +1055,12 @@ impl<'a> Arena> { } } } -} \ No newline at end of file +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_display_list_file() { + +} diff --git a/src/dom.rs b/src/dom.rs index d6decd112..3bdd9af53 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -434,4 +434,12 @@ impl Dom { } } } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_dom_file() { + } \ No newline at end of file diff --git a/src/font.rs b/src/font.rs index 8ee34724d..318e05fcc 100644 --- a/src/font.rs +++ b/src/font.rs @@ -39,4 +39,12 @@ pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontErro let collection = FontCollection::from_bytes(data)?; let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); Ok(font) +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_font_file() { + } \ No newline at end of file diff --git a/src/images.rs b/src/images.rs index 8ecd6dd3d..0b677f485 100644 --- a/src/images.rs +++ b/src/images.rs @@ -194,4 +194,11 @@ pub(crate) fn premultiply(data: &mut [u8]) { pixel[1] = ((g * a + 128) / 255) as u8; pixel[0] = ((b * a + 128) / 255) as u8; } +} + +#[test] +fn test_premultiply() { + let mut color = [255, 0, 0, 127]; + premultiply(&mut color); + assert_eq!(color, [127, 0, 0, 127]); } \ No newline at end of file diff --git a/src/menu.rs b/src/menu.rs index 4e52ae1bd..a79f735ee 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -48,4 +48,12 @@ pub enum MenuItem { pub mod command_ids { // "Test" menu pub const CMD_TEST: u16 = 9001; +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_menu_file() { + } \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index 172a3575c..342b96922 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -207,4 +207,12 @@ impl<'a> AppResources<'a> { pub(crate) fn clear_all_texts(&mut self) { self.text_cache.clear_all_texts(); } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_resources_file() { + } \ No newline at end of file diff --git a/src/svg.rs b/src/svg.rs index 35a33ce56..d37e60553 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -687,3 +687,11 @@ mod svg_to_lyon { Some(String::from("hello")) } } + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_svg_file() { + +} \ No newline at end of file diff --git a/src/task.rs b/src/task.rs index ee8d214f5..0eed005dc 100644 --- a/src/task.rs +++ b/src/task.rs @@ -43,4 +43,12 @@ impl Drop for Task { let _ = thread_handle.join().unwrap(); } } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_task_file() { + } \ No newline at end of file diff --git a/src/text_cache.rs b/src/text_cache.rs index 606e011c2..bcaf20bcd 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -50,4 +50,12 @@ impl TextCache { pub(crate) fn clear_all_texts(&mut self) { self.cached_strings.clear(); } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_text_cache_file() { + } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index 2d9d3a7b2..130a22c30 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -809,4 +809,25 @@ fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) c.point.x += x; c.point.y += y; } +} + +#[test] +fn test_it_should_add_origin() { + let mut instances = vec![ + GlyphInstance { + index: 20, + point: TypedPoint2D::new(0.0, 0.0), + }, + GlyphInstance { + index: 40, + point: TypedPoint2D::new(20.0, 10.0), + }, + ]; + + add_origin(&mut instances, 13.0, 0.0); + + assert_eq!(instances[0].point.x as usize, 13); + assert_eq!(instances[0].point.y as usize, 0); + assert_eq!(instances[1].point.x as usize, 33); + assert_eq!(instances[1].point.y as usize, 10); } \ No newline at end of file diff --git a/src/traits.rs b/src/traits.rs index ea2cd372e..d5ad17024 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -263,4 +263,12 @@ fn cascade_constraints<'a, T: Layout>( #[inline] fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { list.list.push(rule.declaration.1.clone()); +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_traits_file() { + } \ No newline at end of file diff --git a/src/ui_description.rs b/src/ui_description.rs index 199562010..fb7f1d34e 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -68,4 +68,12 @@ pub(crate) struct StyledNode { #[derive(Debug, Default, Clone, PartialEq)] pub(crate) struct CssConstraintList { pub(crate) list: Vec +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_ui_description_file() { + } \ No newline at end of file diff --git a/src/ui_state.rs b/src/ui_state.rs index 7273b8c31..095eef716 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -50,4 +50,12 @@ impl UiState { node_ids_to_callbacks_list: node_ids_to_callbacks_list, } } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_ui_state_file() { + } \ No newline at end of file diff --git a/src/widgets.rs b/src/widgets.rs index 7de592ee1..95160b0fb 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -152,4 +152,12 @@ impl GetDom for Label { fn dom(self) -> Dom { Dom::new(NodeType::Label(self.text)) } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_widgets_file() { + } \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 211b9d84e..db4754793 100644 --- a/src/window.rs +++ b/src/window.rs @@ -776,4 +776,12 @@ impl Drop for Window { let renderer = self.renderer.take().unwrap(); renderer.deinit(); } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_window_file() { + } \ No newline at end of file diff --git a/src/window_state.rs b/src/window_state.rs index e2398e39f..ff7c2b30d 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -168,7 +168,15 @@ fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { } } +// TODO fn virtual_key_code_to_char(code: VirtualKeyCode) -> Option { - // TODO Some('a') +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_window_state_file() { + } \ No newline at end of file From 3f7a6a071d966595c62dac2c850ff264bea3e078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 05:31:47 +0200 Subject: [PATCH 103/868] Use resvg to implement proper SVG drawing + updated webrender --- Cargo.toml | 7 +- src/css_parser.rs | 7 +- src/images.rs | 12 +- src/lib.rs | 2 +- src/svg.rs | 601 +++++++++++++++++++++++----------------------- src/window.rs | 2 +- 6 files changed, 316 insertions(+), 315 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da99e6a97..4466bfd42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,16 @@ rusttype = "0.5.2" app_units = "0.6" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" -svg = "0.5.10" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" +[dependencies.resvg] +git = "https://github.com/RazrFalcon/resvg.git" +rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" + [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "2e381e0325f367429810099d5abf76e674f697ed" +rev = "470d304c2ff7951a5bb600a9655f1949703e7fbb" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/css_parser.rs b/src/css_parser.rs index 400b986d2..6318da355 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -3,10 +3,9 @@ pub use euclid::{TypedSize2D, SideOffsets2D}; pub use webrender::api::{ BorderRadius, BorderWidths, BorderDetails, NormalBorder, - NinePatchBorder, GradientBorder, RadialGradientBorder, - LayoutPixel, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, - Gradient, RadialGradient, LayoutPoint, LayoutSize, - ExtendMode + NinePatchBorder, LayoutPixel, BoxShadowClipMode, ColorU, + ColorF, LayoutVector2D, Gradient, RadialGradient, LayoutPoint, + LayoutSize, ExtendMode }; // TODO: 9patch images! use webrender::api::{BorderStyle, BorderSide, LayoutRect}; diff --git a/src/images.rs b/src/images.rs index 0b677f485..697832f52 100644 --- a/src/images.rs +++ b/src/images.rs @@ -181,18 +181,12 @@ pub(crate) fn is_image_opaque(format: WebrenderImageFormat, bytes: &[u8]) -> boo // From webrender/wrench // These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions -// This function also converts from RGBA8 to BRGA8 pub(crate) fn premultiply(data: &mut [u8]) { for pixel in data.chunks_mut(4) { let a = u32::from(pixel[3]); - let r = u32::from(pixel[2]); - let g = u32::from(pixel[1]); - let b = u32::from(pixel[0]); - - pixel[3] = a as u8; - pixel[2] = ((r * a + 128) / 255) as u8; - pixel[1] = ((g * a + 128) / 255) as u8; - pixel[0] = ((b * a + 128) / 255) as u8; + pixel[0] = (((pixel[0] as u32 * a) + 128) / 255) as u8; + pixel[1] = (((pixel[1] as u32 * a) + 128) / 255) as u8; + pixel[2] = (((pixel[2] as u32 * a) + 128) / 255) as u8; } } diff --git a/src/lib.rs b/src/lib.rs index b6c96fd97..94f8f2234 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub extern crate image; extern crate lazy_static; extern crate euclid; extern crate lyon; -extern crate svg as svg_crate; +extern crate resvg; extern crate webrender; extern crate cassowary; extern crate twox_hash; diff --git a/src/svg.rs b/src/svg.rs index d37e60553..8bb4ad7de 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,31 +1,47 @@ +use resvg::usvg::ViewBox; +use resvg::usvg::Transform; use std::sync::Mutex; -use glium::backend::Facade; -use std::rc::Rc; -use glium::DrawParameters; -use glium::IndexBuffer; -use glium::VertexBuffer; -use glium::Display; -use glium::Texture2d; -use glium::Program; -use webrender::api::ColorF; -use std::io::Read; -use lyon::path::default::Path; -use webrender::api::ColorU; +use glium::{ + backend::Facade, + DrawParameters, IndexBuffer, VertexBuffer, Display, + Texture2d, Program, +}; +use std::{fmt, rc::Rc, + io::{Error as IoError, Read}, + sync::atomic::{Ordering, AtomicUsize}, + cell::UnsafeCell, + hash::{Hash, Hasher}, +}; +use lyon::{path::{PathEvent, default::Path}, tessellation::{LineCap, VertexBuffers, LineJoin}}; +use resvg::usvg::Error as SvgError; +use webrender::api::{ColorU, ColorF}; +use euclid::TypedRect; + use dom::Callback; use traits::Layout; -use std::sync::atomic::{Ordering, AtomicUsize}; use FastHashMap; -use std::hash::{Hash, Hasher}; -use svg_crate::parser::Error as SvgError; -use std::io::Error as IoError; -use std::fmt; -use euclid::TypedRect; -use lyon::tessellation::VertexBuffers; -use std::cell::UnsafeCell; /// In order to store / compare SVG files, we have to pub(crate) static SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SvgTransformId(usize); + +const SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); + +pub fn new_svg_transform_id() -> SvgTransformId { + SvgTransformId(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst)) +} + +const SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SvgViewBoxId(usize); + +pub fn new_view_box_id() -> SvgViewBoxId { + SvgViewBoxId(SVG_VIEW_BOX_ID.fetch_add(1, Ordering::SeqCst)) +} + const SVG_VERTEX_SHADER: &str = " #version 130 @@ -74,6 +90,12 @@ pub struct SvgCache { gpu_ready_to_upload_cache: FastHashMap, Vec)>, vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, shader: Mutex>, + // Stores the 2D transforms of the shapes on the screen. The vertices are + // offset by the X, Y value in the transforms struct. This should be expanded + // to full matrices later on, so you can do full 3D transformations + // on 2D shapes later on. For now, each transform is just an X, Y offset + transforms: FastHashMap, + view_boxes: FastHashMap, } impl Default for SvgCache { @@ -83,6 +105,8 @@ impl Default for SvgCache { gpu_ready_to_upload_cache: FastHashMap::default(), vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), shader: Mutex::new(None), + transforms: FastHashMap::default(), + view_boxes: FastHashMap::default(), } } } @@ -154,12 +178,19 @@ impl SvgCache { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { let new_svg_id = SvgLayerId(SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst)); - let (vertex_buf, index_buf) = tesselate_layer_data(&layer.data); + // TODO: set tolerance based on zoom + let (vertex_buf, index_buf) = tesselate_layer_data(&layer.data, 0.01); self.layers.insert(new_svg_id, layer); self.gpu_ready_to_upload_cache.insert(new_svg_id, (vertex_buf, index_buf)); new_svg_id } + pub fn add_transforms(&mut self, transforms: FastHashMap) { + transforms.into_iter().for_each(|(k, v)| { + self.transforms.insert(k, v); + }); + } + pub fn delete_layer(&mut self, svg_id: SvgLayerId) { self.layers.remove(&svg_id); self.gpu_ready_to_upload_cache.remove(&svg_id); @@ -177,29 +208,29 @@ impl SvgCache { /// Parses an input source, parses the SVG, adds the shapes as layers into /// the registry, returns the IDs of the added shapes, in the order that they appeared in the Svg pub fn add_svg(&mut self, input: R) -> Result, SvgParseError> { - Ok(self::svg_to_lyon::parse_from(input)? + let (layers, transforms) = self::svg_to_lyon::parse_from(input, &mut self.view_boxes)?; + self.add_transforms(transforms); + Ok(layers .into_iter() - .map(|layer| - self.add_layer(layer)) + .map(|layer| self.add_layer(layer)) .collect()) } } -fn tesselate_layer_data(layer_data: &[SvgLayerType]) -> (Vec, Vec) { +fn tesselate_layer_data(layer_data: &SvgLayerType, tolerance: f32) -> (Vec, Vec) { const GL_RESTART_INDEX: u32 = ::std::u32::MAX; let mut last_index = 0; let mut vertex_buf = Vec::::new(); let mut index_buf = Vec::::new(); - for layer in layer_data { - let VertexBuffers { vertices, indices } = layer.tesselate(); - let vertices_len = vertices.len(); - vertex_buf.extend(vertices.into_iter()); - index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); - index_buf.push(GL_RESTART_INDEX); - last_index += vertices_len; - } + let VertexBuffers { vertices, indices } = layer_data.tesselate(tolerance); + let vertices_len = vertices.len(); + + vertex_buf.extend(vertices.into_iter()); + index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); + index_buf.push(GL_RESTART_INDEX); + last_index += vertices_len; (vertex_buf, index_buf) } @@ -225,9 +256,12 @@ impl From for SvgParseError { } pub struct SvgLayer { - pub data: Vec, + pub data: SvgLayerType, pub callbacks: SvgCallbacks, pub style: SvgStyle, + // ID in the transform idx + pub transform_id: Option, + pub view_box_id: SvgViewBoxId, } impl Clone for SvgLayer { @@ -236,6 +270,8 @@ impl Clone for SvgLayer { data: self.data.clone(), callbacks: self.callbacks.clone(), style: self.style.clone(), + transform_id: self.transform_id, + view_box_id: self.view_box_id, } } } @@ -289,61 +325,109 @@ impl Eq for SvgCallbacks { } #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] pub struct SvgStyle { /// Stroke color - pub stroke: Option, - /// Stroke width * 1000, since otherwise `Hash` can't be derived - /// - /// i.e. a stroke width of `5.0` = `5000`. - pub stroke_width: Option, + pub stroke: Option<(ColorU, SvgStrokeOptions)>, /// Fill color pub fill: Option, - // missing: - // - // fill-opacity - // stroke-miterlimit - // stroke-dasharray - // stroke-opacity -} - -impl SvgStyle { - /// Parses the Svg style from a string, on error returns the default `SvgStyle`. - pub fn from_svg_string(input: &str) -> Self { - use css_parser::parse_css_color; - use FastHashMap; - - let mut style = FastHashMap::<&str, &str>::default(); - - for kv in input.split(";") { - let mut iter = kv.trim().split(":"); - let key = iter.next(); - let value = iter.next(); - if let (Some(k), Some(v)) = (key, value) { - style.insert(k, v); - } - } + // TODO: stroke-dasharray +} + +// similar to lyon::SvgStrokeOptions, except the +// thickness is a usize (f32 * 1000 as usize), in order +// to implement Hash +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub struct SvgStrokeOptions { + /// What cap to use at the start of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub start_cap: SvgLineCap, + + /// What cap to use at the end of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub end_cap: SvgLineCap, + + /// See the SVG specification. + /// + /// Default value: `LineJoin::Miter`. + pub line_join: SvgLineJoin, - let fill = style.get("fill") - .and_then(|s| parse_css_color(s).ok()); + /// Line width + /// + /// Default value: `StrokeOptions::DEFAULT_LINE_WIDTH`. + pub line_width: usize, + + /// See the SVG specification. + /// + /// Must be greater than or equal to 1.0. + /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`. + pub miter_limit: usize, - let stroke = style.get("stroke") - .and_then(|s| parse_css_color(s).ok()); + /// Maximum allowed distance to the path when building an approximation. + /// + /// See [Flattening and tolerance](index.html#flattening-and-tolerance). + /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`. + pub tolerance: usize, - let stroke_width = style.get("stroke-width") - .and_then(|s| s.parse::().ok()) - .and_then(|sw_float| Some((sw_float * 1000.0) as usize)); + /// Apply line width + /// + /// When set to false, the generated vertices will all be positioned in the centre + /// of the line. The width can be applied later on (eg in a vertex shader) by adding + /// the vertex normal multiplied by the line with to each vertex position. + /// + /// Default value: `true`. + pub apply_line_width: bool, +} + +impl Default for SvgStrokeOptions { + fn default() -> Self { + const DEFAULT_MITER_LIMIT: f32 = 4.0; + const DEFAULT_LINE_WIDTH: f32 = 1.0; + const DEFAULT_TOLERANCE: f32 = 0.1; Self { - fill, - stroke_width, - stroke, + start_cap: SvgLineCap::default(), + end_cap: SvgLineCap::default(), + line_join: SvgLineJoin::default(), + line_width: (DEFAULT_LINE_WIDTH * 1000.0) as usize, + miter_limit: (DEFAULT_MITER_LIMIT * 1000.0) as usize, + tolerance: (DEFAULT_TOLERANCE * 1000.0) as usize, + apply_line_width: true, } } } +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum SvgLineCap { + Butt, + Square, + Round, +} + +impl Default for SvgLineCap { + fn default() -> Self { + SvgLineCap::Butt + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum SvgLineJoin { + Miter, + MiterClip, + Round, + Bevel, +} + +impl Default for SvgLineJoin { + fn default() -> Self { + SvgLineJoin::Miter + } +} + /// One "layer" is simply one or more polygons that get drawn using the same style /// i.e. one SVG `` element #[derive(Debug, Clone)] pub enum SvgLayerType { - Polygon(Path), + Polygon(Vec), Circle(SvgCircle), Rect(SvgRect), Text(String), @@ -361,22 +445,30 @@ implement_vertex!(SvgVert, xy, normal); pub struct SvgWorldPixel; impl SvgLayerType { - pub fn tesselate(&self) + pub fn tesselate(&self, tolerance: f32) -> VertexBuffers { use self::SvgLayerType::*; - use lyon::tessellation::{VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator}; - use lyon::tessellation::basic_shapes::{fill_circle, fill_rounded_rectangle}; - use lyon::geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}; - use lyon::tessellation::basic_shapes::BorderRadii; + use lyon::tessellation::{ + VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, + path::{default::Builder, builder::{PathBuilder, FlatPathBuilder}}, + basic_shapes::{fill_circle, fill_rounded_rectangle, BorderRadii}, + geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, + }; let mut geometry = VertexBuffers::new(); match self { Polygon(p) => { + let mut builder = Builder::with_capacity(p.len()).flattened(tolerance); + for event in p { + builder.path_event(*event); + } + let path = builder.with_svg().build(); + let mut tessellator = FillTessellator::new(); tessellator.tessellate_path( - p.path_iter(), + path.path_iter(), &FillOptions::default(), &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { SvgVert { @@ -385,6 +477,8 @@ impl SvgLayerType { } }), ).unwrap(); + + // TODO: stroke! }, Circle(c) => { fill_circle( @@ -441,250 +535,161 @@ pub struct SvgRect { mod svg_to_lyon { - use svg_crate::node::Attributes; - use std::io::Read; - use std::collections::HashMap; - use lyon::path::default::Path; + use std::{slice, iter, io::Read}; use lyon::{ - path::{PathEvent, default::Builder, builder::SvgPathBuilder}, - tessellation::{self, StrokeOptions}, math::Point, - geom::{ArcFlags, euclid::{TypedPoint2D, TypedVector2D, Angle}}, - path::{SvgEvent, builder::SvgBuilder}, + path::{PathEvent, iterator::PathIter}, + tessellation::{self, StrokeOptions}, }; - use svg::{SvgCircle, SvgRect, SvgParseError, SvgLayer, SvgStyle}; - use svg_crate::node::element::path::Parameters; - use svg_crate::node::element::tag::Tag; + use resvg::usvg::{self, ViewBox, Transform, Tree, Path, PathSegment, + Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; + use svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, + SvgLayerType, SvgStyle, SvgCallbacks, SvgParseError, SvgTransformId, + new_svg_transform_id, new_view_box_id, SvgViewBoxId}; use traits::Layout; + use webrender::api::ColorU; + use FastHashMap; + + pub fn parse_from(mut svg_source: R, view_boxes: &mut FastHashMap) + -> Result<(Vec>, FastHashMap), SvgParseError> { + let svg_source = { + let mut source_str = String::new(); + svg_source.read_to_string(&mut source_str)?; + source_str + }; - pub fn parse_from(svg_source: R) - -> Result>, SvgParseError> - { - use svg_crate::{read, parser::{Event, Error}}; - use std::mem::discriminant; - use svg::{SvgLayerType, SvgCallbacks}; + let opt = Options::default(); + let rtree = Tree::from_str(&svg_source, &opt).unwrap(); - let file = read(svg_source)?; + let mut layer_data = Vec::new(); + let mut transform = None; + let mut transforms = FastHashMap::default(); - let mut last_err = None; + let view_box = rtree.svg_node().view_box; + let view_box_id = new_view_box_id(); + view_boxes.insert(view_box_id, view_box); - let layer_data = file - // We are only interested in tags, not comments or other stuff - .filter_map(|event| match event { - Event::Tag(id, _, attributes) => Some((id, attributes)), - Event::Error(e) => { /* TODO: hacky */ last_err = Some(e); None }, - _ => None, + for node in rtree.root().descendants() { + if let NodeKind::Path(p) = &*node.borrow() { + let mut style = SvgStyle::default(); + + // use the first transform component + if transform.is_none() { + transform = Some(node.borrow().transform()); } - ) - // assert that the shape has a style. If it doesn't have a style, we can't draw it, - // so there is no point in parsing it - .filter_map(|(id, attributes)| { - let svg_style = match attributes.get("style") { - Some(style_string) => SvgStyle::from_svg_string(style_string), - _ => return None, - }; - Some((id, svg_style, attributes)) - }) - // Now parse the shape - .filter_map(|(id, style, attributes)| { - let layer_data = match id { - "path" => match parse_path(&attributes) { - None => return None, - Some(s) => SvgLayerType::Polygon(s), - } - "circle" => match parse_circle(&attributes) { - None => return None, - Some(s) => SvgLayerType::Circle(s), - }, - "rect" => match parse_rect(&attributes) { - None => return None, - Some(s) => SvgLayerType::Rect(s), - }, - "flowRoot" => match parse_flow_root(&attributes) { - None => return None, - Some(s) => SvgLayerType::Text(s), - }, - "text" => match parse_text(&attributes) { - None => return None, - Some(s) => SvgLayerType::Text(s), - }, - _ => return None, - }; - Some((layer_data, style)) - }) - .map(|(data, style)| { - SvgLayer { - data: vec![data], + + if let Some(ref fill) = p.fill { + // fall back to always use color fill + // no gradients (yet?) + let color = match fill.paint { + Paint::Color(c) => c, + _ => FALLBACK_COLOR, + }; + + style.fill = Some(ColorU { + r: color.red, + g: color.green, + b: color.blue, + a: (fill.opacity.value() * 255.0) as u8 + }); + } + + if let Some(ref stroke) = p.stroke { + style.stroke = Some(convert_stroke(stroke)); + } + + let transform_id = transform.and_then(|t| { + let new_id = new_svg_transform_id(); + transforms.insert(new_id, t.clone()); + Some(new_id) + }); + + layer_data.push(SvgLayer { + data: SvgLayerType::Polygon(p.segments.iter().map(|e| as_event(e)).collect()), callbacks: SvgCallbacks::None, style: style, - } - }) - .collect(); + transform_id: transform_id, + view_box_id: view_box_id, + }) + } + } - if let Some(e) = last_err { - Err(e.into()) - } else { - Ok(layer_data) + Ok((layer_data, transforms)) + } + + // Map resvg::tree::PathSegment to lyon::path::PathEvent + fn as_event(ps: &PathSegment) -> PathEvent { + match *ps { + PathSegment::MoveTo { x, y } => PathEvent::MoveTo(Point::new(x as f32, y as f32)), + PathSegment::LineTo { x, y } => PathEvent::LineTo(Point::new(x as f32, y as f32)), + PathSegment::CurveTo { x1, y1, x2, y2, x, y, } => { + PathEvent::CubicTo( + Point::new(x1 as f32, y1 as f32), + Point::new(x2 as f32, y2 as f32), + Point::new(x as f32, y as f32)) + } + PathSegment::ClosePath => PathEvent::Close, } } - fn parse_path(attributes: &Attributes) -> Option { - use lyon::path::default::Builder; - use lyon::path::builder::SvgPathBuilder; - use lyon::path::builder::FlatPathBuilder; - use lyon::path::SvgEvent; - use svg_crate::node::element::{ - tag::Path, - path::{Command, Command::*, Data}, - }; - use svg_crate::node::element::path::Position::*; + pub struct PathConv<'a>(SegmentIter<'a>); - let data = attributes.get("d")?; - let data = Data::parse(data).ok()?; + // Alias for the iterator returned by resvg::tree::Path::iter() + type SegmentIter<'a> = slice::Iter<'a, PathSegment>; - let mut builder = SvgPathBuilder::new(Builder::new()); + // Alias for our `interface` iterator + type PathConvIter<'a> = iter::Map, fn(&PathSegment) -> PathEvent>; - for command in data.iter() { - match command { - Move(position, parameters) => match position { - Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::MoveTo(TypedPoint2D::new(x, y))), - _ => { }, - }), - Relative => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::RelativeMoveTo(TypedVector2D::new(x, y))), - _ => { }, - }), - }, - Line(position, parameters) => match position { - Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::LineTo(TypedPoint2D::new(x, y))), - _ => { }, - }), - Relative => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::RelativeLineTo(TypedVector2D::new(x, y))), - _ => { }, - }), - }, - HorizontalLine(position, parameters) => match position { - Absolute => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::HorizontalLineTo(*num))), - Relative => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::RelativeHorizontalLineTo(*num))), - }, - VerticalLine(position, parameters) => match position { - Absolute => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::VerticalLineTo(*num))), - Relative => parameters.iter().for_each(|num| builder.svg_event(SvgEvent::RelativeVerticalLineTo(*num))), - }, - QuadraticCurve(position, parameters) => match position { - Absolute => parameters.chunks(4).for_each(|chunk| match *chunk { - [x1, y1, x2, y2] => builder.svg_event(SvgEvent::QuadraticTo(TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2))), - _ => { }, - }), - Relative => parameters.chunks(4).for_each(|chunk| match *chunk { - [x1, y1, x2, y2] => builder.svg_event(SvgEvent::RelativeQuadraticTo( - TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2))), - _ => { }, - }), - }, - SmoothQuadraticCurve(position, parameters) => match position { - Absolute => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::SmoothQuadraticTo(TypedPoint2D::new(x, y))), - _ => { }, - }), - Relative => parameters.chunks(2).for_each(|chunk| match *chunk { - [x, y] => builder.svg_event(SvgEvent::SmoothRelativeQuadraticTo(TypedVector2D::new(x, y))), - _ => { }, - }), - }, - CubicCurve(position, parameters) => match position { - Absolute => parameters.chunks(6).for_each(|chunk| match *chunk { - [x1, y1, x2, y2, x3, y3] => builder.svg_event(SvgEvent::CubicTo( - TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2), TypedPoint2D::new(x3, y3))), - _ => { }, - }), - Relative => parameters.chunks(6).for_each(|chunk| match *chunk { - [x1, y1, x2, y2, x3, y3] => builder.svg_event(SvgEvent::RelativeCubicTo( - TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2), TypedVector2D::new(x3, y3))), - _ => { }, - }), - }, - SmoothCubicCurve(position, parameters) => match position { - Absolute => parameters.chunks(4).for_each(|chunk| match *chunk { - [x1, y1, x2, y2] => builder.svg_event(SvgEvent::SmoothCubicTo( - TypedPoint2D::new(x1, y1), TypedPoint2D::new(x2, y2))), - _ => { }, - }), - Relative => parameters.chunks(4).for_each(|chunk| match *chunk { - [x1, y1, x2, y2] => builder.svg_event(SvgEvent::SmoothRelativeCubicTo( - TypedVector2D::new(x1, y1), TypedVector2D::new(x2, y2))), - _ => { }, - }), - }, - EllipticalArc(position, parameters) => match position { - Absolute => parameters.chunks(5).for_each(|chunk| match *chunk { - [x1, y1, angle, x2, y2] => builder.svg_event( - SvgEvent::ArcTo( - TypedVector2D::new(x1, y1), - Angle::degrees(angle), - ArcFlags { large_arc: true, sweep: true, }, - TypedPoint2D::new(x2, y2) - )), - _ => { }, - }), - Relative => parameters.chunks(5).for_each(|chunk| match *chunk { - [x1, y1, angle, x2, y2] => builder.svg_event( - SvgEvent::ArcTo( - TypedVector2D::new(x1, y1), - Angle::degrees(angle), - ArcFlags { large_arc: true, sweep: true, }, - TypedPoint2D::new(x2, y2) - )), - _ => { }, - }), - }, - Close => { - builder.close(); - }, - } + // Provide a function which gives back a PathIter which is compatible with + // tesselators, so we don't have to implement the PathIterator trait + impl<'a> PathConv<'a> { + pub fn path_iter(self) -> PathIter> { + PathIter::new(self.0.map(as_event)) } + } - Some(builder.build()) + pub fn convert_path<'a>(p: &'a Path) -> PathConv<'a> { + PathConv(p.segments.iter()) } - fn parse_circle(attributes: &Attributes) -> Option { - let center_x = attributes.get("cx")?.parse::().ok()?; - let center_y = attributes.get("cy")?.parse::().ok()?; - let radius = attributes.get("r")?.parse::().ok()?; + pub const FALLBACK_COLOR: Color = Color { + red: 0, + green: 0, + blue: 0, + }; - Some(SvgCircle { - center_x, - center_y, - radius - }) - } + // dissect a resvg::Stroke into a webrender::ColorU + SvgStrokeOptions + pub fn convert_stroke(s: &Stroke) -> (ColorU, SvgStrokeOptions) { - fn parse_rect(attributes: &Attributes) -> Option { - let width = attributes.get("width")?.parse::().ok()?; - let height = attributes.get("height")?.parse::().ok()?; - let x = attributes.get("x")?.parse::().ok()?; - let y = attributes.get("y")?.parse::().ok()?; - let rx = attributes.get("rx")?.parse::().ok()?; - let ry = attributes.get("ry")?.parse::().ok()?; - Some(SvgRect { - width, - height, - x, y, - rx, ry - }) - } + let color = match s.paint { + Paint::Color(c) => c, + _ => FALLBACK_COLOR, + }; + let line_cap = match s.linecap { + LineCap::Butt => SvgLineCap::Butt, + LineCap::Square => SvgLineCap::Square, + LineCap::Round => SvgLineCap::Round, + }; + let line_join = match s.linejoin { + LineJoin::Miter => SvgLineJoin::Miter, + LineJoin::Bevel => SvgLineJoin::Bevel, + LineJoin::Round => SvgLineJoin::Round, + }; - // TODO: use text attributes instead of string - fn parse_flow_root(attributes: &Attributes) -> Option { - Some(String::from("hello")) - } + let opts = SvgStrokeOptions { + line_width: ((s.width as f32) * 1000.0) as usize, + start_cap: line_cap, + end_cap: line_cap, + line_join, + .. Default::default() + }; - // TODO: use text attributes instead of string - fn parse_text(attributes: &Attributes) -> Option { - Some(String::from("hello")) + (ColorU { + r: color.red, + g: color.green, + b: color.blue, + a: (s.opacity.value() * 255.0) as u8 + }, opts) } } diff --git a/src/window.rs b/src/window.rs index db4754793..2f7c139ca 100644 --- a/src/window.rs +++ b/src/window.rs @@ -372,7 +372,7 @@ impl RenderNotifier for Notifier { self.events_loop_proxy.wakeup().unwrap_or_else(|_| { eprintln!("couldn't wakeup event loop"); }); } - fn new_frame_ready(&self, _id: DocumentId, _scrolled: bool, _composite_needed: bool) { + fn new_frame_ready(&self, _id: DocumentId, _scrolled: bool, _composite_needed: bool, _render_time: Option) { self.wake_up(); } } From 5422fd854b6c128bc7f54cac67553c0bb84a0928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 08:48:30 +0200 Subject: [PATCH 104/868] Use the tiger.svg for the debug example --- assets/svg/tiger.svg | 725 +++++++++++++++++++++++++++++++++++++++++++ examples/debug.rs | 22 +- 2 files changed, 732 insertions(+), 15 deletions(-) create mode 100644 assets/svg/tiger.svg diff --git a/assets/svg/tiger.svg b/assets/svg/tiger.svg new file mode 100644 index 000000000..679edec2e --- /dev/null +++ b/assets/svg/tiger.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/debug.rs b/examples/debug.rs index 8af2a1cdb..e4ff4bbd6 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -7,8 +7,7 @@ use azul::widgets::*; const TEST_CSS: &str = include_str!("test_content.css"); const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); -const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); -const TEST_SVG: &[u8] = include_bytes!("../assets/svg/test.svg"); +const TEST_SVG: &[u8] = include_bytes!("../assets/svg/tiger.svg"); #[derive(Debug)] pub struct MyAppData { @@ -31,9 +30,13 @@ impl Layout for MyAppData { fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { // Load and parse the SVG file, register polygon data as IDs + use std::time::{Instant}; + let start_time_loading_svg = Instant::now(); let mut svg_cache = SvgCache::empty(); - let svg_layers= svg_cache.add_svg(TEST_SVG).unwrap(); + let svg_layers = svg_cache.add_svg(TEST_SVG).unwrap(); app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); + let time = (Instant::now() - start_time_loading_svg).subsec_nanos() as f32 / 1_000_000.0; + println!("time loading svg: {} milliseconds", time); UpdateScreen::Redraw } @@ -41,20 +44,9 @@ fn main() { // Parse and validate the CSS let css = Css::new_from_string(TEST_CSS).unwrap(); - - let my_app_data = MyAppData { - svg: None, - }; - - let mut app = App::new(my_app_data); + let mut app = App::new(MyAppData { svg: None }); app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); - // app.delete_font("Webly Sleeky UI"); - - app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); - // app.delete_image("Cat01"); - - // app.create_window(WindowCreateOptions::default(), css.clone()).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } From bf1f891861d4a3aeb86183b1f133f1f6ed124782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 08:48:51 +0200 Subject: [PATCH 105/868] Optimize SvgTransformID to be a NonZero --- src/svg.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/svg.rs b/src/svg.rs index 8bb4ad7de..2ef18c875 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -20,17 +20,18 @@ use euclid::TypedRect; use dom::Callback; use traits::Layout; use FastHashMap; +use id_tree::NonZeroUsizeHack; /// In order to store / compare SVG files, we have to pub(crate) static SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct SvgTransformId(usize); +pub struct SvgTransformId(NonZeroUsizeHack); const SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); pub fn new_svg_transform_id() -> SvgTransformId { - SvgTransformId(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst)) + SvgTransformId(NonZeroUsizeHack::new(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst))) } const SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); From b104b809e0cc3218327905902053b5532d8df2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 12:56:47 +0200 Subject: [PATCH 106/868] Added logo to docs --- assets/images/azul_logo_full_min.svg.png | Bin 0 -> 3152 bytes assets/images/favicon.ico | Bin 0 -> 43006 bytes src/lib.rs | 4 ++++ 3 files changed, 4 insertions(+) create mode 100644 assets/images/azul_logo_full_min.svg.png create mode 100644 assets/images/favicon.ico diff --git a/assets/images/azul_logo_full_min.svg.png b/assets/images/azul_logo_full_min.svg.png new file mode 100644 index 0000000000000000000000000000000000000000..b9762e8deba09928066d6df974a1bf6fa130dd31 GIT binary patch literal 3152 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 IrS16SzY6WVR{#J2 literal 0 HcmV?d00001 diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3354f2f3e20ec6b8cbfb8e29d45849523a0b3ead GIT binary patch literal 43006 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 Itz8ZHf3>JWng9R* literal 0 HcmV?d00001 diff --git a/src/lib.rs b/src/lib.rs index 94f8f2234..63ccfe251 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,10 @@ //! //! For more examples, please look in the `/examples` folder. +#![doc( + html_logo_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/azul_logo_full_min.svg.png", + html_favicon_url = "https://raw.githubusercontent.com/maps4print/azul/master/assets/images/favicon.ico", +)] #![deny(unused_must_use)] #![deny(missing_copy_implementations)] From cbd4fd08a59fed236005af7138725359088bf9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 14:12:26 +0200 Subject: [PATCH 107/868] Added SRGB color correction to SVG drawing --- Cargo.toml | 1 + src/lib.rs | 1 + src/widgets.rs | 13 ++++++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4466bfd42..abd6fe405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" +palette = "0.4.0" [dependencies.resvg] git = "https://github.com/RazrFalcon/resvg.git" diff --git a/src/lib.rs b/src/lib.rs index 63ccfe251..60498a5c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub extern crate image; #[macro_use] extern crate lazy_static; +extern crate palette; extern crate euclid; extern crate lyon; extern crate resvg; diff --git a/src/widgets.rs b/src/widgets.rs index 95160b0fb..db7b0c10a 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -103,18 +103,25 @@ impl Svg { { let mut surface = tex.as_surface(); - // TODO: cache the vertex buffers / index buffers for layer_id in &self.layers { - let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); + + use palette::Srgba; + let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); let style = svg_cache.get_style(layer_id); let color: ColorF = style.fill.unwrap_or(DEFAULT_COLOR).into(); + let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); let uniforms = uniform! { bbox_origin: (bbox.origin.x, bbox.origin.y), bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), z_index: z_index, - color: (color.r as f32, color.g as f32, color.b as f32, color.a as f32), + color: ( + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, + color.alpha as f32 + ), }; surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); From f33840919a29180fff71c592667165f6fbf766c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 30 Jun 2018 15:21:39 +0200 Subject: [PATCH 108/868] Non-working SVG stroking implementation --- Cargo.toml | 2 +- src/svg.rs | 6 ++++++ src/widgets.rs | 58 ++++++++++++++++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index abd6fe405..2a7405e6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "470d304c2ff7951a5bb600a9655f1949703e7fbb" +rev = "34a498f7e46c385a189299e7369e204e1cb2060c" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/svg.rs b/src/svg.rs index 2ef18c875..99e4381c5 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -127,6 +127,12 @@ impl SvgCache { shader_lock.as_ref().and_then(|s| Some(s.clone())).unwrap() } + pub fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + -> &'a (VertexBuffer, IndexBuffer) + { + + } + /// Note: panics if the ID isn't found. /// /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` diff --git a/src/widgets.rs b/src/widgets.rs index db7b0c10a..70ee3ed7a 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -106,25 +106,47 @@ impl Svg { for layer_id in &self.layers { use palette::Srgba; - - let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); let style = svg_cache.get_style(layer_id); - let color: ColorF = style.fill.unwrap_or(DEFAULT_COLOR).into(); - let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, - color.alpha as f32 - ), - }; - - surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + + if let Some(color) = style.fill { + let color: ColorF = color.into(); + let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); + let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, + color.alpha as f32 + ), + }; + + surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + } + + if let Some((stroke_color, stroke_options)) = style.stroke { + let stroke_color: ColorF = stroke_color.into(); + let (stroke_vertex_buffer, stroke_index_buffer) = svg_cache.get_stroke_vertices_and_indices(window, layer_id); + let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + stroke_color.color.red as f32, + stroke_color.color.green as f32, + stroke_color.color.blue as f32, + stroke_color.alpha as f32 + ), + }; + + surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + } } } From 4d46799e0b99d791680a96c93f604d4ba0beba2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 1 Jul 2018 04:40:47 +0200 Subject: [PATCH 109/868] Added stroking for SVG --- src/svg.rs | 279 ++++++++++++++++++++++++++++++++++++------------- src/widgets.rs | 20 ++-- 2 files changed, 217 insertions(+), 82 deletions(-) diff --git a/src/svg.rs b/src/svg.rs index 99e4381c5..b3059918b 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -12,10 +12,15 @@ use std::{fmt, rc::Rc, cell::UnsafeCell, hash::{Hash, Hasher}, }; -use lyon::{path::{PathEvent, default::Path}, tessellation::{LineCap, VertexBuffers, LineJoin}}; +use lyon::tessellation::{ + VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, + LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, + path::{default::{Builder, Path}, builder::{PathBuilder, FlatPathBuilder}, PathEvent}, + basic_shapes::{fill_circle, stroke_circle, fill_rounded_rectangle, stroke_rounded_rectangle, BorderRadii}, + geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, +}; use resvg::usvg::Error as SvgError; use webrender::api::{ColorU, ColorF}; -use euclid::TypedRect; use dom::Callback; use traits::Layout; @@ -89,7 +94,9 @@ pub struct SvgCache { layers: FastHashMap>, // Stores the vertices and indices necessary for drawing. Must be synchronized with the `layers` gpu_ready_to_upload_cache: FastHashMap, Vec)>, + stroke_gpu_ready_to_upload_cache: FastHashMap, Vec)>, vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, + stroke_vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, shader: Mutex>, // Stores the 2D transforms of the shapes on the screen. The vertices are // offset by the X, Y value in the transforms struct. This should be expanded @@ -104,7 +111,9 @@ impl Default for SvgCache { Self { layers: FastHashMap::default(), gpu_ready_to_upload_cache: FastHashMap::default(), + stroke_gpu_ready_to_upload_cache: FastHashMap::default(), vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), + stroke_vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), shader: Mutex::new(None), transforms: FastHashMap::default(), view_boxes: FastHashMap::default(), @@ -130,9 +139,20 @@ impl SvgCache { pub fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) -> &'a (VertexBuffer, IndexBuffer) { - + use std::collections::hash_map::Entry::*; + use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; + + let rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; + let rnotmut = &self.stroke_gpu_ready_to_upload_cache; + + rmut.entry(*id).or_insert_with(|| { + let (vbuf, ibuf) = rnotmut.get(id).as_ref().unwrap(); + let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); + (vertex_buffer, index_buffer) + }) } - + /// Note: panics if the ID isn't found. /// /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` @@ -184,32 +204,50 @@ impl fmt::Debug for SvgCache { impl SvgCache { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { - let new_svg_id = SvgLayerId(SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst)); // TODO: set tolerance based on zoom - let (vertex_buf, index_buf) = tesselate_layer_data(&layer.data, 0.01); - self.layers.insert(new_svg_id, layer); + let new_svg_id = SvgLayerId(SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst)); + + let ((vertex_buf, index_buf), opt_stroke) = + tesselate_layer_data(&layer.data, 0.01, layer.style.stroke.and_then(|s| Some(s.1.clone()))); + self.gpu_ready_to_upload_cache.insert(new_svg_id, (vertex_buf, index_buf)); - new_svg_id - } - pub fn add_transforms(&mut self, transforms: FastHashMap) { - transforms.into_iter().for_each(|(k, v)| { - self.transforms.insert(k, v); - }); + if let Some((stroke_vertex_buf, stroke_index_buf)) = opt_stroke { + self.stroke_gpu_ready_to_upload_cache.insert(new_svg_id, (stroke_vertex_buf, stroke_index_buf)); + } + + self.layers.insert(new_svg_id, layer); + + new_svg_id } pub fn delete_layer(&mut self, svg_id: SvgLayerId) { self.layers.remove(&svg_id); self.gpu_ready_to_upload_cache.remove(&svg_id); + self.stroke_gpu_ready_to_upload_cache.remove(&svg_id); let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; + let stroke_rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; rmut.remove(&svg_id); + stroke_rmut.remove(&svg_id); } pub fn clear_all_layers(&mut self) { self.layers.clear(); + self.gpu_ready_to_upload_cache.clear(); + self.stroke_gpu_ready_to_upload_cache.clear(); + let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; rmut.clear(); + + let stroke_rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; + stroke_rmut.clear(); + } + + pub fn add_transforms(&mut self, transforms: FastHashMap) { + transforms.into_iter().for_each(|(k, v)| { + self.transforms.insert(k, v); + }); } /// Parses an input source, parses the SVG, adds the shapes as layers into @@ -224,22 +262,43 @@ impl SvgCache { } } -fn tesselate_layer_data(layer_data: &SvgLayerType, tolerance: f32) -> (Vec, Vec) { +fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: Option) +-> ((Vec, Vec), Option<(Vec, Vec)>) +{ const GL_RESTART_INDEX: u32 = ::std::u32::MAX; let mut last_index = 0; let mut vertex_buf = Vec::::new(); let mut index_buf = Vec::::new(); - let VertexBuffers { vertices, indices } = layer_data.tesselate(tolerance); - let vertices_len = vertices.len(); + let mut last_stroke_index = 0; + let mut stroke_vertex_buf = Vec::::new(); + let mut stroke_index_buf = Vec::::new(); + + for layer in layer_data.get() { - vertex_buf.extend(vertices.into_iter()); - index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); - index_buf.push(GL_RESTART_INDEX); - last_index += vertices_len; + let (VertexBuffers { vertices, indices }, stroke_vertices) = layer.tesselate(tolerance, stroke_options); - (vertex_buf, index_buf) + let vertices_len = vertices.len(); + vertex_buf.extend(vertices.into_iter()); + index_buf.extend(indices.into_iter().map(|i| i as u32 + last_index as u32)); + index_buf.push(GL_RESTART_INDEX); + last_index += vertices_len; + + if let Some(VertexBuffers { vertices, indices }) = stroke_vertices { + let stroke_vertices_len = vertices.len(); + stroke_vertex_buf.extend(vertices.into_iter()); + stroke_index_buf.extend(indices.into_iter().map(|i| i as u32 + last_stroke_index as u32)); + stroke_index_buf.push(GL_RESTART_INDEX); + last_stroke_index += stroke_vertices_len; + } + } + + if stroke_options.is_some() { + ((vertex_buf, index_buf), Some((stroke_vertex_buf, stroke_index_buf))) + } else { + ((vertex_buf, index_buf), None) + } } #[derive(Debug)] @@ -263,14 +322,29 @@ impl From for SvgParseError { } pub struct SvgLayer { - pub data: SvgLayerType, + pub data: LayerType, pub callbacks: SvgCallbacks, pub style: SvgStyle, - // ID in the transform idx pub transform_id: Option, pub view_box_id: SvgViewBoxId, } +#[derive(Debug, Clone)] +pub enum LayerType { + KnownSize([SvgLayerType; 1]), + UnknownSize(Vec), +} + +impl LayerType { + pub fn get(&self) -> &[SvgLayerType] { + use self::LayerType::*; + match self { + KnownSize(a) => &a[..], + UnknownSize(b) => &b[..], + } + } +} + impl Clone for SvgLayer { fn clone(&self) -> Self { Self { @@ -385,6 +459,24 @@ pub struct SvgStrokeOptions { pub apply_line_width: bool, } +impl Into for SvgStrokeOptions { + fn into(self) -> StrokeOptions { + let target = StrokeOptions::default() + .with_tolerance(self.tolerance as f32 / 1000.0) + .with_start_cap(self.start_cap.into()) + .with_end_cap(self.end_cap.into()) + .with_line_join(self.line_join.into()) + .with_line_width(self.line_width as f32 / 1000.0) + .with_miter_limit(self.miter_limit as f32 / 1000.0); + + if !self.apply_line_width { + target.dont_apply_line_width() + } else { + target + } + } +} + impl Default for SvgStrokeOptions { fn default() -> Self { const DEFAULT_MITER_LIMIT: f32 = 4.0; @@ -416,6 +508,18 @@ impl Default for SvgLineCap { } } +impl Into for SvgLineCap { + #[inline] + fn into(self) -> LineCap { + use self::SvgLineCap::*; + match self { + Butt => LineCap::Butt, + Square => LineCap::Square, + Round => LineCap::Round, + } + } +} + #[derive(Debug, Copy, Clone, PartialEq, Hash)] pub enum SvgLineJoin { Miter, @@ -430,6 +534,19 @@ impl Default for SvgLineJoin { } } +impl Into for SvgLineJoin { + #[inline] + fn into(self) -> LineJoin { + use self::SvgLineJoin::*; + match self { + Miter => LineJoin::Miter, + MiterClip => LineJoin::MiterClip, + Round => LineJoin::Round, + Bevel => LineJoin::Bevel, + } + } +} + /// One "layer" is simply one or more polygons that get drawn using the same style /// i.e. one SVG `` element #[derive(Debug, Clone)] @@ -451,22 +568,20 @@ implement_vertex!(SvgVert, xy, normal); #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; + impl SvgLayerType { - pub fn tesselate(&self, tolerance: f32) - -> VertexBuffers + pub fn tesselate(&self, tolerance: f32, stroke: Option) + -> (VertexBuffers, Option>) { - use self::SvgLayerType::*; - use lyon::tessellation::{ - VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, - path::{default::Builder, builder::{PathBuilder, FlatPathBuilder}}, - basic_shapes::{fill_circle, fill_rounded_rectangle, BorderRadii}, - geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, - }; - let mut geometry = VertexBuffers::new(); + let mut stroke_geometry = VertexBuffers::new(); + let stroke = stroke.and_then(|s| { + let s: StrokeOptions = s.into(); + Some(s.with_tolerance(tolerance)) + }); match self { - Polygon(p) => { + SvgLayerType::Polygon(p) => { let mut builder = Builder::with_capacity(p.len()).flattened(tolerance); for event in p { builder.path_event(*event); @@ -485,11 +600,24 @@ impl SvgLayerType { }), ).unwrap(); - // TODO: stroke! + if let Some(ref stroke_options) = stroke { + let mut stroke_tess = StrokeTessellator::new(); + stroke_tess.tessellate_path( + path.path_iter(), + stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + }), + ); + } }, - Circle(c) => { - fill_circle( - TypedPoint2D::new(c.center_x, c.center_y), c.radius, &FillOptions::default(), + SvgLayerType::Circle(c) => { + let center = TypedPoint2D::new(c.center_x, c.center_y); + let radius = c.radius; + fill_circle(center, radius, &FillOptions::default(), &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { SvgVert { xy: (vertex.position.x, vertex.position.y), @@ -497,17 +625,29 @@ impl SvgLayerType { } } )); + + if let Some(ref stroke_options) = stroke { + stroke_circle(center, radius, stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + } }, - Rect(r) => { - fill_rounded_rectangle( - &TypedRect::new(TypedPoint2D::new(r.x, r.y), TypedSize2D::new(r.width, r.height)), - &BorderRadii { - top_left: r.rx, - top_right: r.rx, - bottom_left: r.rx, - bottom_right: r.rx, - }, - &FillOptions::default(), + SvgLayerType::Rect(r) => { + let size = TypedSize2D::new(r.width, r.height); + let rect = TypedRect::new(TypedPoint2D::new(r.x, r.y), size); + let radii = BorderRadii { + top_left: r.rx, + top_right: r.rx, + bottom_left: r.rx, + bottom_right: r.rx, + }; + + fill_rounded_rectangle(&rect, &radii, &FillOptions::default(), &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| { SvgVert { xy: (vertex.position.x, vertex.position.y), @@ -515,11 +655,26 @@ impl SvgLayerType { } } )); + + if let Some(ref stroke_options) = stroke { + stroke_rounded_rectangle(&rect, &radii, stroke_options, + &mut BuffersBuilder::new(&mut stroke_geometry, |vertex: StrokeVertex| { + SvgVert { + xy: (vertex.position.x, vertex.position.y), + normal: (vertex.normal.x, vertex.position.y), + } + } + )); + } }, - Text(_t) => { }, + SvgLayerType::Text(_t) => { }, } - geometry + if stroke.is_some() { + (geometry, Some(stroke_geometry)) + } else { + (geometry, None) + } } } @@ -552,7 +707,7 @@ mod svg_to_lyon { Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; use svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, SvgLayerType, SvgStyle, SvgCallbacks, SvgParseError, SvgTransformId, - new_svg_transform_id, new_view_box_id, SvgViewBoxId}; + new_svg_transform_id, new_view_box_id, SvgViewBoxId, LayerType}; use traits::Layout; use webrender::api::ColorU; use FastHashMap; @@ -612,7 +767,7 @@ mod svg_to_lyon { }); layer_data.push(SvgLayer { - data: SvgLayerType::Polygon(p.segments.iter().map(|e| as_event(e)).collect()), + data: LayerType::KnownSize([SvgLayerType::Polygon(p.segments.iter().map(|e| as_event(e)).collect())]), callbacks: SvgCallbacks::None, style: style, transform_id: transform_id, @@ -639,26 +794,6 @@ mod svg_to_lyon { } } - pub struct PathConv<'a>(SegmentIter<'a>); - - // Alias for the iterator returned by resvg::tree::Path::iter() - type SegmentIter<'a> = slice::Iter<'a, PathSegment>; - - // Alias for our `interface` iterator - type PathConvIter<'a> = iter::Map, fn(&PathSegment) -> PathEvent>; - - // Provide a function which gives back a PathIter which is compatible with - // tesselators, so we don't have to implement the PathIterator trait - impl<'a> PathConv<'a> { - pub fn path_iter(self) -> PathIter> { - PathIter::new(self.0.map(as_event)) - } - } - - pub fn convert_path<'a>(p: &'a Path) -> PathConv<'a> { - PathConv(p.segments.iter()) - } - pub const FALLBACK_COLOR: Color = Color { red: 0, green: 0, diff --git a/src/widgets.rs b/src/widgets.rs index 70ee3ed7a..72a1f728d 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -104,12 +104,12 @@ impl Svg { let mut surface = tex.as_surface(); for layer_id in &self.layers { - + use palette::Srgba; let style = svg_cache.get_style(layer_id); if let Some(color) = style.fill { - let color: ColorF = color.into(); + let color: ColorF = color.into(); let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); @@ -118,17 +118,17 @@ impl Svg { bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), z_index: z_index, color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, color.alpha as f32 ), }; surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); } - - if let Some((stroke_color, stroke_options)) = style.stroke { + + if let Some((stroke_color, _)) = style.stroke { let stroke_color: ColorF = stroke_color.into(); let (stroke_vertex_buffer, stroke_index_buffer) = svg_cache.get_stroke_vertices_and_indices(window, layer_id); let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); @@ -138,9 +138,9 @@ impl Svg { bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), z_index: z_index, color: ( - stroke_color.color.red as f32, - stroke_color.color.green as f32, - stroke_color.color.blue as f32, + stroke_color.color.red as f32, + stroke_color.color.green as f32, + stroke_color.color.blue as f32, stroke_color.alpha as f32 ), }; From b03288529fc11efc997b90bcc75bab3f78294386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 1 Jul 2018 10:57:46 +0200 Subject: [PATCH 110/868] Updated README, cleaned imports + virtual key to char function --- README.md | 5 ++++ src/app.rs | 43 +++++++++++++++------------- src/app_state.rs | 34 ++++++++++++---------- src/cache.rs | 17 +++++++---- src/compositor.rs | 14 ++++++---- src/constraints.rs | 8 ++++-- src/css.rs | 10 ++++--- src/css_parser.rs | 17 +++++------ src/lib.rs | 9 +++--- src/svg.rs | 48 +++++++++++++++++++------------- src/task.rs | 12 +++++--- src/text_cache.rs | 15 ++++++---- src/text_layout.rs | 13 ++++++--- src/traits.rs | 26 +++++++++-------- src/ui_description.rs | 25 +++++++++-------- src/ui_state.rs | 16 +++++++---- src/widgets.rs | 16 +++++------ src/window.rs | 32 +++++++++++---------- src/window_state.rs | 65 +++++++++++++++++++++++++++++++++++++++---- 19 files changed, 266 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 9560a2144..97d2ed9e2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ and a CSS / DOM model for layout and rendering [Crates.io](https://crates.io/crates/azul) | [Library documentation](https://docs.rs/azul) | [User guide](http://azul.rs/) +## Installation notes + +On Linux, you currently need to install `cmake` before you can use. It is used +during the build process to compile freetype. + ## Design azul is a library designed from the experience gathered during working with other diff --git a/src/app.rs b/src/app.rs index ed4c21614..f9fc39023 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,24 +1,27 @@ -use css_parser::{Font as FontId, PixelValue, FontSize}; -use text_cache::TextId; -use dom::UpdateScreen; -use window::FakeWindow; -use css::{Css, FakeCss}; -use resources::AppResources; -use app_state::AppState; -use traits::Layout; -use ui_state::UiState; -use ui_description::UiDescription; -use std::sync::{Arc, Mutex, PoisonError}; -use window::{Window, WindowCreateOptions, WindowCreateError, WindowId}; -use glium::glutin::Event; -use euclid::TypedScale; -use std::io::Read; -use images::{ImageType}; -use image::ImageError; -use font::FontError; +use std::{ + fmt, + io::Read, + sync::{Arc, Mutex, PoisonError}, +}; +use glium::{SwapBuffersError, glutin::Event}; use webrender::api::{RenderApi, HitTestFlags}; -use glium::SwapBuffersError; -use std::fmt; +use image::ImageError; +use euclid::TypedScale; +use { + images::ImageType, + errors::FontError, + window::{Window, WindowCreateOptions, WindowCreateError, WindowId}, + css_parser::{Font as FontId, PixelValue, FontSize}, + text_cache::TextId, + dom::UpdateScreen, + window::FakeWindow, + css::{Css, FakeCss}, + resources::AppResources, + app_state::AppState, + traits::Layout, + ui_state::UiState, + ui_description::UiDescription, +}; /// Graphical application that maintains some kind of application state pub struct App<'a, T: Layout> { diff --git a/src/app_state.rs b/src/app_state.rs index 9f4a0a3eb..fc61b9416 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,19 +1,23 @@ -use text_cache::TextId; -use window::FakeWindow; -use window_state::WindowState; -use task::Task; -use dom::UpdateScreen; -use traits::Layout; -use resources::{AppResources}; -use std::io::Read; -use images::ImageType; +use std::{ + io::Read, + collections::hash_map::Entry::*, + sync::{Arc, Mutex}, +}; use image::ImageError; -use font::FontError; -use std::collections::hash_map::Entry::*; -use FastHashMap; -use std::sync::{Arc, Mutex}; -use svg::{SvgLayerId, SvgLayer, SvgParseError}; -use css_parser::{Font as FontId, FontSize, PixelValue}; +use { + FastHashMap, + text_cache::TextId, + window::FakeWindow, + window_state::WindowState, + task::Task, + dom::UpdateScreen, + traits::Layout, + resources::AppResources, + images::ImageType, + font::FontError, + svg::{SvgLayerId, SvgLayer, SvgParseError}, + css_parser::{Font as FontId, FontSize, PixelValue}, +}; /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application diff --git a/src/cache.rs b/src/cache.rs index b946dc2e7..e3a50acb3 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -35,13 +35,18 @@ //! meaning that the variable was not present in the current DOM tree, so leaving the variables in the solver //! would be garbage. -use std::collections::BTreeMap; -use constraints::DisplayRect; +use std::{ + ops::Deref, + collections::BTreeMap, +}; use cassowary::Solver; -use id_tree::{NodeId, Arena}; -use traits::Layout; -use dom::NodeData; -use std::ops::Deref; + +use { + constraints::DisplayRect, + id_tree::{NodeId, Arena}, + traits::Layout, + dom::NodeData, +}; /// We keep the tree from the previous re-layout. Then, when a re-layout is required, /// we re-hash all the nodes, insert the diff --git a/src/compositor.rs b/src/compositor.rs index dce8423d3..6413397bd 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -2,20 +2,22 @@ //! This makes it possible to use OpenGL images in the background and compose SVG elements //! into the UI. -use dom::Texture; -use {FastHashMap, FastHashSet}; -use webrender::{ExternalImageHandler, ExternalImageSource}; -use webrender::api::{ExternalImageId, TexelRect, DevicePixel}; use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}; - +use webrender::{ + ExternalImageHandler, ExternalImage, ExternalImageSource, + api::{ExternalImageId, TexelRect, DevicePixel}, +}; use glium::{ Program, VertexBuffer, Display, index::{NoIndices, PrimitiveType::TriangleStrip}, texture::texture2d::Texture2d, backend::Facade, }; -use webrender::ExternalImage; use euclid::TypedPoint2D; +use { + FastHashMap, FastHashSet, + dom::Texture, +}; lazy_static! { /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long diff --git a/src/constraints.rs b/src/constraints.rs index 0576ef39c..3c8e3f85e 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -1,8 +1,10 @@ //! Constraint building (mostly taken from `limn_layout`) -use cassowary::{Solver, Variable, Constraint}; -use cassowary::WeightedRelation::{EQ, GE}; -use cassowary::strength::{WEAK, REQUIRED}; +use cassowary::{ + Solver, Variable, Constraint, + WeightedRelation::{EQ, GE}, + strength::{WEAK, REQUIRED}, +}; use euclid::{Point2D, Size2D}; pub type Size = Size2D; diff --git a/src/css.rs b/src/css.rs index b20952e8c..90b6c5a4b 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,9 +1,11 @@ //! CSS parsing and styling -use traits::IntoParsedCssProperty; -use FastHashMap; use std::ops::Add; -use css_parser::{ParsedCssProperty, CssParsingError}; -use errors::CssSyntaxError; +use { + FastHashMap, + traits::IntoParsedCssProperty, + css_parser::{ParsedCssProperty, CssParsingError}, + errors::CssSyntaxError, +}; #[cfg(target_os="windows")] const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); diff --git a/src/css_parser.rs b/src/css_parser.rs index 6318da355..614c8405a 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1,15 +1,16 @@ //! Contains utilities to convert strings (CSS strings) to servo types -pub use euclid::{TypedSize2D, SideOffsets2D}; -pub use webrender::api::{ - BorderRadius, BorderWidths, BorderDetails, NormalBorder, - NinePatchBorder, LayoutPixel, BoxShadowClipMode, ColorU, - ColorF, LayoutVector2D, Gradient, RadialGradient, LayoutPoint, - LayoutSize, ExtendMode +use std::num::{ParseIntError, ParseFloatError}; +pub use { + euclid::{TypedSize2D, SideOffsets2D}, + webrender::api::{ + BorderRadius, BorderWidths, BorderDetails, NormalBorder, + NinePatchBorder, LayoutPixel, BoxShadowClipMode, ColorU, + ColorF, LayoutVector2D, Gradient, RadialGradient, LayoutPoint, + LayoutSize, ExtendMode + }, }; -// TODO: 9patch images! use webrender::api::{BorderStyle, BorderSide, LayoutRect}; -use std::num::{ParseIntError, ParseFloatError}; use euclid::{TypedRotation2D, Angle, TypedPoint2D}; pub(crate) const EM_HEIGHT: f32 = 16.0; diff --git a/src/lib.rs b/src/lib.rs index 60498a5c8..425c92699 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,10 +116,9 @@ pub mod prelude { pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; pub use traits::{Layout, ModifyAppState, GetDom}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, - MouseMode, UpdateBehaviour, UpdateMode, WindowCreateError, + MouseMode, UpdateBehaviour, UpdateMode, WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; pub use window_state::WindowState; - pub use font::FontError; pub use images::ImageType; pub use css_parser::{ ParsedCssProperty, BorderRadius, BackgroundColor, TextColor, @@ -137,9 +136,6 @@ pub mod prelude { }; pub use svg::{SvgLayerId, SvgLayer, SvgCache}; - - // from the extern crate image - pub use image::ImageError; } /// Re-exports of errors @@ -154,4 +150,7 @@ pub mod errors { pub use simplecss::Error as CssSyntaxError; pub use css::{CssParseError, DynamicCssParseError}; pub use svg::SvgParseError; + pub use font::FontError; + pub use window::WindowCreateError; + pub use image::ImageError; } diff --git a/src/svg.rs b/src/svg.rs index b3059918b..e2ee03090 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,31 +1,39 @@ -use resvg::usvg::ViewBox; -use resvg::usvg::Transform; -use std::sync::Mutex; +use std::{ + fmt, + rc::Rc, + io::{Error as IoError, Read}, + sync::{Mutex, atomic::{Ordering, AtomicUsize}}, + cell::UnsafeCell, + hash::{Hash, Hasher}, +}; use glium::{ backend::Facade, DrawParameters, IndexBuffer, VertexBuffer, Display, Texture2d, Program, }; -use std::{fmt, rc::Rc, - io::{Error as IoError, Read}, - sync::atomic::{Ordering, AtomicUsize}, - cell::UnsafeCell, - hash::{Hash, Hasher}, -}; -use lyon::tessellation::{ - VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, - LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, - path::{default::{Builder, Path}, builder::{PathBuilder, FlatPathBuilder}, PathEvent}, - basic_shapes::{fill_circle, stroke_circle, fill_rounded_rectangle, stroke_rounded_rectangle, BorderRadii}, +use lyon::{ + tessellation::{ + VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, + LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, + basic_shapes::{ + fill_circle, stroke_circle, fill_rounded_rectangle, + stroke_rounded_rectangle, BorderRadii + }, + }, + path::{ + default::{Builder, Path}, + builder::{PathBuilder, FlatPathBuilder}, PathEvent, + }, geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, }; -use resvg::usvg::Error as SvgError; +use resvg::usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF}; - -use dom::Callback; -use traits::Layout; -use FastHashMap; -use id_tree::NonZeroUsizeHack; +use { + FastHashMap, + dom::Callback, + traits::Layout, + id_tree::NonZeroUsizeHack, +}; /// In order to store / compare SVG files, we have to pub(crate) static SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); diff --git a/src/task.rs b/src/task.rs index 0eed005dc..8efa2492f 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,9 +1,13 @@ //! Preliminary async IO / Task system -use app_state::AppState; -use traits::Layout; -use std::sync::{Arc, Mutex, Weak}; -use std::thread::{spawn, JoinHandle}; +use std::{ + sync::{Arc, Mutex, Weak}, + thread::{spawn, JoinHandle}, +}; +use { + app_state::AppState, + traits::Layout, +}; pub struct Task { // Task is in progress diff --git a/src/text_cache.rs b/src/text_cache.rs index bcaf20bcd..74d5e44e3 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -1,9 +1,12 @@ -use css_parser::FontSize; -use text_layout::SemanticWordItem; -use FastHashMap; -use std::sync::atomic::{Ordering, AtomicUsize}; -use css_parser::Font; -use std::rc::Rc; +use std::{ + rc::Rc, + sync::atomic::{Ordering, AtomicUsize}, +}; +use { + FastHashMap, + css_parser::{Font, FontSize}, + text_layout::SemanticWordItem, +}; static TEXT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); diff --git a/src/text_layout.rs b/src/text_layout.rs index 130a22c30..6b19534fc 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,11 +1,16 @@ #![allow(unused_variables, dead_code)] -use resources::AppResources; -use display_list::TextInfo; -use webrender::api::*; +use webrender::api::{LayoutPixel, GlyphInstance}; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; -use css_parser::{TextAlignmentHorz, FontSize, BackgroundColor, Font as FontId, TextAlignmentVert, LineHeight, LayoutOverflow}; +use { + resources::AppResources, + display_list::TextInfo, + css_parser::{ + TextAlignmentHorz, FontSize, BackgroundColor, + Font as FontId, TextAlignmentVert, LineHeight, LayoutOverflow + }, +}; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing diff --git a/src/traits.rs b/src/traits.rs index d5ad17024..a44cda463 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,14 +1,18 @@ -use std::collections::BTreeMap; -use dom::{NodeData, Dom}; -use ui_description::{StyledNode, CssConstraintList, UiDescription}; -use css::{Css, CssRule}; -use window::WindowInfo; -use id_tree::{NodeId, Arena}; -use std::rc::Rc; -use std::cell::RefCell; -use std::hash::Hash; -use css_parser::{ParsedCssProperty, CssParsingError}; -use std::sync::{Arc, Mutex}; +use std::{ + collections::BTreeMap, + rc::Rc, + cell::RefCell, + hash::Hash, + sync::{Arc, Mutex}, +}; +use { + dom::{NodeData, Dom}, + ui_description::{StyledNode, CssConstraintList, UiDescription}, + css::{Css, CssRule}, + window::WindowInfo, + id_tree::{NodeId, Arena}, + css_parser::{ParsedCssProperty, CssParsingError}, +}; pub trait GetDom { fn dom(self) -> Dom; diff --git a/src/ui_description.rs b/src/ui_description.rs index fb7f1d34e..007452024 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -1,14 +1,17 @@ -use css_parser::ParsedCssProperty; -use FastHashMap; -use id_tree::{Arena, NodeId}; -use traits::Layout; -use ui_state::UiState; -use css::Css; -use dom::NodeData; -use std::cell::RefCell; -use std::rc::Rc; -use std::collections::BTreeMap; -use css::CssDeclaration; +use std::{ + cell::RefCell, + rc::Rc, + collections::BTreeMap, +}; +use { + FastHashMap, + css_parser::ParsedCssProperty, + id_tree::{Arena, NodeId}, + traits::Layout, + ui_state::UiState, + css::{Css, CssDeclaration}, + dom::NodeData, +}; pub struct UiDescription { pub(crate) ui_descr_arena: Rc>>>, diff --git a/src/ui_state.rs b/src/ui_state.rs index 095eef716..db2d68956 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -1,9 +1,13 @@ -use traits::Layout; -use window::WindowInfo; -use std::collections::BTreeMap; -use dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}; -use app_state::AppState; -use std::fmt; +use std::{ + fmt, + collections::BTreeMap, +}; +use { + window::WindowInfo, + traits::Layout, + dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}, + app_state::AppState, +}; pub struct UiState { pub dom: Dom, diff --git a/src/widgets.rs b/src/widgets.rs index 72a1f728d..32a951da1 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,12 +1,10 @@ -#![allow(non_snake_case)] - -use svg::SvgCache; -use svg::SvgLayerId; -use window::ReadOnlyWindow; -use traits::GetDom; -use traits::Layout; -use dom::{Dom, NodeType}; -use images::ImageId; +use { + svg::{SvgCache, SvgLayerId}, + window::ReadOnlyWindow, + traits::{GetDom, Layout}, + dom::{Dom, NodeType}, + images::ImageId, +}; // --- button diff --git a/src/window.rs b/src/window.rs index 2f7c139ca..3266dd46e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,10 @@ //! Window creation module -use dom::Texture; -use glium::backend::Facade; -use glium::backend::Context; -use css::FakeCss; -use window_state::{WindowState, WindowPosition}; -use std::{time::Duration, fmt, rc::Rc}; - +use std::{ + time::Duration, + fmt, + rc::Rc +}; use webrender::{ api::*, Renderer, RendererOptions, RendererKind, @@ -17,7 +15,7 @@ use glium::{ debug::DebugCallbackBehavior, glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, - backend::glutin::DisplayCreationError, + backend::{Context, Facade, glutin::DisplayCreationError}, }; use gleam::gl; use euclid::TypedScale; @@ -26,13 +24,17 @@ use cassowary::{ strength::*, }; -use display_list::SolvedLayout; -use traits::Layout; -use css::Css; -use cache::{EditVariableCache, DomTreeCache}; -use id_tree::NodeId; -use compositor::Compositor; -use app::FrameEventInfo; +use { + dom::Texture, + css::{Css, FakeCss}, + window_state::{WindowState, WindowPosition}, + display_list::SolvedLayout, + traits::Layout, + cache::{EditVariableCache, DomTreeCache}, + id_tree::NodeId, + compositor::Compositor, + app::FrameEventInfo, +}; /// azul-internal ID for a window #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] diff --git a/src/window_state.rs b/src/window_state.rs index ff7c2b30d..bd99ef8c0 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -3,11 +3,12 @@ use glium::glutin::{ Window, Event, WindowEvent, KeyboardInput, ElementState, - MouseCursor, VirtualKeyCode, MouseButton, MouseScrollDelta, TouchPhase + MouseCursor, VirtualKeyCode, MouseButton, MouseScrollDelta, TouchPhase, +}; +use { + dom::On, + menu::{ApplicationMenu, ContextMenu}, }; - -use dom::On; -use menu::{ApplicationMenu, ContextMenu}; const DEFAULT_TITLE: &str = "Azul App"; const DEFAULT_WIDTH: u32 = 800; @@ -168,9 +169,61 @@ fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { } } -// TODO fn virtual_key_code_to_char(code: VirtualKeyCode) -> Option { - Some('a') + use glium::glutin::VirtualKeyCode::*; + match code { + Key1 => Some('1'), + Key2 => Some('2'), + Key3 => Some('3'), + Key4 => Some('4'), + Key5 => Some('5'), + Key6 => Some('6'), + Key7 => Some('7'), + Key8 => Some('8'), + Key9 => Some('9'), + Key0 => Some('0'), + A => Some('a'), + B => Some('b'), + C => Some('c'), + D => Some('d'), + E => Some('e'), + F => Some('f'), + G => Some('g'), + H => Some('h'), + I => Some('i'), + J => Some('j'), + K => Some('k'), + L => Some('l'), + M => Some('m'), + N => Some('n'), + O => Some('a'), + P => Some('p'), + Q => Some('q'), + R => Some('r'), + S => Some('s'), + T => Some('t'), + U => Some('u'), + V => Some('v'), + W => Some('w'), + X => Some('x'), + Y => Some('y'), + Z => Some('z'), + Return | NumpadEnter => Some('\n'), + Space => Some(' '), + Caret => Some('^'), + Apostrophe => Some('\''), + Backslash => Some('\\'), + Colon | Period=> Some('.'), + Comma | NumpadComma => Some(','), + Divide | Slash => Some('/'), + Equals | NumpadEquals => Some('='), + Grave => Some('´'), + Minus | Subtract => Some('-'), + Multiply => Some('*'), + Semicolon => Some(':'), + Tab => Some('\t'), + _ => None + } } // Empty test, for some reason codecov doesn't detect any files (and therefore From e40f16aee84b36e947cf09e11e788b46fddab83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 1 Jul 2018 11:52:34 +0200 Subject: [PATCH 111/868] Make seperate layout trait for unit tests + start writing red/green DOM tests --- src/dom.rs | 97 ++++++++++++++++++++++++++++++++++++++----------- src/id_tree.rs | 29 ++++++++------- src/traits.rs | 3 ++ src/ui_state.rs | 11 +++++- 4 files changed, 102 insertions(+), 38 deletions(-) diff --git a/src/dom.rs b/src/dom.rs index 3bdd9af53..f86a09545 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -1,22 +1,23 @@ -use window::WindowEvent; -use traits::GetCssId; -use app_state::AppState; -use traits::Layout; -use std::collections::BTreeMap; -use id_tree::{NodeId, Arena}; -use std::sync::{Arc, Mutex}; -use std::fmt; -use std::rc::Rc; -use std::cell::RefCell; -use std::hash::{Hash, Hasher}; +use std::{ + fmt, + rc::Rc, + cell::RefCell, + hash::{Hash, Hasher}, + sync::atomic::{AtomicUsize, Ordering}, + collections::BTreeMap, +}; use webrender::api::ColorU; -use glium::Texture2d; -use svg::SvgLayerId; -use images::ImageId; -use cache::DomHash; -use text_cache::TextId; -use glium::framebuffer::SimpleFrameBuffer; -use std::sync::atomic::{AtomicUsize, Ordering}; +use glium::{Texture2d, framebuffer::SimpleFrameBuffer}; +use { + window::WindowEvent, + svg::SvgLayerId, + images::ImageId, + cache::DomHash, + text_cache::TextId, + traits::{Layout, GetCssId}, + app_state::AppState, + id_tree::{NodeId, Arena}, +}; /// This is only accessed from the main thread, so it's safe to use pub(crate) static NODE_ID: AtomicUsize = AtomicUsize::new(0); @@ -436,10 +437,62 @@ impl Dom { } } -// Empty test, for some reason codecov doesn't detect any files (and therefore -// doesn't report codecov % correctly) except if they have at least one test in -// the file. This is an empty test, which should be updated later on #[test] -fn __codecov_test_dom_file() { +fn test_dom_sibling_1() { + + use window::WindowInfo; + + struct TestLayout { } + + impl Layout for TestLayout { + fn layout(&self) -> Dom { + Dom::new(NodeType::Div) + .with_child( + Dom::new(NodeType::Div) + .with_id("sibling-1") + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-1-child-1"))) + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-2") + .with_child(Dom::new(NodeType::Div) + .with_id("sibling-2-child-1"))) + } + } + let dom = TestLayout{ }.layout(); + let arena = dom.arena.borrow(); + + assert_eq!(NodeId::new(0), dom.root); + + assert_eq!(Some(String::from("sibling-1")), + arena[ + arena[dom.root] + .first_child().expect("root has no first child") + ].data.id); + + assert_eq!(Some(String::from("sibling-2")), + arena[ + arena[ + arena[dom.root] + .first_child().expect("root has no first child") + ].next_sibling().expect("root has no second sibling") + ].data.id); + + assert_eq!(Some(String::from("sibling-1-child-1")), + arena[ + arena[ + arena[dom.root] + .first_child().expect("root has no first child") + ].first_child().expect("first child has no first child") + ].data.id); + + assert_eq!(Some(String::from("sibling-2-child-1")), + arena[ + arena[ + arena[ + arena[dom.root] + .first_child().expect("root has no first child") + ].next_sibling().expect("first child has no second sibling") + ].first_child().expect("second sibling has no first child") + ].data.id); } \ No newline at end of file diff --git a/src/id_tree.rs b/src/id_tree.rs index c085c7f82..19e5fdcfd 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -119,20 +119,21 @@ pub struct Node { // Manual implementation, since `#[derive(Debug)]` requires `T: Debug` impl fmt::Debug for Node { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Node {{ \ - parent: {:?}, \ - previous_sibling: {:?}, \ - next_sibling: {:?}, \ - first_child: {:?}, \ - last_child: {:?}, \ - data: {:?}, \ - }}", - self.parent, - self.previous_sibling, - self.next_sibling, - self.first_child, - self.last_child, - self.data) + write!(f, + "Node {{ \ + parent: {:?}, \ + previous_sibling: {:?}, \ + next_sibling: {:?}, \ + first_child: {:?}, \ + last_child: {:?}, \ + data: {:?}, \ + }}", + self.parent, + self.previous_sibling, + self.next_sibling, + self.first_child, + self.last_child, + self.data) } } diff --git a/src/traits.rs b/src/traits.rs index a44cda463..d6810b70d 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -27,7 +27,10 @@ pub trait Layout { /// The `style_dom` looks through the given DOM rules, applies the style and /// recalculates the layout. This is done on each frame (except there are shortcuts /// when the DOM doesn't have to be recalculated). + #[cfg(not(test))] fn layout(&self, window_id: WindowInfo) -> Dom where Self: Sized; + #[cfg(test)] + fn layout(&self) -> Dom where Self: Sized; /// Applies the CSS styles to the nodes calculated from the `layout_screen` /// function and calculates the final display list that is submitted to the /// renderer. diff --git a/src/ui_state.rs b/src/ui_state.rs index db2d68956..5215d1371 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -30,15 +30,22 @@ impl fmt::Debug for UiState { } impl UiState { + #[allow(unused_imports, unused_variables)] pub(crate) fn from_app_state(app_state: &AppState, window_info: WindowInfo) -> Self { - use dom::{Dom, On}; + use dom::{Dom, On, NodeType}; use std::sync::atomic::Ordering; // Only shortly lock the data to get the dom out let dom: Dom = { let dom_lock = app_state.data.lock().unwrap(); - dom_lock.layout(window_info) + #[cfg(test)]{ + Dom::::new(NodeType::Div) + } + + #[cfg(not(test))]{ + dom_lock.layout(window_info) + } }; NODE_ID.swap(0, Ordering::SeqCst); From 819241debfba3c7ae9f49685e6c5f517d5266316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 08:07:02 +0200 Subject: [PATCH 112/868] Updated README to reflect latest changes --- README.md | 603 +++++++++++++++++++++++++--------------------- examples/debug.rs | 5 - 2 files changed, 323 insertions(+), 285 deletions(-) diff --git a/README.md b/README.md index 97d2ed9e2..ee8b29825 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # azul -# WARNING: This README has been written for the future (so I have a "spec" and don't need to update it for the 0.1 release). -# The features advertised won't work yet, they will work when the 0.1 version releases on crates.io. +# WARNING: The features advertised don't work yet. # See the /examples folder for an example of what's currently possible. [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -11,10 +10,12 @@ [![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) [![Rust Compiler Version](https://img.shields.io/badge/rustc-1.26%20stable-blue.svg)]() -azul is a cross-platform, stylable GUI framework using Mozillas `webrender` engine for rendering -and a CSS / DOM model for layout and rendering +azul is a cross-platform, stylable GUI framework using Mozillas `webrender` +engine for rendering and a CSS / DOM model for layout and rendering -[Crates.io](https://crates.io/crates/azul) | [Library documentation](https://docs.rs/azul) | [User guide](http://azul.rs/) +[Crates.io](https://crates.io/crates/azul) | +[Library documentation](https://docs.rs/azul) | +[User guide](http://azul.rs/) ## Installation notes @@ -23,29 +24,30 @@ during the build process to compile freetype. ## Design -azul is a library designed from the experience gathered during working with other -GUI toolkits. azul is very different from (QT / GTK / FLTK / etc.) in the following regards: +azul is a library designed from the experience gathered during working with +other GUI toolkits. azul is very different from (QT / GTK / FLTK / etc.) in the +following regards: -- GUIs are seen as a "view" into your applications data, they are not "objects" like - in any other toolkit. There is no `button.setActive(true)` for example, as that would - introduce stateful design. +- GUIs are seen as a "view" into your applications data, they are not "objects" + like in any other toolkit. There is no `button.setActive(true)` for example, + as that would introduce stateful design. - Widgets types are simply enums that "serialize" themselves into a DOM tree. - The DOM is immutable and gets re-generated every frame. This makes testing and debugging very easy, since if you give the `get_dom()` function a - specific data model, you always get the same DOM back (`get_dom()` is a pure function). - This comes at a slight performance cost, however in practice the cost isn't too - high and it makes the seperation of application data and GUI data very clean. + specific data model, you always get the same DOM back (`get_dom()` is a pure + function). This comes at a slight performance cost, however in practice the + cost isn't too high and it makes the seperation of application data and GUI + data very clean. - The layout model closely follows the CSS flexbox model. The default for CSS is `display:flex` instead of `display:static` (keep that in mind). Some semantics of CSS are not the same, especially the `image` and `vertical-align` properties. However, most attributes work in azul, same as they do in CSS, i.e. `color`, `linear-gradient`, etc. - azul trades a slight bit of performance for better usability. azul is not meant - for game UIs, it is currently too slow for that (currently using 2 - 4 ms per frame) -- azul does not have any asyncronous callbacks - you can implement them manually by - using threads, but we will wait until the Rust compiler stabilizes async / await - later this year (should be stabilized until Dec 2018). -- azul links everything statically, including freetype and + for game UIs, it is currently too slow for that (currently using 2 - 4 ms per + frame) +- azul links everything statically, including freetype and OSMesa (in case the target + system has no hardware accelerated drawing available) ## Data model / Reactive programming @@ -57,86 +59,109 @@ programming / memory model. One image says more than 1000 words: This creates a very simple programming flow: ```rust -// Your data model +use azul::prelude::*; +use azul::widgets::*; + +// Your data model that stores everything except the visual +// representation of the app +#[derive(Default)] struct DataModel { - /* store anything you want here that is relevant to the application */ + // Store anything relevant to the application here + // i.e. settings, a counter, user login data, application data + // the current application zoom, whatever. + // + // This decouples visual components from another - instead of + // updating the visual representation of another component + // on an event, they only update the data they operate on. + // + // For example: + user_name: Option<(Username, Password)> } // Data model -> DOM -impl LayoutScreen for DataModel { - fn get_dom(&self, _window_id: WindowId) -> Dom { - /* DataModel is read-only here, "serialize" from the data model into a UI */ - Dom::new(NodeType::Button { text: hello, .. }) - .with_event(On::MouseDown, Callback::Sync(my_button_was_clicked)) +impl Layout for DataModel { + fn layout(&self, _info: WindowInfo) -> Dom { + // DataModel is read-only here, "serialize" from the data model into a UI + // + // Conditional logic / updating the contents of an existing window + // is very easy - instead of something like `screen.remove(LoginButton);` + // `screen.add(HelloLabel)`, we can simply write: + match self.user_name { + None => Button::with_text("Please log in").dom() + .with_event(On::MouseUp, Callback(login_callback)), + Some((user, _)) => Label::with_text(format!("Hello {}", user)).dom() + } } } // Callback updates data model, when the button is clicked -fn my_button_was_clicked(_app_state: &mut AppState) -> UpdateScreen { - println!("Button clicked!"); - // performance optimization, tell azul that this function doesn't change the UI - // azul will still redraw when the window is resized / CSS events changed - // but by default, azul only redraws when it's absolutely necessary. - UpdateScreen::DontRedraw +fn login_callback(app_state: &mut AppState) -> UpdateScreen { + // Let's just log the user in once he clicks the button + app_state.data.user_name = Some(("Jon Doe", "12345")); + UpdateScreen::Redraw } fn main() { - let mut app = App::new(DataModel { }); + // Initialize the initial state of the app + let mut app = App::new(DataModel::default()); + // Create as many initial windows as you want app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); - app.run(); + // Run it! + app.run().unwrap(); } ``` -This makes it easy to compose the UI from a set of functions, where each function -creates a sub-DOM that can be composed into a larger UI: +This makes it easy to compose the UI from a set of functions, where each +function creates a sub-DOM that can be composed into a larger UI: ```rust -impl LayoutScreen for DataModel { - fn get_dom(&self, _window_id: WindowId) -> Dom { - let mut dom = Dom::new(); +impl Layout for DataModel { + fn layout(&self, _window_id: WindowId) -> Dom { if !self.is_email_sent { - dom.add_child(email_recipients_list(&self.names)); - dom.add_child(email_send_button()); + Dom::new(NodeType::Div) + .with_child(email_recipients_list(&self.names)); + .with_child(email_send_button()); } else { - dom.add_child(no_email_label()); + Dom::new(NodeType::Div) + .with_child(no_email_label()); } - dom } } -fn email_recipients_list(names: &[String]) -> Dom { +fn email_recipients_list(names: &[String]) -> Dom { let mut names_list = Dom::new(NodeType::Div); for name in names { - names_list.add_child(Dom::new(NodeType::Label { - text: name, - })); + names_list.add_child(Label::new(name).dom()); } names_list } fn email_send_button() -> Dom { - Dom::new(NodeType::Button { text: hello, .. }) + Button::labeled("Send email").dom() .with_id("email-send-button") - .with_event(On::MouseDown, Callback::Sync(my_button_was_clicked)) + .with_event(On::MouseUp, Callback(send_email)) } fn no_email_label() -> Dom { - Dom::new(NodeType::Button { text: "No email to send!", .. }) - .with_id("email-done-label") + Button::labeled("No email to send!").dom() + .with_id("no-email-label") } -fn send_email(app_state: &mut AppState, _window_id: WindowId) -> UpdateScreen { +fn send_email(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen { app_state.data.is_email_sent = false; - // trigger a redraw, so the list gets removed from the screen - // and the "No email to send!" message is displayed instead UpdateScreen::Redraw } ``` And this is why azul doesn't really have a large API like other frameworks - -that's really all there is to it! Didn't I say it was simple to learn? +that's really all there is to it! The widgets themselves might require you to +pass a cache or some state across into the `.dom()` function, but the core +model doesn't change. Meaning, if you remove / add visual components, it +doesn't break the whole application as long as the data model stays the same. +A visual component has no knowledge of any other components, it only acts on +the data model. -The benefit of this is that it's very simple to test: +The benefit of this is that it's very simple to refactor and to test: ```rust #[test] @@ -147,155 +172,113 @@ fn test_it_should_send_the_email() { } ``` -However, this model gets a bit tricky when you want to know about additional -information in the callback (such as determining which email recipient of the -list was clicked on). +As well as to write DOM / visual regression tests: + +```rust +#[test] +fn test_layout_email_dom() { + let dom = DataModel { is_email_sent: false }.layout(); + let arena = dom.arena.borrow(); + + let expected = NodeType::Label(String::from("Send email")); + let got = arena[arena[dom.root].first_child().unwrap()].data.data; + + assert_eq!(expected, got); +} +``` + +The inner workings of the DOM are only available in testing functions, not +regular code. -// TODO: explain how to send hit test IDs in the callback +You might have noticed that a `WindowEvent` gets passed to the callback. +This struct contains callback information that is necessary to determine what +item (of a bigger list, for example) was interacted with. Ex. if you have +100 list items, you don't want to write 100 callbacks for each one, but rather +have one callback that acts on which ID was selected. The `WindowEvent` +gives you the necessary information to react to these events. ## Updating window properties -You may have noticed that the callback takes in a `AppState`, not -the `DataModel` directly. This is because you can change the window settings, for -example the title of the window: +You may also have noticed that the callback takes in a `AppState`, +not the `DataModel` directly. This is because you can change the window +settings, for example the title of the window: ```rust -fn callback(app_state: &mut AppState, window_id: WindowId) -> UpdateScreen { +fn callback(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen { app_state.windows[window_id].window.title = "Hello"; app_state.windows[window_id].window.menu += "&Application > &Quit\tAlt+F4"; } ``` -Note how there isn't any `.get_title()` or `.set_title()`. Simply setting the title -is enough to invoke the (stateful) Win32 / X11 / Wayland / Cocoa functions for setting -the window title. +Note how there isn't any `.get_title()` or `.set_title()`. Simply setting the +title is enough to invoke the (stateful) Win32 / X11 / Wayland / Cocoa functions +for setting the window title. You can query the active title / mouse or keyboard +state in the same way. -## Working with blocking IO +## Async I/O -Bloking IO is when you have to wait for something to complete, a website returning -HTMl / JSON. Azul has to continouusly poll if the execution has finished. +When you have to perform a larger task, such as waiting for network content or +waiting for a large file to be loaded, you don't want to block the user +interface, which would give a bad experience. -For this, azul has a mechanism called a "Task" (similar to C#). A Task starts a -background thread and azul registers it and looks if the thread has finished yet. -Usually you lock the data model when the task is done, i.e. when you've finished loading -the file / website, etc. +Instead, azul provides two mechanisms: a `Task` and a `Deamon`. Both are +essentially function callbacks, but the `Task` gets run on a seperate thread +(one thread per task) while a `Deamon` gets run on the same thread as the main +UI. + +azul takes care of querying if the `Task` or `Deamon` has finished. Both have +access to the applications data model and can modify it (without race conditions): ```rust +use std::{thread, time::Duration}; struct DataModel { - website_data: Option, + website: Option, } -impl LayoutScreen { - /// Note: `get_dom` is called in a thread-safe way by azul. - fn get_dom(&self, _window_id: WindowId) -> Dom { - let mut dom = Dom::new(); - match self.website_data { - Some(data) => dom.append_child(Dom::new(NodeType::Label { text: data.clone() })), - None => dom.append_child( - Dom::new(NodeType::Button { text: "Download the website", .. }) - .with_event(On::Click, Callback::Async(start_download))), +impl Layout for DataModel { + fn layout(&self, _: WindowInfo) -> Dom { + match self.website { + None => + Dom::new() + .with_child(Button::labeled("Click to download").dom()) + .with_event(On::MouseUp, Callback(start_download)) + Some(data) => + Dom::new() + .with_child(Label::new(data.clone()).dom()), } - dom } } -// Note: push_background_task are only implemented on Arc>, not T itself. -fn start_download(app_state: &mut Arc>>, _window_id: WindowId) -> UpdateScreen { - // background_fns creates a background thread that clones the app_state Arc, - // waits until the thread has completed and then calls `get_dom()` on completion, to update the UI - app_state.push_background_task(Task(download_website)); +fn start_download(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen { + app_state.add_task(download_website); UpdateScreen::DontRedraw } -// Note: The `_drop` is necessary so that azul can tell that the thread has finished executing. fn download_website(app_state: Arc>>, _drop: Arc<()>) { // simulate slow, blocking IO - ::std::thread::sleep(::std::time::Duration::from_secs(5)); - // only lock the Arc when done with the work - let app_state = app_state.lock().unwrap(); - app_state.data.website_data = Some("

Hello

".into()); + thread::sleep(Duration::from_secs(5)); + app_state.modify(|data| data.website = Some("

Hello

".into())); } ``` -Note that there is no "wait". If you call `app_state.lock()` in the `download_website` function, -it will block the main thread, so only call it once you are done with the blocking IO. - -These concepts currently start full, OS-level threads. However, generally in -desktop applications, you don't start 10k tasks at once, maybe 4 - 5 max. This concept -will be replaced by async / await syntax, until then it uses the OS threads. - -## Deamons - -Sometimes you want to run functions independent of the user interacting with the application. -Example: you want to update a progress bar to how what percentage of a file has loaded. Or -you want to start a timer. For this, azul has "deamons" or "polling functions", that run -continouusly in the background, until they stop. - -```rust -use std::time::Duration; - -struct DataModel { - // technically you'd only need to store the Instant of the start, - // but this is just to demonstrate how deamons work - stopwatch: Option<(Instant, Duration)>, -} - -impl LayoutScreen { - // pseudocode, you can imagine what display_stop_watch, create_stop_timer_btn - // and create_start_timer_button do - fn get_dom(&self, _window_id: WindowId) -> Dom { - let mut dom = Dom::new(); - match stopwatch { - Some(_, current_duration) => { - dom.append_child(display_stop_watch(current_duration)); - dom.append_child( - create_stop_timer_btn() - .with_callback(On::MouseDown, Callback::Sync(stop_timer)) - ); - }, - None => { - dom.append_child( - create_start_timer_button() - .with_callback(On::MouseDown, Callback::Sync(start_timer)) - ) - } - } - dom - } - - fn start_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { - app_state.stopwatch = Some(Instant::now(), Duration::from_secs(0)); - // Deamons are identified by ID, to allow to run ex. multiple timers at once - app_state.push_deamon("timer_1", Callback::Sync(update_timer)); - UpdateScreen::Redraw - } - - fn update_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { - app_state.data.last_time.1 = Instant::now() - app_state.data.last_time.0; - // Trigger a redraw on every frame - UpdateScreen::Redraw - } - - fn stop_timer(app_state: &mut AppState>>, _window_id: WindowId) -> UpdateScreen { - app_state.pop_deamon("timer_1"); - UpdateScreen::Redraw - } -} -``` +The `app_state.modify` is a only conveniece function that locks and unlocks your +data model. The `_drop` variable is necessary so that azul can see when the thread +has finished and join it afterwards. -Polling functions / deamons are useful when implementing actions that should run -independently if the user interacts with the application or not. +A `Task` starts one full, OS-level thread. Usually doing this is a bad idea for +performance, since you may at one point have too many threads running. However, in +desktop applications, you usually don't run 1000 tasks at once, maybe 4 - 5 maximum. +For this, OS-level threads are usually sufficient and performant enough. ## Styling -azul comes with default styles that mimick the operating-system native style. +Azul has default visual styles that mimick the sperating-systems native style. However, you can overwrite parts (or everything) with your custom CSS styles: ```rust let default_css = Css::native(); -let my_css = Css::new_from_string(include_str!("my_custom.css")); -// Use the default CSS as a fallback, but overwrite only the styles in my_custom.css +let my_css = Css::new_from_string(include_str!("my_custom.css")).unwrap(); let custom_css = default_css + my_css; ``` @@ -303,120 +286,167 @@ The default styles are implemented using CSS classes, with the special name `.__azul-native-`, i.e. `__azul-native-button` for styling buttons, `.__azul-native-scrollbar` for styling scrollbars, etc. -You can add and remove CSS styles dynamically using `my_style.push_css_rule(CssRule::new("color", "#fff"));`. -However, this will trigger a re-build of the CSS rules, relayout and re-style, and is -generally not recommended. It is recommended that you don't over-use this feature and rather switch out -CSS blocks in the `get_dom()` method, rather than changing CSS properties: +## Dynamic CSS properties + +You can override CSS properties from Rust during runtime, but after every frame +the modifications are cleared again. You do not have to "unset" a CSS style once the +state of your application changes. Example: -```css -.btn-active { color: blue; } -.btn-danger { color: red; } -``` ```rust -if self.button[i].is_danger { - dom.class("btn-danger"); -} else { - dom.class("btn-active"); +struct Discord { light_theme: bool } + +impl Layout for Discord { + fn layout(&self, _: WindowInfo) -> Dom { + Dom::new(NodeType::Div) + .with_class("background") + .with_event(On::MouseOver, Callback(mouse_over_window)) + } +} + +fn mouse_over_window(_: &mut AppState, window: WindowEvent) -> UpdateScreen { + window.css.set("theme_background_color", ("color", "#fff")).unwrap(); + UpdateScreen::Redraw +} + +fn main() { + let css = Css::new_from_string(" + .hovered { + color: [[ theme_background_color | #333 ]]; + } + ").unwrap(); + let mut app = App::new(Discord { light_theme: false }) + app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run().unwrap(); } ``` -instead of: +The `[[ variable | default ]]` denotes a "dynamic CSS property". If the +`theme_background_color` variable isn't set for this frame, it will use the +default. The reason why the CSS state is unset on every frame is to prevent +forgetting to unset it. Especially with highly conditional styling, this +can easily lead to bugs (i.e. only set this button to green if a certain setting +is set to true, but not if today is Wednesday). + +The second reason is to make CSS properties testable, i.e. in the same way that +you can test state properties, you can test CSS properties. Azuls philosophy is +that the state of the previous frame never affects the current frame. Only the +data model can affect the visual content, there is no `.setBackground("blue")` +because at some point, you'd have to write a function to un-set the background +again - this stateful design can quickly lead to visual bugs. + +## SVG / OpenGL API + +For drawing custom graphics, azul has a high-performance 2D vector API. It also +allows you to load and draw SVG files (with the exceptions of gradients: gradients +in SVG files are not yet supported). But azul itself does not know about SVG shapes +at all - so how the SVG widget implemented? + +The solution is to draw the SVG to an OpenGL texture and hand that to azul. This +way, the SVG drawing component could even be implemented in an external crate, if +you really wanted to. This mechanism also allows for completely custom drawing +(let's say: a game, a 3D viewer, etc.) to be drawn. + +The SVG component currently uses the `resvg` parser, `usvg` minifaction and the +`lyon` triangulation libraries). Of course you can also add custom shapes +(bezier curves, circles, lines, whatever) programatically, without going through +the SVG parser: ```rust -if self.button[i].is_danger { - app_data.windows[window_id].css.push_rule(".btn-active { color: red; }"); -} else { - // warning: pushing CSS is stateful and won't be re-generated every frame - // DONT DO THIS, VERY BAD PERFORMANCE - app_data.windows[window_id].css.push_rule(".btn-active { color: blue; }"); +const TEST_SVG: &str = include_str!("tiger.svg"); + +impl Layout for Model { + fn layout() { + if let Some((svg_cache, svg_layers)) = self.svg { + Svg::with_layers(svg_layers).dom(&info.window, &svg_cache) + } else { + Button::labeled("Load SVG file").dom() + .with_callback(load_svg) + } + } +} + +fn load_svg(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen { + let mut svg_cache = SvgCache::empty(); + let svg_layers = svg_cache.add_svg(TEST_SVG).unwrap(); + app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); + UpdateScreen::Redraw } ``` -## SVG / Canvas API +This is one of the few exceptions where azul allows persistent data across frames +since it wouldn't be performant enough otherwise. Ideally you'd have to load, triangulate +and draw the SVG file on every frame, but this isn't performant. You might have +noticed that the `.dom()` function takes in an extra parameter: The `svg_cache` +and the `info.window`. This way, the `svg_cache` handles everything necessary to +cache vertex buffers / the triangulated layers and shapes, only the drawing itself +is done on every frame. + +Additionally, you can also register callbacks on any item **inside** the SVG using the +`SvgCallbacks`, i.e. when someone clicks on or hovers over a certain shape. In order +to draw your own vector data (for example in order to make a vector graphics editor), +you can build the "SVG layers" yourself (ex. from the SVG data). Each layer is +batch-rendered, so you can draw many lines or polygons in one draw call, as long as +they share the same `SvgStyle`. + +## Supported CSS attributes + +### Implemented + +This is a list of CSS attributes that are currently implemented. They work in +the same way as on a regular web page, except if noted otherwise: + +- `border-radius` +- `background-color` +- `color` +- `border` +- `background` [see #1] +- `font-size` +- `font-family` [see #1] +- `box-shadow` +- `line-height` +- `width`, `min-width`, `max-width` +- `height`, `min-height`, `max-height` +- `align-items` [see #2] +- `overflow`, `overflow-x`, `overflow-y` +- `text-align` [see #3] + +Notes: + +1. `image()` takes an ID instead of a URL. Images and fonts are external resources + that have to be cached. Use `app.add_image("id", my_image_data)` or + `app_state.add_image()`, then you can use the `"id"` in the CSS to select + your image. + If an image is not present on a displayed div (i.e. you added it to the CSS, + but forgot to add the image), the following happens: + - In debug mode, the app crashes with a descriptive error message + - In release mode, the app doesn't display the image and logs the error +2. Currently `align-items` is only implemented to center text vertically +3. Justified text is not (yet) supported -**NOTE: This README was written for the future, not implemented yet** +### Planned -For drawing custom graphics, azul has a high-performance 2D vector & raster API. -The core of the custom-drawing API is based on an OpenGL texture. A `NodeType::SvgComponent` -consists of "layers", like in Photoshop, which are OpenGL textures composited on top -of each other. To make it easier to display vector graphics, you can directly initialize -a component from a SVG file (uses the `resvg` parser, `usvg` minifaction, `lyon` triangulation -and `glium` drawing libraries): +These properties are planned for the next (currently 0.1) release: -```rust -// Don't parse and SVG in the `get_dom()` function, store the parsed SVG in -// the data model, to cache it -let svg_parsed = Svg::new_from_string(include_str!("hello.svg")); - -dom.add_child - Dom::new(NodeType::DrawComponent( - DrawComponent::Svg { - layers: vec![ - ("layer-01", svg_parsed.into(), SvgCallbacks::None) - ], - } - )) - .with_id("my-svg") -); -``` +- `flex-wrap` +- `flex-direction` +- `justify-content` +- `align-content` -If you want callbacks on any item **inside** the SVG, i.e. when someone clicks or hovers on / over a shape, -you can register a callback for that, using the `SvgCallbacks`. - -// TODO: explain how to register custom events - -In order to draw your own vector graphics, without putting the data through an SVG parser -first, you can build the layers yourself (ex. from the SVG data). - -Since azul needs the image library anyway (for decoding), it is re-exported, to improve build times -and reduce duplication (so you don't have to do `extern crate image`, just do `use azul::image;`) - -## Other features - -### Current - -- Supported CSS attributes (syntax is the same as CSS, expect when marked otherwise): - - `border-radius` - - `background-color` - - `color` - - `border` - - `background`: **Note**: `image()` takes an ID instead of a URL, see below. - - `font-size` - - `font-family`: **Note**: Like images, you need to register font IDs first (from Rust) - - `box-shadow` - - `line-height` - - `width`, `min-width`, `max-width` - - `height`, `min-height`, `max-height` - - `align-items`: **Note**: Currently only implemented for centering text vertically - - `overflow`, `overflow-x`, `overflow-y` - - `text-align`: **Note**: Justified text is not supported. - -- Not implemented yet, but planned: - - `flex-wrap` - - `flex-direction` - - `justify-content` - - `align-content` - -Remarks: - -1. Any measurements can be given in `px` or `em`. `px` does not respect high-DPI scaling, while `em` does. - The default is `1em` = `16px * high_dpi_scale` -2. Images and fonts are external resources that have to be cached. Use `app.add_image("id", my_image_data)` - or `app_state.add_image()`, then you can use the `"id"` that you gave the image in the CSS. - If an image is not present on a displayed div (i.e. you added it to the CSS, but forgot to add the image), - the following happens: - - In debug mode, the app crashes with a message (to notify you of the failure) - - In release mode, the app doesn't display the image (how could it?) and silently fails - The same goes for fonts (azul does currently not load any default font, but that is subject to change) -3. CSS rules are (within a block), parsed from top to bottom, so: +### Remarks + +1. Any measurements can be given in `px`, `pt` or `em`. `pt` does not + respect high-DPI scaling, while `em` and `px` do. The default is `1em` = + `16px * high_dpi_scale` +2. CSS rules are (within a block), parsed from top to bottom, ex: ```css #my_div { background: image("Cat01"); background: linear-gradient(""); } ``` - ... will give you the linear gradient, not display the image. -4. Maybe the most important thing, cascading is currently extremely buggy, + This will draw a linear gradient, not the image, since the `linear-gradient` rule + overwrote the `image` rule. +3. Maybe the most important thing, cascading is currently extremely buggy, the best result is done if you do everything via classes / ids and not mix them: ```css /* Will work */ @@ -424,31 +454,44 @@ Remarks: /* Won't work */ .general #specific .very-specific { color: black; } ``` - The CSS parser currently only supports CSS 2.1, not CSS 3 attributes (esp. animations) - Animations are a feature to be implemented. + +The CSS parser currently only supports CSS 2.1, not CSS 3 attributes. Animations +are not done in CSS, but rather by using dynamic CSS properties (see above) ### Planned -- Animations (should be implemented in CSS, not in Rust, no breaking change necessary) -- WEBM Video playback (using libvp9 / rust-media, will be exposed the same way as images, using IDs, no breaking change) -- Asynchronous callbacks (waiting on rustc to stabilize async / await) -- Looping / polling functions (important to drive futures Executors / update the app state continouusly) - -## CPU & Memory usage - -azul checks for all the displays in an infinite loop. Windows run by default at -60 FPS, but you can limit / unlimit this in the `WindowCreateOptions`. With these -default settings, azul uses ~ 0 - 0.5% CPU and ~ 39MB RAM. However, if you add images -and fonts, the data for these has to be kept in memory (with the uncompressed RGBA -values), so the memory usage can spike to 60MB or more once images are involved. -The redraw time (when using hardware acceleration) lies between 2 and 4 milliseconds, -i.e. 400 - 200 FPS. However, azul will only redraw the screen when absolutely necessary, -so the real FPS is usually much lower. This is usually fast enough for most desktop -applications, but not for games. However, if you use the SVG API (which skips the layout step and -uses absolute positioning), this library may be fast enough for drawing the UI in your game (~ 1 ms). - -The startup time depends on how many fonts / images you add on startup, the default -time is between 100 and 200 ms for an app with no images and a single font. +- WEBM Video playback (using libvp9 / rust-media, will be exposed the same way as + images, using IDs, no breaking change) + +## CPU / memory usage / startup time + +While efficiency is definitely one goal, ease of use is the first and foremost +goal. In order to respond to events, azul runs in an infinite loop and checks all +windows for incoming events, which consumes around 0-0.5% CPU when idling. + +A default window, with no fonts or images added takes up roughly 39MB of RAM and +5MB in binary size. This usage can go up once you load more images and fonts, since +azul has to load and keep the images in RAM. + +The frame time (i.e. the time necessary to draw a single frame, including layout) +lies between 2 - 5 milliseconds, which equals roughly 200 - 500 frames per second. +However, azul limits this frame time and only redraws the window when absolutely +necessary, in order to not waste the users battery life. + +The startup time depends on how many fonts / images you add on startup, the +default time is between 100 and 200 ms for an app with no images and a single font. + +## Thanks + +Several projects have helped azul severely and should be noted here: + +- Chris Tollidays [limn](https://github.com/christolliday/limn) framework has helped + severely with discovering undocumented parts of webrender as well as working with + constraints (the `constraints.rs` file was copied from limn with the [permission of + the author](https://github.com/christolliday/limn/issues/22#issuecomment-362545167)) +- Nicolas Silva for his work on [lyon](https://github.com/nical/lyon) - without this, + the SVG renderer wouldn't have been possible +- All webrender contributors who have been patient enough to answer my questions on IRC ## License diff --git a/examples/debug.rs b/examples/debug.rs index e4ff4bbd6..f5a942c33 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -29,14 +29,9 @@ impl Layout for MyAppData { } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { - // Load and parse the SVG file, register polygon data as IDs - use std::time::{Instant}; - let start_time_loading_svg = Instant::now(); let mut svg_cache = SvgCache::empty(); let svg_layers = svg_cache.add_svg(TEST_SVG).unwrap(); app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); - let time = (Instant::now() - start_time_loading_svg).subsec_nanos() as f32 / 1_000_000.0; - println!("time loading svg: {} milliseconds", time); UpdateScreen::Redraw } From a03671240f2e08f830518bc59b2e97629a4852b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 09:38:31 +0200 Subject: [PATCH 113/868] Make appveyor only test the master branch --- appveyor.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0aeb05802..32b7bcfc2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,6 @@ environment: global: - # This will be used as part of the zipfile name - # TODO change the project name PROJECT_NAME: azul - # TODO feel free to delete targets/channels you don't need matrix: # Disable windows-gnu because appveyor has problems installing CMAKE correctly # (CMAKE_C_COMPILER not set) @@ -22,8 +19,12 @@ environment: - TARGET: x86_64-pc-windows-msvc CHANNEL: stable +branches: + only: + - master + # Install Rust and Cargo -# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml) +# Based on https://github.com/rust-lang/libc/blob/master/appveyor.yml install: - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - rustup-init -yv --default-toolchain %channel% --default-host %target% @@ -33,13 +34,12 @@ install: - rustc -vV - cargo -vV -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents -# the "directory does not contain a project or solution file" error. +# 'cargo test' takes care of building for us, so disable Appveyor's build stage. +# This prevents the "directory does not contain a project or solution file" error. # source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113 build: false # Equivalent to Travis' `script` phase -# TODO modify this phase as you see fit test_script: - cargo build --verbose --examples - cargo test --verbose --features "doc-test no-opengl-tests" \ No newline at end of file From 37eeac6637f1d15437d9cff50d2d5539d34fc090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 10:38:10 +0200 Subject: [PATCH 114/868] Fill out optional fields in Cargo.toml --- Cargo.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2a7405e6e..78b1e21f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,18 @@ name = "azul" version = "0.1.0" authors = ["Felix Schütt "] +description = ''' + Azul GUI is a free, functional, MVVM-oriented GUI framework + for rapid development of desktop applications written in Rust, + supported by the Mozilla WebRender rendering engine +''' +documentation = "https://docs.rs/azul" +homepage = "https://fschutt.github.io/azul.rs/" +keywords = ["gui", "GUI", "user interface", "svg", "graphics" ] +categories = ["gui"] +license = "MIT" +repository = "https://github.com/maps4print/azul" +readme = "README.md" [dependencies] cassowary = "0.3.0" From 20dd3b2a8e427ab30071870a657dc58dfa7aa172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 14:23:08 +0200 Subject: [PATCH 115/868] Added open / save dialogs, message boxes, etc. --- Cargo.toml | 2 ++ examples/debug.rs | 20 +++++++++--- src/dialogs.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 6 +++- src/svg.rs | 18 ++++------- 5 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 src/dialogs.rs diff --git a/Cargo.toml b/Cargo.toml index 78b1e21f3..d4e8aa26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ harfbuzz_rs = "0.1.0" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" palette = "0.4.0" +tinyfiledialogs = "3.3.5" +nfd = "0.0.4" [dependencies.resvg] git = "https://github.com/RazrFalcon/resvg.git" diff --git a/examples/debug.rs b/examples/debug.rs index f5a942c33..1d2ac772f 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -4,10 +4,11 @@ extern crate azul; use azul::prelude::*; use azul::widgets::*; +use azul::dialogs::*; +use std::fs; const TEST_CSS: &str = include_str!("test_content.css"); const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); -const TEST_SVG: &[u8] = include_bytes!("../assets/svg/tiger.svg"); #[derive(Debug)] pub struct MyAppData { @@ -29,10 +30,19 @@ impl Layout for MyAppData { } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { - let mut svg_cache = SvgCache::empty(); - let svg_layers = svg_cache.add_svg(TEST_SVG).unwrap(); - app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); - UpdateScreen::Redraw + open_file_dialog(None, None) + .and_then(|path| fs::read_to_string(path.clone()).ok()) + .and_then(|contents| { + let mut svg_cache = SvgCache::empty(); + let svg_layers = svg_cache.add_svg(&contents).ok()?; + app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); + message_box("File loaded successfully"); + Some(UpdateScreen::Redraw) + }) + .unwrap_or_else(|| { + + UpdateScreen::DontRedraw + }) } fn main() { diff --git a/src/dialogs.rs b/src/dialogs.rs new file mode 100644 index 000000000..90eb87714 --- /dev/null +++ b/src/dialogs.rs @@ -0,0 +1,81 @@ +//! Bindings to platform-specific file dialogs using the `tinyfiledialogs` library + +#![doc(inline)] + +pub use tinyfiledialogs::DefaultColorValue; +pub use tinyfiledialogs::MessageBoxIcon; +pub use tinyfiledialogs::OkCancel; +pub use tinyfiledialogs::YesNo; + +pub use tinyfiledialogs::color_chooser_dialog; +pub use tinyfiledialogs::input_box; +pub use tinyfiledialogs::list_dialog; +pub use tinyfiledialogs::message_box_ok; +pub use tinyfiledialogs::message_box_ok_cancel; +pub use tinyfiledialogs::message_box_yes_no; +pub use tinyfiledialogs::password_box; + +// We don't use tinyfiledialogs for file dialogs +// because it doesn't handle Unicode correctly + +/// Open a single file, returns `None` if the user canceled the dialog. +/// +/// - `filter_list` may be `["doc", "docx", "jpg"]` +pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option +{ + use nfd::{open_dialog, DialogType, Response}; + + let filter_list = filter_list.map(|list| list.join(";")); + let filter_list_2 = filter_list.as_ref().map(|x| &**x); + + match open_dialog(filter_list_2, default_path, DialogType::SingleFile).unwrap() { + Response::Okay(file_path) => Some(file_path), + _ => None, + } +} + +/// Open a directory, returns `None` if the user canceled the dialog +pub fn open_directory_dialog(default_path: Option<&str>) +-> Option +{ + use nfd::{open_dialog, DialogType, Response}; + + match open_dialog(None, default_path, DialogType::PickFolder).unwrap() { + Response::Okay(file_path) => Some(file_path), + _ => None, + } +} + +// Open multiple files at once, returns `None` if the user canceled the dialog, +// otherwise returns the `Vec` with the given file paths +pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option> +{ + use nfd::{open_dialog, DialogType, Response}; + + let filter_list = filter_list.map(|list| list.join(";")); + let filter_list_2 = filter_list.as_ref().map(|x| &**x); + + match open_dialog(filter_list_2, default_path, DialogType::MultipleFiles).unwrap() { + Response::Okay(file_path) => Some(vec![file_path]), + Response::OkayMultiple(paths) => Some(paths), + _ => None, + } +} + +/// Opens a save file dialog, returns `None` if the user canceled the dialog +pub fn save_file_dialog(default_path: Option<&str>) +-> Option +{ + use nfd::{open_dialog, DialogType, Response}; + + match open_dialog(None, default_path, DialogType::SaveFile).unwrap() { + Response::Okay(file_path) => Some(file_path), + _ => None, + } +} + +pub fn message_box(content: &str) { + message_box_ok("Info", content, MessageBoxIcon::Info); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 425c92699..4c033c570 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,8 @@ extern crate rusttype; extern crate app_units; extern crate unicode_normalization; extern crate harfbuzz_rs; +extern crate tinyfiledialogs; +extern crate nfd; /// DOM / HTML node handling pub mod dom; @@ -63,6 +65,8 @@ pub mod task; pub mod svg; /// Built-in widgets pub mod widgets; +/// Bindings to the native file-chooser, color picker, etc. dialogs +pub mod dialogs; /// Global application (Initialization starts here) mod app; /// Wrapper for the application data & application state @@ -116,7 +120,7 @@ pub mod prelude { pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; pub use traits::{Layout, ModifyAppState, GetDom}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, - MouseMode, UpdateBehaviour, UpdateMode, + MouseMode, UpdateBehaviour, UpdateMode, WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; pub use window_state::WindowState; pub use images::ImageType; diff --git a/src/svg.rs b/src/svg.rs index e2ee03090..231a3e7cc 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,5 +1,5 @@ use std::{ - fmt, + fmt, rc::Rc, io::{Error as IoError, Read}, sync::{Mutex, atomic::{Ordering, AtomicUsize}}, @@ -16,12 +16,12 @@ use lyon::{ VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, basic_shapes::{ - fill_circle, stroke_circle, fill_rounded_rectangle, + fill_circle, stroke_circle, fill_rounded_rectangle, stroke_rounded_rectangle, BorderRadii }, }, path::{ - default::{Builder, Path}, + default::{Builder, Path}, builder::{PathBuilder, FlatPathBuilder}, PathEvent, }, geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, @@ -260,7 +260,7 @@ impl SvgCache { /// Parses an input source, parses the SVG, adds the shapes as layers into /// the registry, returns the IDs of the added shapes, in the order that they appeared in the Svg - pub fn add_svg(&mut self, input: R) -> Result, SvgParseError> { + pub fn add_svg>(&mut self, input: S) -> Result, SvgParseError> { let (layers, transforms) = self::svg_to_lyon::parse_from(input, &mut self.view_boxes)?; self.add_transforms(transforms); Ok(layers @@ -720,16 +720,10 @@ mod svg_to_lyon { use webrender::api::ColorU; use FastHashMap; - pub fn parse_from(mut svg_source: R, view_boxes: &mut FastHashMap) + pub fn parse_from, T: Layout>(mut svg_source: S, view_boxes: &mut FastHashMap) -> Result<(Vec>, FastHashMap), SvgParseError> { - let svg_source = { - let mut source_str = String::new(); - svg_source.read_to_string(&mut source_str)?; - source_str - }; - let opt = Options::default(); - let rtree = Tree::from_str(&svg_source, &opt).unwrap(); + let rtree = Tree::from_str(svg_source.as_ref(), &opt).unwrap(); let mut layer_data = Vec::new(); let mut transform = None; From 1f1d223cdc17eb986c57f748e970b3116e734cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 15:20:23 +0200 Subject: [PATCH 116/868] Documented the dialogs module --- examples/debug.rs | 1 - src/dialogs.rs | 164 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 20 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 1d2ac772f..cf92eefa4 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -36,7 +36,6 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let mut svg_cache = SvgCache::empty(); let svg_layers = svg_cache.add_svg(&contents).ok()?; app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); - message_box("File loaded successfully"); Some(UpdateScreen::Redraw) }) .unwrap_or_else(|| { diff --git a/src/dialogs.rs b/src/dialogs.rs index 90eb87714..b70fd2edf 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,26 +1,138 @@ -//! Bindings to platform-specific file dialogs using the `tinyfiledialogs` library +use tinyfiledialogs::MessageBoxIcon; +use tinyfiledialogs::DefaultColorValue; -#![doc(inline)] +/// Default color in the color picker +const DEFAULT_COLOR: [u8; 3] = [0, 0, 0]; -pub use tinyfiledialogs::DefaultColorValue; -pub use tinyfiledialogs::MessageBoxIcon; -pub use tinyfiledialogs::OkCancel; -pub use tinyfiledialogs::YesNo; +/// Ok or cancel result, returned from the `msg_box_ok_cancel` function +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OkCancel { + Ok, + Cancel, +} + +impl From<::tinyfiledialogs::OkCancel> for OkCancel { + #[inline] + fn from(e: ::tinyfiledialogs::OkCancel) -> OkCancel { + match e { + ::tinyfiledialogs::OkCancel::Ok => OkCancel::Ok, + ::tinyfiledialogs::OkCancel::Cancel => OkCancel::Cancel, + } + } +} -pub use tinyfiledialogs::color_chooser_dialog; -pub use tinyfiledialogs::input_box; -pub use tinyfiledialogs::list_dialog; -pub use tinyfiledialogs::message_box_ok; -pub use tinyfiledialogs::message_box_ok_cancel; -pub use tinyfiledialogs::message_box_yes_no; -pub use tinyfiledialogs::password_box; +impl From for ::tinyfiledialogs::OkCancel { + #[inline] + fn from(e: OkCancel) -> ::tinyfiledialogs::OkCancel { + match e { + OkCancel::Ok => ::tinyfiledialogs::OkCancel::Ok, + OkCancel::Cancel => ::tinyfiledialogs::OkCancel::Cancel, + } + } +} + +/// Yes or No result, returned from the `msg_box_yes_no` function +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum YesNo { + Yes, + No, +} + +impl From for ::tinyfiledialogs::YesNo { + #[inline] + fn from(e: YesNo) -> ::tinyfiledialogs::YesNo { + match e { + YesNo::Yes => ::tinyfiledialogs::YesNo::Yes, + YesNo::No => ::tinyfiledialogs::YesNo::No, + } + } +} + +impl From<::tinyfiledialogs::YesNo> for YesNo { + #[inline] + fn from(e: ::tinyfiledialogs::YesNo) -> YesNo { + match e { + ::tinyfiledialogs::YesNo::Yes => YesNo::Yes, + ::tinyfiledialogs::YesNo::No => YesNo::No, + } + } +} + +/// MsgBox icon to use in the `msg_box_*` functions +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum MsgBoxIcon { + Info, + Warning, + Error, + Question, +} + +impl From for MessageBoxIcon { + #[inline] + fn from(e: MsgBoxIcon) -> MessageBoxIcon { + match e { + MsgBoxIcon::Info => MessageBoxIcon::Info, + MsgBoxIcon::Warning => MessageBoxIcon::Warning, + MsgBoxIcon::Error => MessageBoxIcon::Error, + MsgBoxIcon::Question => MessageBoxIcon::Question, + } + } +} + +// Note: password_box, input_box and list_dialog do not work, so they're not included here. + +/// "Y/N" MsgBox (title, message, icon, default) +pub fn msg_box_yes_no(title: &str, message: &str, icon: MessageBoxIcon, default: YesNo) -> YesNo { + ::tinyfiledialogs::message_box_yes_no(title, message, icon, default.into()).into() +} + +/// "Ok" MsgBox (title, message, icon) +pub fn msg_box_ok(title: &str, message: &str, icon: MessageBoxIcon) { + ::tinyfiledialogs::message_box_ok(title, message, icon) +} + +/// "Ok / Cancel" MsgBox (title, message, icon, default) +pub fn msg_box_ok_cancel(title: &str, message: &str, icon: MessageBoxIcon, default: OkCancel) -> OkCancel { + ::tinyfiledialogs::message_box_ok_cancel(title, message, icon, default.into()).into() +} + +/// Color value (hex or rgb) to open the `color_chooser_dialog` with +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ColorValue<'a> { + Hex(&'a str), + RGB(&'a [u8; 3]), +} + +impl<'a> Default for ColorValue<'a> { + fn default() -> Self { + ColorValue::RGB(&DEFAULT_COLOR) + } +} + +impl<'a> Into> for ColorValue<'a> { + fn into(self) -> DefaultColorValue<'a> { + match self { + ColorValue::Hex(s) => DefaultColorValue::Hex(s), + ColorValue::RGB(r) => DefaultColorValue::RGB(r), + } + } +} + +/// Opens the default color picker dialog +pub fn color_picker_dialog(title: &str, default_value: Option) +-> Option<(String, [u8; 3])> +{ + let default = default_value.unwrap_or_default().into(); + ::tinyfiledialogs::color_chooser_dialog(title, default) +} // We don't use tinyfiledialogs for file dialogs // because it doesn't handle Unicode correctly /// Open a single file, returns `None` if the user canceled the dialog. /// -/// - `filter_list` may be `["doc", "docx", "jpg"]` +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) -> Option { @@ -47,8 +159,11 @@ pub fn open_directory_dialog(default_path: Option<&str>) } } -// Open multiple files at once, returns `None` if the user canceled the dialog, -// otherwise returns the `Vec` with the given file paths +/// Open multiple files at once, returns `None` if the user canceled the dialog, +/// otherwise returns the `Vec` with the given file paths +/// +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) -> Option> { @@ -76,6 +191,17 @@ pub fn save_file_dialog(default_path: Option<&str>) } } -pub fn message_box(content: &str) { - message_box_ok("Info", content, MessageBoxIcon::Info); -} \ No newline at end of file +/// Wrapper around `message_box_ok` with the default title "Info" + an info icon. +/// +/// Note: If you are too young to remember Visual Basics glorious `MsgBox` +/// then I pity you. Those were the days. +pub fn msg_box(content: &str) { + msg_box_ok("Info", content, MessageBoxIcon::Info); +} + +// TODO (at least on Windows): +// - Find and replace dialog +// - Font picker dialog +// - Page setup dialog +// - Print dialog +// - Print property dialog \ No newline at end of file From 09c74418d529d4512c451bbb04773b1eb4a0bcd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 17:17:10 +0200 Subject: [PATCH 117/868] Added initial FXAA shader, not yet used --- examples/debug.rs | 3 +- src/svg.rs | 154 ++++++++++++++++++++++++++++++++++++++++++++++ src/widgets.rs | 8 +-- 3 files changed, 159 insertions(+), 6 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index cf92eefa4..6b551223b 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -30,7 +30,7 @@ impl Layout for MyAppData { } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { - open_file_dialog(None, None) + open_file_dialog(None, Some(&["svg"])) .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { let mut svg_cache = SvgCache::empty(); @@ -39,7 +39,6 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv Some(UpdateScreen::Redraw) }) .unwrap_or_else(|| { - UpdateScreen::DontRedraw }) } diff --git a/src/svg.rs b/src/svg.rs index 231a3e7cc..365456237 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -81,6 +81,160 @@ const SVG_FRAGMENT_SHADER: &str = " } "; +// inputs: +// +// - `resolution` +// - `position` +// - `uv` +// - `source` +const SVG_FXAA_VERTEX_SHADER: &str = " + #version 130 + precision mediump float; + + out vec2 v_rgbNW; + out vec2 v_rgbNE; + out vec2 v_rgbSW; + out vec2 v_rgbSE; + out vec2 v_rgbM; + + uniform vec2 resolution; + uniform vec2 position; + uniform vec2 uv; + + void texcoords(vec2 fragCoord, vec2 resolution, + out vec2 v_rgbNW, out vec2 v_rgbNE, + out vec2 v_rgbSW, out vec2 v_rgbSE, + out vec2 v_rgbM) { + vec2 inverseVP = 1.0 / resolution.xy; + v_rgbNW = (fragCoord + vec2(-1.0, -1.0)) * inverseVP; + v_rgbNE = (fragCoord + vec2(1.0, -1.0)) * inverseVP; + v_rgbSW = (fragCoord + vec2(-1.0, 1.0)) * inverseVP; + v_rgbSE = (fragCoord + vec2(1.0, 1.0)) * inverseVP; + v_rgbM = vec2(fragCoord * inverseVP); + } + + void main() { + gl_Position = vec4(position, 1.0, 1.0); + uv = (position + 1.0) * 0.5; + uv.y = 1.0 - uv.y; + vec2 frag_coord = uv * resolution; + texcoords(frag_coord, resolution, v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM); + } +"; + +// Optimized version for mobile, where dependent texture reads can be a bottleneck +// +// Taken from: https://github.com/mattdesl/glsl-fxaa/blob/master/fxaa.glsl +// +// Basic FXAA implementation based on the code on geeks3d.com with the +// modification that the texture2DLod stuff was removed since it's +// unsupported by WebGL. +// -- +// +// From: +// +// https://github.com/mitsuhiko/webgl-meincraft +// +// Copyright (c) 2011 by Armin Ronacher. +// +// Some rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * The names of the contributors may not be used to endorse or +// promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +const SVG_FXAA_FRAG_SHADER: &str = " + #version 130 + + #define FXAA_REDUCE_MIN (1.0/ 128.0) + #define FXAA_REDUCE_MUL (1.0 / 8.0) + #define FXAA_SPAN_MAX 8.0 + + precision mediump float; + + in vec2 v_rgbNW; + in vec2 v_rgbNE; + in vec2 v_rgbSW; + in vec2 v_rgbSE; + in vec2 v_rgbM; + + uniform vec2 resolution; + uniform sampler2D source; + + vec4 fxaa(sampler2D tex, vec2 fragCoord, vec2 resolution, + vec2 v_rgbNW, vec2 v_rgbNE, + vec2 v_rgbSW, vec2 v_rgbSE, + vec2 v_rgbM) { + vec4 color; + mediump vec2 inverseVP = vec2(1.0 / resolution.x, 1.0 / resolution.y); + vec3 rgbNW = texture2D(tex, v_rgbNW).xyz; + vec3 rgbNE = texture2D(tex, v_rgbNE).xyz; + vec3 rgbSW = texture2D(tex, v_rgbSW).xyz; + vec3 rgbSE = texture2D(tex, v_rgbSE).xyz; + vec4 texColor = texture2D(tex, v_rgbM); + vec3 rgbM = texColor.xyz; + vec3 luma = vec3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot(rgbM, luma); + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + mediump vec2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * + (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), + dir * rcpDirMin)) * inverseVP; + + vec3 rgbA = 0.5 * ( + texture2D(tex, fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz + + texture2D(tex, fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz); + vec3 rgbB = rgbA * 0.5 + 0.25 * ( + texture2D(tex, fragCoord * inverseVP + dir * -0.5).xyz + + texture2D(tex, fragCoord * inverseVP + dir * 0.5).xyz); + + float lumaB = dot(rgbB, luma); + if ((lumaB < lumaMin) || (lumaB > lumaMax)) + color = vec4(rgbA, texColor.a); + else + color = vec4(rgbB, texColor.a); + return color; + } + + void main() { + gl_FragColor = fxaa(source, gl_FragCoord.xy, resolution, v_rgbNW, v_rgbNE, v_rgbSW, v_rgbSE, v_rgbM); + } +"; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct SvgLayerId(usize); diff --git a/src/widgets.rs b/src/widgets.rs index 32a951da1..e534661fb 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -136,10 +136,10 @@ impl Svg { bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), z_index: z_index, color: ( - stroke_color.color.red as f32, - stroke_color.color.green as f32, - stroke_color.color.blue as f32, - stroke_color.alpha as f32 + stroke_color.color.red as f32, + stroke_color.color.green as f32, + stroke_color.color.blue as f32, + stroke_color.alpha as f32 ), }; From c7dca5e3e123ae25cee2d8c99bae3dce4e527be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 2 Jul 2018 18:13:53 +0200 Subject: [PATCH 118/868] Fix build issues in dialogs.rs on Linux --- Cargo.toml | 2 ++ README.md | 8 +++-- examples/debug.rs | 2 +- src/dialogs.rs | 87 +++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 1 + src/svg.rs | 2 +- 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4e8aa26a..eaef7696c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" palette = "0.4.0" tinyfiledialogs = "3.3.5" + +[target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" [dependencies.resvg] diff --git a/README.md b/README.md index ee8b29825..a1233b874 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,12 @@ engine for rendering and a CSS / DOM model for layout and rendering ## Installation notes -On Linux, you currently need to install `cmake` before you can use. It is used -during the build process to compile freetype. +On Linux, you currently need to install `cmake` before you can use azul. +CMake is used during the build process to compile servo-freetype. + +``` +sudo apt install cmake +``` ## Design diff --git a/examples/debug.rs b/examples/debug.rs index 6b551223b..12408e6da 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -30,7 +30,7 @@ impl Layout for MyAppData { } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { - open_file_dialog(None, Some(&["svg"])) + open_file_dialog(None, None) .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { let mut svg_cache = SvgCache::empty(); diff --git a/src/dialogs.rs b/src/dialogs.rs index b70fd2edf..bdab3dceb 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,9 +1,6 @@ use tinyfiledialogs::MessageBoxIcon; use tinyfiledialogs::DefaultColorValue; -/// Default color in the color picker -const DEFAULT_COLOR: [u8; 3] = [0, 0, 0]; - /// Ok or cancel result, returned from the `msg_box_ok_cancel` function #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum OkCancel { @@ -31,6 +28,11 @@ impl From for ::tinyfiledialogs::OkCancel { } } +/// "Ok / Cancel" MsgBox (title, message, icon, default) +pub fn msg_box_ok_cancel(title: &str, message: &str, icon: MessageBoxIcon, default: OkCancel) -> OkCancel { + ::tinyfiledialogs::message_box_ok_cancel(title, message, icon, default.into()).into() +} + /// Yes or No result, returned from the `msg_box_yes_no` function #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum YesNo { @@ -91,9 +93,12 @@ pub fn msg_box_ok(title: &str, message: &str, icon: MessageBoxIcon) { ::tinyfiledialogs::message_box_ok(title, message, icon) } -/// "Ok / Cancel" MsgBox (title, message, icon, default) -pub fn msg_box_ok_cancel(title: &str, message: &str, icon: MessageBoxIcon, default: OkCancel) -> OkCancel { - ::tinyfiledialogs::message_box_ok_cancel(title, message, icon, default.into()).into() +/// Wrapper around `message_box_ok` with the default title "Info" + an info icon. +/// +/// Note: If you are too young to remember Visual Basics glorious `MsgBox` +/// then I pity you. Those were the days. +pub fn msg_box(content: &str) { + msg_box_ok("Info", content, MessageBoxIcon::Info); } /// Color value (hex or rgb) to open the `color_chooser_dialog` with @@ -103,6 +108,9 @@ pub enum ColorValue<'a> { RGB(&'a [u8; 3]), } +/// Default color in the color picker +const DEFAULT_COLOR: [u8; 3] = [0, 0, 0]; + impl<'a> Default for ColorValue<'a> { fn default() -> Self { ColorValue::RGB(&DEFAULT_COLOR) @@ -126,13 +134,25 @@ pub fn color_picker_dialog(title: &str, default_value: Option) ::tinyfiledialogs::color_chooser_dialog(title, default) } -// We don't use tinyfiledialogs for file dialogs -// because it doesn't handle Unicode correctly +// The difference between tinyfiledialogs and nfd is that nfd links +// to a specific dialog at compile time, while tinyfiledialogs selects +// the dialog at runtime from a set of specific dialogs (i.e. Mate, KDE, +// dolphin, whatever). This (a) doesn't force the library user to choose +// a specific dialog, (b) won't look non-native (GTK3 on a KDE env can +// look jarring) and (c) doesn't require the user to install extra libraries +// +// The only reason why we don't use tinyfiledialogs everywhere is because +// it doesn't handle unicode correctly (so if the user has öüä in his username, +// it won't return a correct file path). However, tinyfiledialogs **does** +// handle Unicode correctly on Linux. So the solution is to use tinyfiledialogs +// on Linux (because of the dependency issue) and nfd everywhere else (because +// of better Unicode) /// Open a single file, returns `None` if the user canceled the dialog. /// /// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow /// "doc" and "docx" files +#[cfg(not(target_os = "linux"))] pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) -> Option { @@ -147,7 +167,21 @@ pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]> } } +/// Open a single file, returns `None` if the user canceled the dialog. +/// +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files +#[cfg(target_os = "linux")] +pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option +{ + let filter_list = filter_list.map(|f| (f, "")); + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::open_file_dialog("Open File", path, filter_list) +} + /// Open a directory, returns `None` if the user canceled the dialog +#[cfg(not(target_os = "linux"))] pub fn open_directory_dialog(default_path: Option<&str>) -> Option { @@ -159,11 +193,20 @@ pub fn open_directory_dialog(default_path: Option<&str>) } } +/// Open a directory, returns `None` if the user canceled the dialog +#[cfg(target_os = "linux")] +pub fn open_directory_dialog(default_path: Option<&str>) +-> Option +{ + ::tinyfiledialogs::select_folder_dialog("Open Filder", default_path.unwrap_or("")) +} + /// Open multiple files at once, returns `None` if the user canceled the dialog, /// otherwise returns the `Vec` with the given file paths /// /// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow /// "doc" and "docx" files +#[cfg(not(target_os = "linux"))] pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) -> Option> { @@ -179,7 +222,22 @@ pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Optio } } +/// Open multiple files at once, returns `None` if the user canceled the dialog, +/// otherwise returns the `Vec` with the given file paths +/// +/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow +/// "doc" and "docx" files +#[cfg(target_os = "linux")] +pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) +-> Option> +{ + let filter_list = filter_list.map(|f| (f, "")); + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::open_file_dialog_multi("Open Folder", path, filter_list) +} + /// Opens a save file dialog, returns `None` if the user canceled the dialog +#[cfg(not(target_os = "linux"))] pub fn save_file_dialog(default_path: Option<&str>) -> Option { @@ -191,12 +249,13 @@ pub fn save_file_dialog(default_path: Option<&str>) } } -/// Wrapper around `message_box_ok` with the default title "Info" + an info icon. -/// -/// Note: If you are too young to remember Visual Basics glorious `MsgBox` -/// then I pity you. Those were the days. -pub fn msg_box(content: &str) { - msg_box_ok("Info", content, MessageBoxIcon::Info); +/// Opens a save file dialog, returns `None` if the user canceled the dialog +#[cfg(target_os = "linux")] +pub fn save_file_dialog(default_path: Option<&str>) +-> Option +{ + let path = default_path.unwrap_or(""); + ::tinyfiledialogs::save_file_dialog("Save File", path) } // TODO (at least on Windows): diff --git a/src/lib.rs b/src/lib.rs index 4c033c570..e98aa2279 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ extern crate app_units; extern crate unicode_normalization; extern crate harfbuzz_rs; extern crate tinyfiledialogs; +#[cfg(not(target_os = "linux"))] extern crate nfd; /// DOM / HTML node handling diff --git a/src/svg.rs b/src/svg.rs index 365456237..efad04d14 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -874,7 +874,7 @@ mod svg_to_lyon { use webrender::api::ColorU; use FastHashMap; - pub fn parse_from, T: Layout>(mut svg_source: S, view_boxes: &mut FastHashMap) + pub fn parse_from, T: Layout>(svg_source: S, view_boxes: &mut FastHashMap) -> Result<(Vec>, FastHashMap), SvgParseError> { let opt = Options::default(); let rtree = Tree::from_str(svg_source.as_ref(), &opt).unwrap(); From eeb42371a6070579bc3a6ba96f123823e71a575f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 3 Jul 2018 17:51:30 +0200 Subject: [PATCH 119/868] Added simple clipboard get / set methods --- Cargo.toml | 1 + README.md | 7 +++++-- src/app.rs | 16 +++++++++++++++- src/app_state.rs | 15 +++++++++++++++ src/lib.rs | 4 ++++ src/resources.rs | 17 ++++++++++++++++- 6 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eaef7696c..9af8a72f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" palette = "0.4.0" tinyfiledialogs = "3.3.5" +clipboard2 = "0.1.0" [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" diff --git a/README.md b/README.md index a1233b874..95e57a2f1 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@ engine for rendering and a CSS / DOM model for layout and rendering ## Installation notes On Linux, you currently need to install `cmake` before you can use azul. -CMake is used during the build process to compile servo-freetype. +CMake is used during the build process to compile servo-freetype. + +For interfacing with the system clipboard, you also need `xorg-dev` or +`libxcb-xkb-dev`. ``` -sudo apt install cmake +sudo apt install cmake libxcb-xkb-dev ``` ## Design diff --git a/src/app.rs b/src/app.rs index f9fc39023..e67b4fc54 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ use image::ImageError; use euclid::TypedScale; use { images::ImageType, - errors::FontError, + errors::{FontError, ClipboardError}, window::{Window, WindowCreateOptions, WindowCreateError, WindowId}, css_parser::{Font as FontId, PixelValue, FontSize}, text_cache::TextId, @@ -480,6 +480,20 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.clear_all_texts(); } + /// Get the contents of the system clipboard as a string + pub(crate) fn get_clipboard_string(&mut self) + -> Result + { + self.app_state.get_clipboard_string() + } + + /// Set the contents of the system clipboard as a string + pub(crate) fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> + { + self.app_state.set_clipboard_string(contents) + } + /// Mock rendering function, for creating a hidden window and rendering one frame /// Used in unit tests. You **have** to enable software rendering, otherwise, /// this function won't work in a headless environment. diff --git a/src/app_state.rs b/src/app_state.rs index fc61b9416..1aa52643c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -17,6 +17,7 @@ use { font::FontError, svg::{SvgLayerId, SvgLayer, SvgParseError}, css_parser::{Font as FontId, FontSize, PixelValue}, + errors::ClipboardError, }; /// Wrapper for your application data. In order to be layout-able, @@ -234,6 +235,20 @@ impl<'a, T: Layout> AppState<'a, T> { pub fn clear_all_texts(&mut self) { self.resources.clear_all_texts(); } + + /// Get the contents of the system clipboard as a string + pub(crate) fn get_clipboard_string(&mut self) + -> Result + { + self.resources.get_clipboard_string() + } + + /// Set the contents of the system clipboard as a string + pub(crate) fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> + { + self.resources.set_clipboard_string(contents) + } } impl<'a, T: Layout + Send + 'static> AppState<'a, T> { diff --git a/src/lib.rs b/src/lib.rs index e98aa2279..fc60aa3f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,8 @@ extern crate app_units; extern crate unicode_normalization; extern crate harfbuzz_rs; extern crate tinyfiledialogs; +extern crate clipboard2; + #[cfg(not(target_os = "linux"))] extern crate nfd; @@ -158,4 +160,6 @@ pub mod errors { pub use font::FontError; pub use window::WindowCreateError; pub use image::ImageError; + // TODO: re-export the sub-types of ClipboardError! + pub use clipboard2::ClipboardError; } diff --git a/src/resources.rs b/src/resources.rs index 342b96922..49937de14 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -21,6 +21,7 @@ use css_parser; use css_parser::Font::ExternalFont; use svg::{SvgLayerId, SvgLayer, SvgParseError}; use text_cache::TextId; +use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; /// Font and image keys /// @@ -32,7 +33,6 @@ use text_cache::TextId; /// /// Images and fonts can be references across window contexts /// (not yet tested, but should work). -#[derive(Clone)] pub(crate) struct AppResources<'a> { /// When looking up images, there are two sources: Either the indirect way via using a /// CssId (which is a String) or a direct ImageId. The indirect way requires one extra @@ -54,6 +54,8 @@ pub(crate) struct AppResources<'a> { pub(crate) fonts: FastHashMap>, /// Stores long texts across frames pub(crate) text_cache: TextCache, + /// Keyboard clipboard storage and retrieval functionality + clipboard: SystemClipboard, } impl<'a> Default for AppResources<'a> { @@ -64,6 +66,7 @@ impl<'a> Default for AppResources<'a> { font_data: FastHashMap::default(), images: FastHashMap::default(), text_cache: TextCache::default(), + clipboard: SystemClipboard::new().unwrap(), } } } @@ -207,6 +210,18 @@ impl<'a> AppResources<'a> { pub(crate) fn clear_all_texts(&mut self) { self.text_cache.clear_all_texts(); } + + pub(crate) fn get_clipboard_string(&mut self) + -> Result + { + self.clipboard.get_string_contents() + } + + pub(crate) fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> + { + self.clipboard.set_string_contents(contents) + } } // Empty test, for some reason codecov doesn't detect any files (and therefore From 5d1bd54326fd3abb5402705d2530bf53115bbfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 4 Jul 2018 18:53:23 +0200 Subject: [PATCH 120/868] Removed useless GetDom trait, added offset for SVG shader --- src/dom.rs | 15 +++++---------- src/lib.rs | 2 +- src/svg.rs | 3 ++- src/traits.rs | 4 ---- src/widgets.rs | 13 ++++++------- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/dom.rs b/src/dom.rs index f86a09545..7e43d0939 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -76,12 +76,7 @@ impl Eq for Callback { } impl Copy for Callback { } -/// List of allowed DOM node types that are supported by `azul`. -/// -/// All node types are purely convenience functions around `Div`, -/// `Image` and `Label`. For example a `Ul` is simply a convenience -/// wrapper around a repeated (`Div` + `Label`) clone where the first -/// `Div` is shaped like a circle (for `Ul`). +/// List of core DOM node types built-into by `azul`. #[derive(Debug, Clone, PartialEq, Hash, Eq)] pub enum NodeType { /// Regular div with no particular type of data attached @@ -439,12 +434,12 @@ impl Dom { #[test] fn test_dom_sibling_1() { - + use window::WindowInfo; struct TestLayout { } - impl Layout for TestLayout { + impl Layout for TestLayout { fn layout(&self) -> Dom { Dom::new(NodeType::Div) .with_child( @@ -469,7 +464,7 @@ fn test_dom_sibling_1() { arena[dom.root] .first_child().expect("root has no first child") ].data.id); - + assert_eq!(Some(String::from("sibling-2")), arena[ arena[ @@ -477,7 +472,7 @@ fn test_dom_sibling_1() { .first_child().expect("root has no first child") ].next_sibling().expect("root has no second sibling") ].data.id); - + assert_eq!(Some(String::from("sibling-1-child-1")), arena[ arena[ diff --git a/src/lib.rs b/src/lib.rs index fc60aa3f8..b116ea5c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ pub mod prelude { pub use app_state::AppState; pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; - pub use traits::{Layout, ModifyAppState, GetDom}; + pub use traits::{Layout, ModifyAppState}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; diff --git a/src/svg.rs b/src/svg.rs index efad04d14..d0644e7df 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -64,10 +64,11 @@ const SVG_VERTEX_SHADER: &str = " uniform vec2 bbox_origin; uniform vec2 bbox_size; + uniform vec2 offset; uniform float z_index; void main() { - gl_Position = vec4(vec2(-1.0) + ((xy - bbox_origin) / bbox_size), z_index, 1.0); + gl_Position = vec4(vec2(-1.0) + ((xy - bbox_origin) / bbox_size) + (offset / bbox_size), z_index, 1.0); }"; const SVG_FRAGMENT_SHADER: &str = " diff --git a/src/traits.rs b/src/traits.rs index d6810b70d..7baad3a3f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,10 +14,6 @@ use { css_parser::{ParsedCssProperty, CssParsingError}, }; -pub trait GetDom { - fn dom(self) -> Dom; -} - pub trait Layout { /// Updates the DOM, must be provided by the final application. /// diff --git a/src/widgets.rs b/src/widgets.rs index e534661fb..6d316b5ce 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,7 +1,7 @@ use { svg::{SvgCache, SvgLayerId}, window::ReadOnlyWindow, - traits::{GetDom, Layout}, + traits::Layout, dom::{Dom, NodeType}, images::ImageId, }; @@ -32,10 +32,8 @@ impl Button { content: ButtonContent::Image(image), } } -} -impl GetDom for Button { - fn dom(self) -> Dom { + pub fn dom(self) -> Dom { use self::ButtonContent::*; let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); button_root.add_child(match self.content { @@ -97,6 +95,7 @@ impl Svg { let z_index: f32 = 0.5; let bbox = Svg::make_bbox((0.0, 0.0), (800.0, 600.0)); let shader = svg_cache.init_shader(window); + let offset = (400.0_f32, 200.0_f32); { let mut surface = tex.as_surface(); @@ -121,6 +120,7 @@ impl Svg { color.color.blue as f32, color.alpha as f32 ), + offset: (offset.0 as f32, offset.1 as f32) }; surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); @@ -141,6 +141,7 @@ impl Svg { stroke_color.color.blue as f32, stroke_color.alpha as f32 ), + offset: (offset.0 as f32, offset.1 as f32) }; surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); @@ -173,10 +174,8 @@ impl Label { pub fn new>(text: S) -> Self { Self { text: text.into() } } -} -impl GetDom for Label { - fn dom(self) -> Dom { + pub fn dom(self) -> Dom { Dom::new(NodeType::Label(self.text)) } } From f8547abec2017eddf7c10676a3ff57a03e5dd5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 4 Jul 2018 20:07:32 +0200 Subject: [PATCH 121/868] Fixed the DOM add_child() and add_sibling() functions See: https://gist.github.com/fschutt/4b3bd9a2654b548a6eb0b6a8623bdc8a for the original algorithm in Python. Essentially we copy the child / sibling nodes to the self.nodes and just fix up the node IDs respectively. While this is highly unsafe, it is performant in practice. I think that we can also remove the RefCell now, since that was only needed in the broken implementation, which relied on cloning the DOM node-for-node. --- src/dom.rs | 150 ++++++++++++++++++++++++++++++++++++++++--------- src/id_tree.rs | 71 ++++++++++++++++++----- 2 files changed, 179 insertions(+), 42 deletions(-) diff --git a/src/dom.rs b/src/dom.rs index 7e43d0939..2223be1d7 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -277,8 +277,7 @@ impl NodeData { pub struct Dom { pub(crate) arena: Rc>>>, pub(crate) root: NodeId, - pub(crate) current_root: NodeId, - pub(crate) last: NodeId, + pub(crate) head: NodeId, } impl fmt::Debug for Dom { @@ -287,13 +286,11 @@ impl fmt::Debug for Dom { "Dom {{ \ \tarena: {:?}, \ \troot: {:?}, \ - \tcurrent_root: {:?}, \ - \tlast: {:?}, \ + \thead: {:?}, \ }}", self.arena, self.root, - self.current_root, - self.last) + self.head) } } @@ -334,30 +331,129 @@ impl Dom { Self { arena: Rc::new(RefCell::new(arena)), root: root, - current_root: root, - last: root, + head: root, } } - /// Adds a child DOM to the current DOM - #[inline] - pub fn add_child(&mut self, child: Self) { - for ch in child.root.children(&*child.arena.borrow()) { - let new_last = (*self.arena.borrow_mut()).new_node((*child.arena.borrow())[ch].data.special_clone()); - self.last.append(new_last, &mut self.arena.borrow_mut()); - self.last = new_last; + /// Adds a sibling to the current DOM + pub fn add_sibling(&mut self, sibling: Self) { + use id_tree::Node; + + let self_len = self.arena.borrow().nodes_len(); + let sibling_len = sibling.arena.borrow().nodes_len(); + + let mut self_arena = self.arena.borrow_mut(); + let mut sibling_arena = sibling.arena.borrow_mut(); + + for node_id in 0..sibling_len { + + let node: &mut Node> = &mut sibling_arena[NodeId::new(node_id)]; + + { + let mut b_node_parent_is_some = false; + if let Some(parent) = node.parent_mut() { + *parent = *parent + self_len; + b_node_parent_is_some = true; + } + if !b_node_parent_is_some { + node.parent = self_arena[self.head].parent; + } + } + + { + let mut b_node_previous_sibling_is_some = false; + if let Some(previous_sibling) = node.previous_sibling_mut() { + *previous_sibling = *previous_sibling + self_len; + b_node_previous_sibling_is_some = true; + } + if !b_node_previous_sibling_is_some { + node.previous_sibling = Some(self.head); + } + } + + if let Some(next_sibling) = node.next_sibling_mut() { + *next_sibling = *next_sibling + self_len; + } + + if let Some(first_child) = node.first_child_mut() { + *first_child = *first_child + self_len; + } + + if let Some(last_child) = node.last_child_mut() { + *last_child = *last_child + self_len; + } } + + let head_node_id = NodeId::new(self_len); + self_arena[self.head].next_sibling = Some(head_node_id); + self.head = head_node_id; + (&mut *self_arena).append(&mut sibling_arena); } - /// Adds a sibling to the current DOM - #[inline] - pub fn add_sibling(&mut self, sibling: Self) { - for sib in sibling.root.following_siblings(&*sibling.arena.borrow()) { - let sibling_clone = (*sibling.arena.borrow())[sib].data.special_clone(); - let new_sibling = (*self.arena.borrow_mut()).new_node(sibling_clone); - self.current_root.insert_after(new_sibling, &mut self.arena.borrow_mut()); - self.current_root = new_sibling; + /// Adds a child DOM to the current DOM + pub fn add_child(&mut self, child: Self) { + + use id_tree::Node; + + let self_len = self.arena.borrow().nodes_len(); + let child_len = child.arena.borrow().nodes_len(); + + let mut self_arena = self.arena.borrow_mut(); + let mut child_arena = child.arena.borrow_mut(); + + let mut last_sibling = None; + + for node_id in 0..child_len { + let node_id = NodeId::new(node_id); + let node: &mut Node> = &mut child_arena[node_id]; + + // WARNING: Order of these blocks is important! + { + let mut b_node_previous_sibling_is_some = false; + if let Some(previous_sibling) = node.previous_sibling_mut() { + *previous_sibling = *previous_sibling + self_len; + b_node_previous_sibling_is_some = true; + } + if !b_node_previous_sibling_is_some { + let last_child = self_arena[self.head].last_child; + if last_child.is_some() && node.parent.is_none() { + node.previous_sibling = last_child; + self_arena[last_child.unwrap()].next_sibling = Some(node_id + self_len); + } + } + } + + { + let mut b_node_parent_is_some = false; + if let Some(parent) = node.parent_mut() { + *parent = *parent + self_len; + b_node_parent_is_some = true; + } + if !b_node_parent_is_some { + if node.next_sibling.is_none() { + // We have encountered the last root item + last_sibling = Some(node_id); + } + node.parent = Some(self.head); + } + } + + if let Some(next_sibling) = node.next_sibling_mut() { + *next_sibling = *next_sibling + self_len; + } + + if let Some(first_child) = node.first_child_mut() { + *first_child = *first_child + self_len; + } + + if let Some(last_child) = node.last_child_mut() { + *last_child = *last_child + self_len; + } } + + self_arena[self.head].first_child.get_or_insert(NodeId::new(self_len)); + self_arena[self.head].last_child = Some(last_sibling.unwrap() + self_len); + (&mut *self_arena).append(&mut child_arena); } /// Same as `id`, but easier to use for method chaining in a builder-style pattern @@ -395,18 +491,18 @@ impl Dom { #[inline] pub fn set_id>(&mut self, id: S) { - self.arena.borrow_mut()[self.last].data.id = Some(id.into()); + self.arena.borrow_mut()[self.head].data.id = Some(id.into()); } #[inline] pub fn set_class>(&mut self, class: S) { - self.arena.borrow_mut()[self.last].data.classes.push(class.into()); + self.arena.borrow_mut()[self.head].data.classes.push(class.into()); } #[inline] pub fn set_callback(&mut self, on: On, callback: Callback) { - self.arena.borrow_mut()[self.last].data.events.callbacks.insert(on, callback); - self.arena.borrow_mut()[self.last].data.tag = Some(NODE_ID.fetch_add(1, Ordering::SeqCst) as u64); + self.arena.borrow_mut()[self.head].data.events.callbacks.insert(on, callback); + self.arena.borrow_mut()[self.head].data.tag = Some(NODE_ID.fetch_add(1, Ordering::SeqCst) as u64); } } diff --git a/src/id_tree.rs b/src/id_tree.rs index 19e5fdcfd..f44b34016 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -3,7 +3,7 @@ use std::{ mem, fmt, - ops::{Index, IndexMut, Deref}, + ops::{Index, Add, IndexMut, Deref}, hash::{Hasher, Hash}, collections::BTreeMap, cmp::Ordering, @@ -51,6 +51,13 @@ impl NonZeroUsizeHack { } } +impl Add for NonZeroUsizeHack { + type Output = NonZeroUsizeHack; + fn add(self, other: usize) -> NonZeroUsizeHack { + NonZeroUsizeHack::new(self.get() + other) + } +} + impl PartialOrd for NonZeroUsizeHack { fn partial_cmp(&self, other: &NonZeroUsizeHack) -> Option { Some(self.get().cmp(&other.get())) @@ -98,6 +105,16 @@ impl NodeId { } } +impl Add for NodeId { + type Output = NodeId; + + fn add(self, other: usize) -> NodeId { + NodeId { + index: self.index + other + } + } +} + impl fmt::Display for NodeId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.index.get()) @@ -106,20 +123,18 @@ impl fmt::Display for NodeId { #[derive(Clone, PartialEq)] pub struct Node { - // Keep these private (with read-only accessors) so that we can keep them consistent. - // E.g. the parent of a node’s child is that node. - parent: Option, - previous_sibling: Option, - next_sibling: Option, - first_child: Option, - last_child: Option, + pub(crate) parent: Option, + pub(crate) previous_sibling: Option, + pub(crate) next_sibling: Option, + pub(crate) first_child: Option, + pub(crate) last_child: Option, pub data: T, } // Manual implementation, since `#[derive(Debug)]` requires `T: Debug` impl fmt::Debug for Node { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, + write!(f, "Node {{ \ parent: {:?}, \ previous_sibling: {:?}, \ @@ -191,9 +206,9 @@ impl Arena { } /// Return an iterator over the indices in the internal arenas Vec - pub fn linear_iter(&self) -> LinearIterator { + pub fn linear_iter(&self) -> LinearIterator { LinearIterator { - arena: &self, + arena_len: self.nodes.len(), position: 0, } } @@ -222,6 +237,12 @@ impl Arena { pub fn is_empty(&self) -> bool { self.nodes_len() == 0 } + + /// Appends another arena to the end of the current arena. + /// Highly unsafe if you don't know what you're doing + pub(crate) fn append(&mut self, other: &mut Arena) { + self.nodes.append(&mut other.nodes); + } } impl Arena { @@ -274,19 +295,39 @@ impl IndexMut for Arena { impl Node { /// Return the ID of the parent node, unless this node is the root of the tree. + #[inline(always)] pub fn parent(&self) -> Option { self.parent } + #[inline(always)] + pub fn parent_mut(&mut self) -> Option<&mut NodeId> { self.parent.as_mut() } + /// Return the ID of the first child of this node, unless it has no child. + #[inline(always)] pub fn first_child(&self) -> Option { self.first_child } + #[inline(always)] + pub fn first_child_mut(&mut self) -> Option<&mut NodeId> { self.first_child.as_mut() } + /// Return the ID of the last child of this node, unless it has no child. + #[inline(always)] pub fn last_child(&self) -> Option { self.last_child } + #[inline(always)] + pub fn last_child_mut(&mut self) -> Option<&mut NodeId> { self.last_child.as_mut() } + /// Return the ID of the previous sibling of this node, unless it is a first child. + #[inline(always)] pub fn previous_sibling(&self) -> Option { self.previous_sibling } + #[inline(always)] + pub fn previous_sibling_mut(&mut self) -> Option<&mut NodeId> { self.previous_sibling.as_mut() } + /// Return the ID of the previous sibling of this node, unless it is a first child. + #[inline(always)] pub fn next_sibling(&self) -> Option { self.next_sibling } + + #[inline(always)] + pub fn next_sibling_mut(&mut self) -> Option<&mut NodeId> { self.next_sibling.as_mut() } } @@ -499,16 +540,16 @@ macro_rules! impl_node_iterator { /// An linear iterator, does not respec the DOM in any way, /// it just iterates over the nodes like a Vec -pub struct LinearIterator<'a, T: 'a> { - arena: &'a Arena, +pub struct LinearIterator { + arena_len: usize, position: usize, } -impl<'a, T> Iterator for LinearIterator<'a, T> { +impl Iterator for LinearIterator { type Item = NodeId; fn next(&mut self) -> Option { - if self.position > (self.arena.nodes.len() - 1) { + if self.position > (self.arena_len - 1) { None } else { let new_id = Some(NodeId::new(self.position)); From e5ce8508b773931297bd7e5034108232549e0b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 6 Jul 2018 09:17:09 +0200 Subject: [PATCH 122/868] Initial event filtering implemented (mouseup / mousedown) Still buggy... for whatever reason the screen doesn't update / redraw when we load the tiger SVG - but at least we open the "open file" dialog once instead of on every mouse move. --- Cargo.toml | 2 +- README.md | 135 +++++++++++++++++++------------------- src/app.rs | 156 +++++++++++++++++++++++++------------------- src/window_state.rs | 85 ++++++++++++++++++++---- 4 files changed, 230 insertions(+), 148 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9af8a72f7..089c64f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "34a498f7e46c385a189299e7369e204e1cb2060c" +rev = "686eca6e7ce3501b3bd5349d33684aaead73857d" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/README.md b/README.md index 95e57a2f1..fb83d34b5 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,22 @@ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) -[![coveralls](https://coveralls.io/repos/github/maps4print/azul/badge.svg?branch=master)](https://coveralls.io/github/maps4print/azul?branch=master) [![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) [![Rust Compiler Version](https://img.shields.io/badge/rustc-1.26%20stable-blue.svg)]() azul is a cross-platform, stylable GUI framework using Mozillas `webrender` engine for rendering and a CSS / DOM model for layout and rendering -[Crates.io](https://crates.io/crates/azul) | -[Library documentation](https://docs.rs/azul) | +[Crates.io](https://crates.io/crates/azul) | +[Library documentation](https://docs.rs/azul) | [User guide](http://azul.rs/) ## Installation notes -On Linux, you currently need to install `cmake` before you can use azul. +On Linux, you currently need to install `cmake` before you can use azul. CMake is used during the build process to compile servo-freetype. -For interfacing with the system clipboard, you also need `xorg-dev` or +For interfacing with the system clipboard, you also need `xorg-dev` or `libxcb-xkb-dev`. ``` @@ -35,15 +34,15 @@ azul is a library designed from the experience gathered during working with other GUI toolkits. azul is very different from (QT / GTK / FLTK / etc.) in the following regards: -- GUIs are seen as a "view" into your applications data, they are not "objects" - like in any other toolkit. There is no `button.setActive(true)` for example, +- GUIs are seen as a "view" into your applications data, they are not "objects" + like in any other toolkit. There is no `button.setActive(true)` for example, as that would introduce stateful design. - Widgets types are simply enums that "serialize" themselves into a DOM tree. - The DOM is immutable and gets re-generated every frame. This makes testing and debugging very easy, since if you give the `get_dom()` function a - specific data model, you always get the same DOM back (`get_dom()` is a pure - function). This comes at a slight performance cost, however in practice the - cost isn't too high and it makes the seperation of application data and GUI + specific data model, you always get the same DOM back (`get_dom()` is a pure + function). This comes at a slight performance cost, however in practice the + cost isn't too high and it makes the seperation of application data and GUI data very clean. - The layout model closely follows the CSS flexbox model. The default for CSS is `display:flex` instead of `display:static` (keep that in mind). Some semantics @@ -51,7 +50,7 @@ following regards: However, most attributes work in azul, same as they do in CSS, i.e. `color`, `linear-gradient`, etc. - azul trades a slight bit of performance for better usability. azul is not meant - for game UIs, it is currently too slow for that (currently using 2 - 4 ms per + for game UIs, it is currently too slow for that (currently using 2 - 4 ms per frame) - azul links everything statically, including freetype and OSMesa (in case the target system has no hardware accelerated drawing available) @@ -69,16 +68,16 @@ This creates a very simple programming flow: use azul::prelude::*; use azul::widgets::*; -// Your data model that stores everything except the visual +// Your data model that stores everything except the visual // representation of the app #[derive(Default)] struct DataModel { - // Store anything relevant to the application here + // Store anything relevant to the application here // i.e. settings, a counter, user login data, application data - // the current application zoom, whatever. + // the current application zoom, whatever. // // This decouples visual components from another - instead of - // updating the visual representation of another component + // updating the visual representation of another component // on an event, they only update the data they operate on. // // For example: @@ -89,14 +88,14 @@ struct DataModel { impl Layout for DataModel { fn layout(&self, _info: WindowInfo) -> Dom { // DataModel is read-only here, "serialize" from the data model into a UI - // + // // Conditional logic / updating the contents of an existing window // is very easy - instead of something like `screen.remove(LoginButton);` // `screen.add(HelloLabel)`, we can simply write: match self.user_name { None => Button::with_text("Please log in").dom() .with_event(On::MouseUp, Callback(login_callback)), - Some((user, _)) => Label::with_text(format!("Hello {}", user)).dom() + Some((user, _)) => Label::with_text(format!("Hello {}", user)).dom() } } } @@ -118,7 +117,7 @@ fn main() { } ``` -This makes it easy to compose the UI from a set of functions, where each +This makes it easy to compose the UI from a set of functions, where each function creates a sub-DOM that can be composed into a larger UI: ```rust @@ -162,10 +161,10 @@ fn send_email(app_state: &mut AppState, _: WindowEvent) -> UpdateScre And this is why azul doesn't really have a large API like other frameworks - that's really all there is to it! The widgets themselves might require you to -pass a cache or some state across into the `.dom()` function, but the core -model doesn't change. Meaning, if you remove / add visual components, it +pass a cache or some state across into the `.dom()` function, but the core +model doesn't change. Meaning, if you remove / add visual components, it doesn't break the whole application as long as the data model stays the same. -A visual component has no knowledge of any other components, it only acts on +A visual component has no knowledge of any other components, it only acts on the data model. The benefit of this is that it's very simple to refactor and to test: @@ -194,20 +193,20 @@ fn test_layout_email_dom() { } ``` -The inner workings of the DOM are only available in testing functions, not -regular code. +The inner workings of the DOM are only available in testing functions, not +regular code. -You might have noticed that a `WindowEvent` gets passed to the callback. -This struct contains callback information that is necessary to determine what -item (of a bigger list, for example) was interacted with. Ex. if you have +You might have noticed that a `WindowEvent` gets passed to the callback. +This struct contains callback information that is necessary to determine what +item (of a bigger list, for example) was interacted with. Ex. if you have 100 list items, you don't want to write 100 callbacks for each one, but rather -have one callback that acts on which ID was selected. The `WindowEvent` +have one callback that acts on which ID was selected. The `WindowEvent` gives you the necessary information to react to these events. ## Updating window properties You may also have noticed that the callback takes in a `AppState`, -not the `DataModel` directly. This is because you can change the window +not the `DataModel` directly. This is because you can change the window settings, for example the title of the window: ```rust @@ -217,19 +216,19 @@ fn callback(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen } ``` -Note how there isn't any `.get_title()` or `.set_title()`. Simply setting the -title is enough to invoke the (stateful) Win32 / X11 / Wayland / Cocoa functions -for setting the window title. You can query the active title / mouse or keyboard +Note how there isn't any `.get_title()` or `.set_title()`. Simply setting the +title is enough to invoke the (stateful) Win32 / X11 / Wayland / Cocoa functions +for setting the window title. You can query the active title / mouse or keyboard state in the same way. ## Async I/O When you have to perform a larger task, such as waiting for network content or -waiting for a large file to be loaded, you don't want to block the user -interface, which would give a bad experience. +waiting for a large file to be loaded, you don't want to block the user +interface, which would give a bad experience. -Instead, azul provides two mechanisms: a `Task` and a `Deamon`. Both are -essentially function callbacks, but the `Task` gets run on a seperate thread +Instead, azul provides two mechanisms: a `Task` and a `Deamon`. Both are +essentially function callbacks, but the `Task` gets run on a seperate thread (one thread per task) while a `Deamon` gets run on the same thread as the main UI. @@ -246,11 +245,11 @@ struct DataModel { impl Layout for DataModel { fn layout(&self, _: WindowInfo) -> Dom { match self.website { - None => + None => Dom::new() .with_child(Button::labeled("Click to download").dom()) .with_event(On::MouseUp, Callback(start_download)) - Some(data) => + Some(data) => Dom::new() .with_child(Label::new(data.clone()).dom()), } @@ -271,9 +270,9 @@ fn download_website(app_state: Arc>>, _drop: Arc<()>) The `app_state.modify` is a only conveniece function that locks and unlocks your data model. The `_drop` variable is necessary so that azul can see when the thread -has finished and join it afterwards. +has finished and join it afterwards. -A `Task` starts one full, OS-level thread. Usually doing this is a bad idea for +A `Task` starts one full, OS-level thread. Usually doing this is a bad idea for performance, since you may at one point have too many threads running. However, in desktop applications, you usually don't run 1000 tasks at once, maybe 4 - 5 maximum. For this, OS-level threads are usually sufficient and performant enough. @@ -296,7 +295,7 @@ The default styles are implemented using CSS classes, with the special name ## Dynamic CSS properties You can override CSS properties from Rust during runtime, but after every frame -the modifications are cleared again. You do not have to "unset" a CSS style once the +the modifications are cleared again. You do not have to "unset" a CSS style once the state of your application changes. Example: ```rust @@ -327,34 +326,34 @@ fn main() { } ``` -The `[[ variable | default ]]` denotes a "dynamic CSS property". If the -`theme_background_color` variable isn't set for this frame, it will use the -default. The reason why the CSS state is unset on every frame is to prevent -forgetting to unset it. Especially with highly conditional styling, this +The `[[ variable | default ]]` denotes a "dynamic CSS property". If the +`theme_background_color` variable isn't set for this frame, it will use the +default. The reason why the CSS state is unset on every frame is to prevent +forgetting to unset it. Especially with highly conditional styling, this can easily lead to bugs (i.e. only set this button to green if a certain setting is set to true, but not if today is Wednesday). The second reason is to make CSS properties testable, i.e. in the same way that you can test state properties, you can test CSS properties. Azuls philosophy is that the state of the previous frame never affects the current frame. Only the -data model can affect the visual content, there is no `.setBackground("blue")` -because at some point, you'd have to write a function to un-set the background +data model can affect the visual content, there is no `.setBackground("blue")` +because at some point, you'd have to write a function to un-set the background again - this stateful design can quickly lead to visual bugs. ## SVG / OpenGL API -For drawing custom graphics, azul has a high-performance 2D vector API. It also +For drawing custom graphics, azul has a high-performance 2D vector API. It also allows you to load and draw SVG files (with the exceptions of gradients: gradients in SVG files are not yet supported). But azul itself does not know about SVG shapes at all - so how the SVG widget implemented? The solution is to draw the SVG to an OpenGL texture and hand that to azul. This way, the SVG drawing component could even be implemented in an external crate, if -you really wanted to. This mechanism also allows for completely custom drawing +you really wanted to. This mechanism also allows for completely custom drawing (let's say: a game, a 3D viewer, etc.) to be drawn. The SVG component currently uses the `resvg` parser, `usvg` minifaction and the -`lyon` triangulation libraries). Of course you can also add custom shapes +`lyon` triangulation libraries). Of course you can also add custom shapes (bezier curves, circles, lines, whatever) programatically, without going through the SVG parser: @@ -381,17 +380,17 @@ fn load_svg(app_state: &mut AppState, _: WindowEvent) -> UpdateScreen ``` This is one of the few exceptions where azul allows persistent data across frames -since it wouldn't be performant enough otherwise. Ideally you'd have to load, triangulate +since it wouldn't be performant enough otherwise. Ideally you'd have to load, triangulate and draw the SVG file on every frame, but this isn't performant. You might have noticed that the `.dom()` function takes in an extra parameter: The `svg_cache` and the `info.window`. This way, the `svg_cache` handles everything necessary to cache vertex buffers / the triangulated layers and shapes, only the drawing itself is done on every frame. -Additionally, you can also register callbacks on any item **inside** the SVG using the -`SvgCallbacks`, i.e. when someone clicks on or hovers over a certain shape. In order +Additionally, you can also register callbacks on any item **inside** the SVG using the +`SvgCallbacks`, i.e. when someone clicks on or hovers over a certain shape. In order to draw your own vector data (for example in order to make a vector graphics editor), -you can build the "SVG layers" yourself (ex. from the SVG data). Each layer is +you can build the "SVG layers" yourself (ex. from the SVG data). Each layer is batch-rendered, so you can draw many lines or polygons in one draw call, as long as they share the same `SvgStyle`. @@ -399,7 +398,7 @@ they share the same `SvgStyle`. ### Implemented -This is a list of CSS attributes that are currently implemented. They work in +This is a list of CSS attributes that are currently implemented. They work in the same way as on a regular web page, except if noted otherwise: - `border-radius` @@ -419,11 +418,11 @@ the same way as on a regular web page, except if noted otherwise: Notes: -1. `image()` takes an ID instead of a URL. Images and fonts are external resources - that have to be cached. Use `app.add_image("id", my_image_data)` or - `app_state.add_image()`, then you can use the `"id"` in the CSS to select +1. `image()` takes an ID instead of a URL. Images and fonts are external resources + that have to be cached. Use `app.add_image("id", my_image_data)` or + `app_state.add_image()`, then you can use the `"id"` in the CSS to select your image. - If an image is not present on a displayed div (i.e. you added it to the CSS, + If an image is not present on a displayed div (i.e. you added it to the CSS, but forgot to add the image), the following happens: - In debug mode, the app crashes with a descriptive error message - In release mode, the app doesn't display the image and logs the error @@ -441,8 +440,8 @@ These properties are planned for the next (currently 0.1) release: ### Remarks -1. Any measurements can be given in `px`, `pt` or `em`. `pt` does not - respect high-DPI scaling, while `em` and `px` do. The default is `1em` = +1. Any measurements can be given in `px`, `pt` or `em`. `pt` does not + respect high-DPI scaling, while `em` and `px` do. The default is `1em` = `16px * high_dpi_scale` 2. CSS rules are (within a block), parsed from top to bottom, ex: ```css @@ -462,12 +461,12 @@ These properties are planned for the next (currently 0.1) release: .general #specific .very-specific { color: black; } ``` -The CSS parser currently only supports CSS 2.1, not CSS 3 attributes. Animations +The CSS parser currently only supports CSS 2.1, not CSS 3 attributes. Animations are not done in CSS, but rather by using dynamic CSS properties (see above) ### Planned -- WEBM Video playback (using libvp9 / rust-media, will be exposed the same way as +- WEBM Video playback (using libvp9 / rust-media, will be exposed the same way as images, using IDs, no breaking change) ## CPU / memory usage / startup time @@ -478,12 +477,12 @@ windows for incoming events, which consumes around 0-0.5% CPU when idling. A default window, with no fonts or images added takes up roughly 39MB of RAM and 5MB in binary size. This usage can go up once you load more images and fonts, since -azul has to load and keep the images in RAM. +azul has to load and keep the images in RAM. -The frame time (i.e. the time necessary to draw a single frame, including layout) -lies between 2 - 5 milliseconds, which equals roughly 200 - 500 frames per second. -However, azul limits this frame time and only redraws the window when absolutely -necessary, in order to not waste the users battery life. +The frame time (i.e. the time necessary to draw a single frame, including layout) +lies between 2 - 5 milliseconds, which equals roughly 200 - 500 frames per second. +However, azul limits this frame time and only redraws the window when absolutely +necessary, in order to not waste the users battery life. The startup time depends on how many fonts / images you add on startup, the default time is between 100 and 200 ms for an app with no images and a single font. @@ -496,7 +495,7 @@ Several projects have helped azul severely and should be noted here: severely with discovering undocumented parts of webrender as well as working with constraints (the `constraints.rs` file was copied from limn with the [permission of the author](https://github.com/christolliday/limn/issues/22#issuecomment-362545167)) -- Nicolas Silva for his work on [lyon](https://github.com/nical/lyon) - without this, +- Nicolas Silva for his work on [lyon](https://github.com/nical/lyon) - without this, the SVG renderer wouldn't have been possible - All webrender contributors who have been patient enough to answer my questions on IRC diff --git a/src/app.rs b/src/app.rs index e67b4fc54..03d6c5e22 100644 --- a/src/app.rs +++ b/src/app.rs @@ -163,7 +163,7 @@ impl<'a, T: Layout> App<'a, T> { let mut events = Vec::new(); window.events_loop.poll_events(|e| events.push(e)); - for event in events { + for event in &events { let should_close = process_event(event, &mut frame_event_info); if should_close { closed_windows.push(idx); @@ -184,10 +184,6 @@ impl<'a, T: Layout> App<'a, T> { } } - if frame_event_info.should_hittest { - Self::do_hit_test_and_call_callbacks(window, window_id, &mut frame_event_info, &ui_state_cache, &mut self.app_state); - } - if frame_event_info.is_resize_event || frame_event_info.should_redraw_window { // This is a hack because during a resize event, winit eats the "awakened" // event. So what we do is that we call the layout-and-render again, to @@ -203,6 +199,18 @@ impl<'a, T: Layout> App<'a, T> { // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); + if frame_event_info.should_hittest { + for event in &events { + do_hit_test_and_call_callbacks( + event, + window, + window_id, + &mut frame_event_info, + &ui_state_cache, + &mut self.app_state); + } + } + if frame_event_info.should_redraw_window || force_redraw_cache[idx] > 0 { // Call the Layout::layout() fn, get the DOM ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { @@ -258,61 +266,6 @@ impl<'a, T: Layout> App<'a, T> { window.internal.api.send_transaction(window.internal.document_id, txn); } - fn do_hit_test_and_call_callbacks( - window: &mut Window, - window_id: WindowId, - info: &mut FrameEventInfo, - ui_state_cache: &[UiState], - app_state: &mut AppState) - { - use dom::UpdateScreen; - use webrender::api::WorldPoint; - - let cursor_x = info.cur_cursor_pos.0 as f32; - let cursor_y = info.cur_cursor_pos.1 as f32; - let point = WorldPoint::new(cursor_x, cursor_y); - let hit_test_results = window.internal.api.hit_test( - window.internal.document_id, - Some(window.internal.pipeline_id), - point, - HitTestFlags::FIND_ALL); - - let mut should_update_screen = UpdateScreen::DontRedraw; - - for item in hit_test_results.items { - let callback_list_opt = ui_state_cache[window_id.id].node_ids_to_callbacks_list.get(&item.tag.0); - if let Some(callback_list) = callback_list_opt { - use window::WindowEvent; - // TODO: filter by `On` type (On::MouseOver, On::MouseLeave, etc.) - // Currently, this just invoke all actions - let window_event = WindowEvent { - window: window_id.id, - // TODO: currently we don't have information about what DOM node was hit - number_of_previous_siblings: None, - cursor_relative_to_item: (item.point_in_viewport.x, item.point_in_viewport.y), - cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), - }; - - for callback_id in callback_list.values() { - let update = (ui_state_cache[window_id.id].callback_list[callback_id].0)(app_state, window_event); - if update == UpdateScreen::Redraw { - should_update_screen = UpdateScreen::Redraw; - } - } - } - } - - if should_update_screen == UpdateScreen::Redraw { - info.should_redraw_window = true; - // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! - // Copy the current fake CSS changes to the real CSS, then clear the fake CSS again - // TODO: .clone() and .clear() can be one operation - window.css.dynamic_css_overrides = app_state.windows[window_id.id].css.dynamic_css_overrides.clone(); - // clear the dynamic CSS overrides - app_state.windows[window_id.id].css.clear(); - } - } - fn initialize_ui_state(windows: &[Window], app_state: &AppState<'a, T>) -> Vec> { @@ -481,15 +434,15 @@ impl<'a, T: Layout> App<'a, T> { } /// Get the contents of the system clipboard as a string - pub(crate) fn get_clipboard_string(&mut self) - -> Result + pub(crate) fn get_clipboard_string(&mut self) + -> Result { self.app_state.get_clipboard_string() } /// Set the contents of the system clipboard as a string - pub(crate) fn set_clipboard_string(&mut self, contents: String) - -> Result<(), ClipboardError> + pub(crate) fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> { self.app_state.set_clipboard_string(contents) } @@ -529,7 +482,71 @@ impl<'a, T: Layout + Send + 'static> App<'a, T> { } } -fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { +fn do_hit_test_and_call_callbacks( + event: &Event, + window: &mut Window, + window_id: WindowId, + info: &mut FrameEventInfo, + ui_state_cache: &[UiState], + app_state: &mut AppState) +{ + use dom::UpdateScreen; + use webrender::api::WorldPoint; + use window::WindowEvent; + use dom::Callback; + + let cursor_x = info.cur_cursor_pos.0 as f32; + let cursor_y = info.cur_cursor_pos.1 as f32; + let point = WorldPoint::new(cursor_x, cursor_y); + let hit_test_results = window.internal.api.hit_test( + window.internal.document_id, + Some(window.internal.pipeline_id), + point, + HitTestFlags::FIND_ALL); + + let mut should_update_screen = UpdateScreen::DontRedraw; + + let callbacks_filter_list = window.state.determine_callbacks(event); + if callbacks_filter_list.is_none() { return; } + let callbacks_filter_list = callbacks_filter_list.unwrap(); + + for item in hit_test_results.items { + + let callback_list = ui_state_cache[window_id.id].node_ids_to_callbacks_list.get(&item.tag.0); + if callback_list.is_none() { continue; } + let callback_list = callback_list.unwrap(); + + // TODO: currently we don't have information about what DOM node was hit + let window_event = WindowEvent { + window: window_id.id, + number_of_previous_siblings: None, + cursor_relative_to_item: (item.point_in_viewport.x, item.point_in_viewport.y), + cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), + }; + + // Invoke callback if necessary + for on_filter in &callbacks_filter_list { + if let Some(callback_id) = callback_list.get(on_filter) { + let Callback(callback_func) = ui_state_cache[window_id.id].callback_list[callback_id]; + if (callback_func)(app_state, window_event) == UpdateScreen::Redraw { + should_update_screen = UpdateScreen::Redraw; + } + } + } + } + + if should_update_screen == UpdateScreen::Redraw { + info.should_redraw_window = true; + // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! + // Copy the current fake CSS changes to the real CSS, then clear the fake CSS again + // TODO: .clone() and .clear() can be one operation + window.css.dynamic_css_overrides = app_state.windows[window_id.id].css.dynamic_css_overrides.clone(); + // clear the dynamic CSS overrides + app_state.windows[window_id.id].css.clear(); + } +} + +fn process_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> bool { use glium::glutin::WindowEvent; match event { Event::WindowEvent { @@ -537,18 +554,21 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { event } => { match event { + WindowEvent::MouseInput { .. } => { + frame_event_info.should_hittest = true; + }, WindowEvent::CursorMoved { device_id, position, modifiers, } => { frame_event_info.should_hittest = true; - frame_event_info.cur_cursor_pos = position; + frame_event_info.cur_cursor_pos = *position; let (_, _, _) = (window_id, device_id, modifiers); }, WindowEvent::Resized(w, h) => { - frame_event_info.new_window_size = Some((w, h)); + frame_event_info.new_window_size = Some((*w, *h)); frame_event_info.is_resize_event = true; frame_event_info.should_redraw_window = true; }, @@ -556,7 +576,7 @@ fn process_event(event: Event, frame_event_info: &mut FrameEventInfo) -> bool { frame_event_info.should_redraw_window = true; }, WindowEvent::HiDPIFactorChanged(dpi) => { - frame_event_info.new_dpi_factor = Some(dpi); + frame_event_info.new_dpi_factor = Some(*dpi); frame_event_info.should_redraw_window = true; }, WindowEvent::Closed => { diff --git a/src/window_state.rs b/src/window_state.rs index bd99ef8c0..72e519a98 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -59,6 +59,8 @@ impl Default for MouseState { #[derive(Debug, Clone)] pub struct WindowState { + /// Previous window state, used for determining mouseout, etc. events + pub(crate) previous_window_state: Option>, /// Current title of the window pub title: String, /// The state of the keyboard for this frame @@ -125,6 +127,7 @@ impl Default for WindowSize { impl Default for WindowState { fn default() -> Self { Self { + previous_window_state: None, title: DEFAULT_TITLE.into(), keyboard_state: KeyboardState::default(), mouse_state: MouseState::default(), @@ -148,18 +151,78 @@ impl WindowState // // This function also updates / mutates the current window state, // so that we are ready for the next frame - pub(crate) fn determine_callback(&mut self, event: &Event) -> Vec { -/* - pub enum On { - MouseOver, - MouseDown, - MouseUp, - MouseEnter, - MouseLeave, + pub(crate) fn determine_callbacks(&mut self, event: &Event) -> Option> { + + use glium::glutin::Event::WindowEvent; + use glium::glutin::WindowEvent::*; + use glium::glutin::{ElementState, MouseButton }; + + let event = if let WindowEvent { event, .. } = event { Some(event) } else { None }; + let event = event?; + + // store the current window state so we can set it in this.previous_window_state later on + let mut previous_state = Box::new(self.clone()); + previous_state.previous_window_state = None; + + let mut events_vec = Vec::::new(); + + // TODO: right mouse down / middle mouse down? + match event { + + // mouse pressed + MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { + if !self.mouse_state.left_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.left_down = true; + }, + MouseInput { state: ElementState::Pressed, button: MouseButton::Right, .. } => { + if !self.mouse_state.right_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.right_down = true; + }, + MouseInput { state: ElementState::Pressed, button: MouseButton::Middle, .. } => { + if !self.mouse_state.middle_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.middle_down = true; + }, + + + // mouse released + MouseInput { state: ElementState::Released, button: MouseButton::Left, .. } => { + if self.mouse_state.left_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.left_down = false; + }, + MouseInput { state: ElementState::Released, button: MouseButton::Right, .. } => { + if self.mouse_state.right_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.right_down = false; + }, + MouseInput { state: ElementState::Released, button: MouseButton::Middle, .. } => { + if self.mouse_state.middle_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.middle_down = false; + }, + + + _ => { + // TODO + } + } + + self.previous_window_state = Some(previous_state); + + if events_vec.is_empty() { + None + } else { + Some(events_vec) } -*/ - // TODO - Vec::new() } } From 447d388085d597099e3ba6a4e54ef83ae2aaee3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 6 Jul 2018 12:59:29 +0200 Subject: [PATCH 123/868] Updated documentation --- src/dom.rs | 62 ++++++++++++++++++++++----------------------- src/lib.rs | 2 +- src/traits.rs | 32 ++++++++++++++++-------- src/widgets.rs | 68 +++++++++++++++++++++++++++++++++++++++----------- src/window.rs | 16 ++++++++++++ 5 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/dom.rs b/src/dom.rs index 2223be1d7..3082a3d18 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -14,7 +14,7 @@ use { images::ImageId, cache::DomHash, text_cache::TextId, - traits::{Layout, GetCssId}, + traits::Layout, app_state::AppState, id_tree::{NodeId, Arena}, }; @@ -94,6 +94,24 @@ pub enum NodeType { GlTexture(Texture), } +impl NodeType { + pub(crate) fn get_css_id(&self) -> &'static str { + use self::NodeType::*; + match self { + Div => "div", + Label(_) | Text(_) => "p", + Image(_) => "image", + GlTexture(_) => "texture", + } + } +} + +/// OpenGL texture, use `ReadOnlyWindow::create_texture` to create a texture +/// +/// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` +/// when you are done with your OpenGL drawing, otherwise webrender will render +/// to the texture, not the window, so your texture will actually never show up. +/// If you use a `Texture` and you get a blank screen, this is probably why. #[derive(Debug, Clone)] pub struct Texture { pub(crate) inner: Rc, @@ -106,6 +124,15 @@ impl Texture { } } + /// Prepares the texture for drawing - you can only draw + /// on a framebuffer, the texture itself is readonly from the + /// OpenGL drivers point of view. + /// + /// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` + /// when you are done with your OpenGL drawing, otherwise webrender will render + /// to the texture instead of the window, so your texture will actually + /// never show up on the screen, since it is never rendered. + /// If you use a `Texture` and you get a blank screen, this is probably why. pub fn as_surface<'a>(&'a self) -> SimpleFrameBuffer<'a> { self.inner.as_surface() } @@ -119,6 +146,8 @@ impl Hash for Texture { } impl PartialEq for Texture { + /// Note: Comparison uses only the OpenGL ID, it doesn't compare the + /// actual contents of the texture. fn eq(&self, other: &Texture) -> bool { use glium::GlObject; self.inner.get_id() == other.inner.get_id() @@ -127,37 +156,6 @@ impl PartialEq for Texture { impl Eq for Texture { } -impl GetCssId for NodeType { - fn get_css_id(&self) -> &'static str { - use self::NodeType::*; - match *self { - Div => "div", - Label(_) | Text(_) => "p", - Image(_) => "image", - GlTexture(_) => "texture", - } - } -} - -/// State of a checkbox (disabled, checked, etc.) -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub enum CheckboxState { - /// `[■]` - Active, - /// `[✔]` - Checked, - /// Greyed out checkbox - Disabled { - /// Should the checkbox fire on a mouseover / mouseup, etc. event - /// - /// This can be useful for showing warnings / tooltips / help messages - /// as to why this checkbox is disabled - fire_on_click: bool, - }, - /// `[ ]` - Unchecked -} - /// When to call a callback action - `On::MouseOver`, `On::MouseOut`, etc. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum On { diff --git a/src/lib.rs b/src/lib.rs index b116ea5c0..0773c1b68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,7 @@ pub mod prelude { pub use app::App; pub use app_state::AppState; pub use css::{Css, FakeCss}; - pub use dom::{Dom, NodeType, Callback, CheckboxState, On, UpdateScreen}; + pub use dom::{Dom, NodeType, Callback, On, UpdateScreen}; pub use traits::{Layout, ModifyAppState}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, diff --git a/src/traits.rs b/src/traits.rs index 7baad3a3f..cf2d9ac9c 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,6 +14,8 @@ use { css_parser::{ParsedCssProperty, CssParsingError}, }; +/// The core trait that has to be implemented for the app model to provide a +/// Model -> View serialization. pub trait Layout { /// Updates the DOM, must be provided by the final application. /// @@ -35,13 +37,6 @@ pub trait Layout { } } -/// Trait for any node type, registers a new top-level CSS id, i.e. -/// `body`, `div`, etc. for custom types -pub trait GetCssId { - /// Returns the top-level CSS identifier for this - fn get_css_id(&self) -> &'static str; -} - pub(crate) struct ParsedCss<'a> { pub(crate) pure_global_rules: Vec<&'a CssRule>, pub(crate) pure_div_rules: Vec<&'a CssRule>, @@ -49,14 +44,32 @@ pub(crate) struct ParsedCss<'a> { pub(crate) pure_id_rules: Vec<&'a CssRule>, } +/// Convenience trait for the `css.set_dynamic_property()` function. +/// /// This trait exists because `TryFrom` / `TryInto` are not yet stabilized. +/// This is the same as `Into`, but with an additional error +/// case (since the parsing of the CSS value could potentially fail) /// -/// This is the same as `Into`, but with an additional error case -/// (the conversion could fail) +/// Using this trait you can write: `css.set_dynamic_property("var", ("width", "500px"))` +/// because `IntoParsedCssProperty` is implemented for `(&str, &str)`. +/// +/// Note that the properties have to be re-parsed on every frame (which incurs a +/// small per-frame performance hit), however `("width", "500px")` is easier to +/// read than `ParsedCssProperty::Width(PixelValue::Pixels(500))` pub trait IntoParsedCssProperty<'a> { fn into_parsed_css_property(self) -> Result>; } +/// Convenience trait that allows the `app_state.modify()` - only implemented for +/// `Arc` - shortly locks the app state mutex, modifies it and unlocks +/// it again. +/// +/// Note: Usually when doing asynchronous programming you don't want to block the main +/// UI. While Rust executes the `app_state.modify()` closure, your `AppState` gets +/// locked, meaning that no layout can happen and no other thread or callback can write +/// to the apps data. In order to make your app performant, don't do heavy computations +/// inside the closure, only use it to write or copy data in and out of the application +/// state. pub trait ModifyAppState { /// Modifies the app state and then returns if the modification was successful /// Takes a FnMut that modifies the state @@ -216,7 +229,6 @@ fn cascade_constraints<'a, T: Layout>( css: &Css) { for div_rule in &parsed_css.pure_div_rules { - use traits::GetCssId; if *node.node_type.get_css_id() == div_rule.html_type { push_rule(list, div_rule); } diff --git a/src/widgets.rs b/src/widgets.rs index 6d316b5ce..5958a12fa 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -21,19 +21,25 @@ pub enum ButtonContent { } impl Button { - pub fn with_label>(text: S) -> Self { + pub fn with_label(text: S) + -> Self where S: Into + { Self { content: ButtonContent::Text(text.into()), } } - pub fn with_image(image: ImageId) -> Self { + pub fn with_image(image: ImageId) + -> Self + { Self { content: ButtonContent::Image(image), } } - pub fn dom(self) -> Dom { + pub fn dom(self) + -> Dom where T: Layout + { use self::ButtonContent::*; let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); button_root.add_child(match self.content { @@ -63,23 +69,31 @@ impl Svg { // todo: remove this later #[inline] - pub fn empty() -> Self { + pub fn empty() + -> Self + { Self { layers: Vec::new(), enable_fxaa: true } } #[inline] - pub fn with_layers(layers: &Vec) -> Self { + pub fn with_layers(layers: &Vec) + -> Self + { Self { layers: layers.clone(), enable_fxaa: true } } #[inline] - pub fn with_fxaa(mut self, enable_fxaa: bool) -> Self { + pub fn with_fxaa(mut self, enable_fxaa: bool) + -> Self + { self.enable_fxaa = enable_fxaa; self } - pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom { - + /// Renders the SVG to an OpenGL texture and creates the DOM + pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) + -> Dom where T: Layout + { const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; window.make_current(); @@ -93,7 +107,10 @@ impl Svg { }; let z_index: f32 = 0.5; - let bbox = Svg::make_bbox((0.0, 0.0), (800.0, 600.0)); + let bbox: TypedRect = TypedRect { + origin: TypedPoint2D::new(0.0, 0.0), + size: TypedSize2D::new(800.0, 600.0), + }; let shader = svg_cache.init_shader(window); let offset = (400.0_f32, 200.0_f32); @@ -157,10 +174,6 @@ impl Svg { Dom::new(NodeType::GlTexture(tex)) } - - pub fn make_bbox((origin_x, origin_y): (f32, f32), (size_x, size_y): (f32, f32)) -> TypedRect { - TypedRect::::new(TypedPoint2D::new(origin_x, origin_y), TypedSize2D::new(size_x, size_y)) - } } // --- label @@ -171,15 +184,40 @@ pub struct Label { } impl Label { - pub fn new>(text: S) -> Self { + pub fn new(text: S) + -> Self where S: Into + { Self { text: text.into() } } - pub fn dom(self) -> Dom { + pub fn dom(self) + -> Dom where T: Layout + { Dom::new(NodeType::Label(self.text)) } } +// -- checkbox (TODO) + +/// State of a checkbox (disabled, checked, etc.) +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum CheckboxState { + /// `[■]` + Active, + /// `[✔]` + Checked, + /// Greyed out checkbox + Disabled { + /// Should the checkbox fire on a mouseover / mouseup, etc. event + /// + /// This can be useful for showing warnings / tooltips / help messages + /// as to why this checkbox is disabled + fire_on_click: bool, + }, + /// `[ ]` + Unchecked +} + // Empty test, for some reason codecov doesn't detect any files (and therefore // doesn't report codecov % correctly) except if they have at least one test in // the file. This is an empty test, which should be updated later on diff --git a/src/window.rs b/src/window.rs index 3266dd46e..5522fa1a3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -61,6 +61,8 @@ pub struct FakeWindow { } impl FakeWindow { + /// Returns a read-only window which can be used to create / draw + /// custom OpenGL texture during the `.layout()` phase pub fn get_window(&self) -> ReadOnlyWindow { ReadOnlyWindow { inner: self.read_only_window.clone() @@ -68,6 +70,8 @@ impl FakeWindow { } } +/// Read-only window which can be used to create / draw +/// custom OpenGL texture during the `.layout()` phase pub struct ReadOnlyWindow { pub(crate) inner: Rc, } @@ -92,6 +96,8 @@ impl ReadOnlyWindow { Texture::new(tex) } + /// Make the window active (OpenGL) - necessary before + /// starting to draw on any window-owned texture pub fn make_current(&self) { unsafe { use glium::glutin::GlContext; @@ -99,6 +105,10 @@ impl ReadOnlyWindow { } } + /// Unbind the current framebuffer manually. Is also executed on `Drop`. + /// + /// TODO: Is it necessary to expose this or is it enough to just + /// unbind the framebuffer on drop? pub fn unbind_framebuffer(&self) { let gl = match self.inner.gl_window().get_api() { glutin::Api::OpenGl => unsafe { @@ -116,6 +126,12 @@ impl ReadOnlyWindow { } } +impl Drop for ReadOnlyWindow { + fn drop(&mut self) { + self.unbind_framebuffer(); + } +} + pub struct WindowInfo { pub window_id: WindowId, pub window: ReadOnlyWindow, From e9c4e9f0a39844fda44cba53d44f13a5e4e6f1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 8 Jul 2018 13:22:42 +0200 Subject: [PATCH 124/868] Refactored window event filtering, debugging timing issues --- src/app.rs | 5 ++- src/window_state.rs | 82 ++++++++++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/app.rs b/src/app.rs index 03d6c5e22..4f82e0109 100644 --- a/src/app.rs +++ b/src/app.rs @@ -498,6 +498,7 @@ fn do_hit_test_and_call_callbacks( let cursor_x = info.cur_cursor_pos.0 as f32; let cursor_y = info.cur_cursor_pos.1 as f32; let point = WorldPoint::new(cursor_x, cursor_y); + let hit_test_results = window.internal.api.hit_test( window.internal.document_id, Some(window.internal.pipeline_id), @@ -510,8 +511,9 @@ fn do_hit_test_and_call_callbacks( if callbacks_filter_list.is_none() { return; } let callbacks_filter_list = callbacks_filter_list.unwrap(); + // NOTE: for some reason hit_test_results is empty... + // ... but only when the mouse is relased - possible timing issue? for item in hit_test_results.items { - let callback_list = ui_state_cache[window_id.id].node_ids_to_callbacks_list.get(&item.tag.0); if callback_list.is_none() { continue; } let callback_list = callback_list.unwrap(); @@ -548,6 +550,7 @@ fn do_hit_test_and_call_callbacks( fn process_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> bool { use glium::glutin::WindowEvent; + match event { Event::WindowEvent { window_id, diff --git a/src/window_state.rs b/src/window_state.rs index 72e519a98..20bb3b06f 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -156,6 +156,7 @@ impl WindowState use glium::glutin::Event::WindowEvent; use glium::glutin::WindowEvent::*; use glium::glutin::{ElementState, MouseButton }; + use glium::glutin::MouseButton::*; let event = if let WindowEvent { event, .. } = event { Some(event) } else { None }; let event = event?; @@ -168,49 +169,52 @@ impl WindowState // TODO: right mouse down / middle mouse down? match event { - - // mouse pressed - MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { - if !self.mouse_state.left_down { - events_vec.push(On::MouseDown); - } - self.mouse_state.left_down = true; - }, - MouseInput { state: ElementState::Pressed, button: MouseButton::Right, .. } => { - if !self.mouse_state.right_down { - events_vec.push(On::MouseDown); + MouseInput { state: ElementState::Pressed, button, .. } => { + match button { + Left => { + if !self.mouse_state.left_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.left_down = true; + }, + Right => { + if !self.mouse_state.right_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.right_down = true; + }, + Middle => { + if !self.mouse_state.middle_down { + events_vec.push(On::MouseDown); + } + self.mouse_state.middle_down = true; + }, + _ => { } } - self.mouse_state.right_down = true; }, - MouseInput { state: ElementState::Pressed, button: MouseButton::Middle, .. } => { - if !self.mouse_state.middle_down { - events_vec.push(On::MouseDown); - } - self.mouse_state.middle_down = true; - }, - - - // mouse released - MouseInput { state: ElementState::Released, button: MouseButton::Left, .. } => { - if self.mouse_state.left_down { - events_vec.push(On::MouseUp); + MouseInput { state: ElementState::Released, button, .. } => { + match button { + Left => { + if self.mouse_state.left_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.left_down = false; + }, + Right => { + if self.mouse_state.right_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.right_down = false; + }, + Middle => { + if self.mouse_state.middle_down { + events_vec.push(On::MouseUp); + } + self.mouse_state.middle_down = false; + }, + _ => { } } - self.mouse_state.left_down = false; }, - MouseInput { state: ElementState::Released, button: MouseButton::Right, .. } => { - if self.mouse_state.right_down { - events_vec.push(On::MouseUp); - } - self.mouse_state.right_down = false; - }, - MouseInput { state: ElementState::Released, button: MouseButton::Middle, .. } => { - if self.mouse_state.middle_down { - events_vec.push(On::MouseUp); - } - self.mouse_state.middle_down = false; - }, - - _ => { // TODO } From 6ba16bc2d1e146e2cea05bddff0be458a27786e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 8 Jul 2018 19:56:41 +0200 Subject: [PATCH 125/868] Fixed order of input handling / redrawing commands Currently I don't know why the order has to be this way: First check for input, then do the drawing. In theory they should be independent from each other, but in another order the display doesn't get swapped correctly (?). I.e. I moved the if frame_event_info.should_hittest { } above the should_swap check. Also migrated to a WindowCloseEvent to not rely on a boolean anymore (more expressive) + use a Vec to indicate no events instead of an Option> - eliminates branching and Vec::new() does not allocate, so in the (likely) case that we have no input events to react to, it'll be a bit faster. --- src/app.rs | 153 ++++++++++++++++++++++---------------------- src/window_state.rs | 12 +--- 2 files changed, 80 insertions(+), 85 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4f82e0109..604a60836 100644 --- a/src/app.rs +++ b/src/app.rs @@ -164,13 +164,24 @@ impl<'a, T: Layout> App<'a, T> { window.events_loop.poll_events(|e| events.push(e)); for event in &events { - let should_close = process_event(event, &mut frame_event_info); - if should_close { + if preprocess_event(event, &mut frame_event_info) == WindowCloseEvent::AboutToClose { closed_windows.push(idx); continue 'window_loop; } } + if frame_event_info.should_hittest { + for event in &events { + do_hit_test_and_call_callbacks( + event, + window, + window_id, + &mut frame_event_info, + &ui_state_cache, + &mut self.app_state); + } + } + if frame_event_info.should_swap_window || frame_event_info.is_resize_event { window.display.swap_buffers()?; if let Some(i) = force_redraw_cache.get_mut(idx) { @@ -199,18 +210,6 @@ impl<'a, T: Layout> App<'a, T> { // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); - if frame_event_info.should_hittest { - for event in &events { - do_hit_test_and_call_callbacks( - event, - window, - window_id, - &mut frame_event_info, - &ui_state_cache, - &mut self.app_state); - } - } - if frame_event_info.should_redraw_window || force_redraw_cache[idx] > 0 { // Call the Layout::layout() fn, get the DOM ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { @@ -482,6 +481,61 @@ impl<'a, T: Layout + Send + 'static> App<'a, T> { } } +#[derive(Debug, Copy, Clone, PartialEq)] +enum WindowCloseEvent { + AboutToClose, + NoCloseEvent, +} + +fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> WindowCloseEvent { + use glium::glutin::WindowEvent; + + match event { + Event::WindowEvent { + window_id, + event + } => { + match event { + WindowEvent::MouseInput { .. } => { + frame_event_info.should_hittest = true; + }, + WindowEvent::CursorMoved { + device_id, + position, + modifiers, + } => { + frame_event_info.should_hittest = true; + frame_event_info.cur_cursor_pos = *position; + + let (_, _, _) = (window_id, device_id, modifiers); + }, + WindowEvent::Resized(w, h) => { + frame_event_info.new_window_size = Some((*w, *h)); + frame_event_info.is_resize_event = true; + frame_event_info.should_redraw_window = true; + }, + WindowEvent::Refresh => { + frame_event_info.should_redraw_window = true; + }, + WindowEvent::HiDPIFactorChanged(dpi) => { + frame_event_info.new_dpi_factor = Some(*dpi); + frame_event_info.should_redraw_window = true; + }, + WindowEvent::Closed => { + return WindowCloseEvent::AboutToClose; + } + _ => { }, + } + }, + Event::Awakened => { + frame_event_info.should_swap_window = true; + }, + _ => { }, + } + + WindowCloseEvent::NoCloseEvent +} + fn do_hit_test_and_call_callbacks( event: &Event, window: &mut Window, @@ -508,16 +562,14 @@ fn do_hit_test_and_call_callbacks( let mut should_update_screen = UpdateScreen::DontRedraw; let callbacks_filter_list = window.state.determine_callbacks(event); - if callbacks_filter_list.is_none() { return; } - let callbacks_filter_list = callbacks_filter_list.unwrap(); // NOTE: for some reason hit_test_results is empty... // ... but only when the mouse is relased - possible timing issue? - for item in hit_test_results.items { - let callback_list = ui_state_cache[window_id.id].node_ids_to_callbacks_list.get(&item.tag.0); - if callback_list.is_none() { continue; } - let callback_list = callback_list.unwrap(); - + for (item, callback_list) in hit_test_results.items.iter().filter_map(|item| + ui_state_cache[window_id.id].node_ids_to_callbacks_list + .get(&item.tag.0) + .and_then(|callback_list| Some((item, callback_list))) + ) { // TODO: currently we don't have information about what DOM node was hit let window_event = WindowEvent { window: window_id.id, @@ -527,12 +579,10 @@ fn do_hit_test_and_call_callbacks( }; // Invoke callback if necessary - for on_filter in &callbacks_filter_list { - if let Some(callback_id) = callback_list.get(on_filter) { - let Callback(callback_func) = ui_state_cache[window_id.id].callback_list[callback_id]; - if (callback_func)(app_state, window_event) == UpdateScreen::Redraw { - should_update_screen = UpdateScreen::Redraw; - } + for callback_id in callbacks_filter_list.iter().filter_map(|on| callback_list.get(on)) { + let Callback(callback_func) = ui_state_cache[window_id.id].callback_list[callback_id]; + if (callback_func)(app_state, window_event) == UpdateScreen::Redraw { + should_update_screen = UpdateScreen::Redraw; } } } @@ -548,55 +598,6 @@ fn do_hit_test_and_call_callbacks( } } -fn process_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> bool { - use glium::glutin::WindowEvent; - - match event { - Event::WindowEvent { - window_id, - event - } => { - match event { - WindowEvent::MouseInput { .. } => { - frame_event_info.should_hittest = true; - }, - WindowEvent::CursorMoved { - device_id, - position, - modifiers, - } => { - frame_event_info.should_hittest = true; - frame_event_info.cur_cursor_pos = *position; - - let (_, _, _) = (window_id, device_id, modifiers); - }, - WindowEvent::Resized(w, h) => { - frame_event_info.new_window_size = Some((*w, *h)); - frame_event_info.is_resize_event = true; - frame_event_info.should_redraw_window = true; - }, - WindowEvent::Refresh => { - frame_event_info.should_redraw_window = true; - }, - WindowEvent::HiDPIFactorChanged(dpi) => { - frame_event_info.new_dpi_factor = Some(*dpi); - frame_event_info.should_redraw_window = true; - }, - WindowEvent::Closed => { - return true; - } - _ => { }, - } - }, - Event::Awakened => { - frame_event_info.should_swap_window = true; - }, - _ => { }, - } - - false -} - fn render( window: &mut Window, _window_id: &WindowId, diff --git a/src/window_state.rs b/src/window_state.rs index 20bb3b06f..58e758b90 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -151,15 +151,14 @@ impl WindowState // // This function also updates / mutates the current window state, // so that we are ready for the next frame - pub(crate) fn determine_callbacks(&mut self, event: &Event) -> Option> { + pub(crate) fn determine_callbacks(&mut self, event: &Event) -> Vec { use glium::glutin::Event::WindowEvent; use glium::glutin::WindowEvent::*; use glium::glutin::{ElementState, MouseButton }; use glium::glutin::MouseButton::*; - let event = if let WindowEvent { event, .. } = event { Some(event) } else { None }; - let event = event?; + let event = if let WindowEvent { event, .. } = event { event } else { return Vec::new(); }; // store the current window state so we can set it in this.previous_window_state later on let mut previous_state = Box::new(self.clone()); @@ -221,12 +220,7 @@ impl WindowState } self.previous_window_state = Some(previous_state); - - if events_vec.is_empty() { - None - } else { - Some(events_vec) - } + events_vec } } From 5c6a8c0b79c1c769a4b5951cb2965f26aa1634cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 8 Jul 2018 21:07:14 +0200 Subject: [PATCH 126/868] Added scroll state to window state TODO: Copy the scroll state from the Window to the scroll state of the FakeWindow --- examples/debug.rs | 10 +++++++++- src/app.rs | 9 +++++++-- src/app_state.rs | 8 ++++---- src/dom.rs | 28 ++++++++++++++++++++++++---- src/window.rs | 24 ++++++++++++++++++++---- src/window_state.rs | 31 +++++++++++++++++++++++++------ 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 12408e6da..1a46075cb 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -24,11 +24,19 @@ impl Layout for MyAppData { } else { Dom::new(NodeType::Div) .with_class("__azul-native-button") - .with_callback(On::MouseUp, Callback(my_button_click_handler)) + .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) + .with_callback(On::Scroll, Callback(on_scroll)) } } } +fn on_scroll(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + let mouse_state = app_state.windows[event.window].get_mouse_state(); + // TODO: copy mouse state of window to mouse state of FakeWindow + println!("scroll!: x: {}, y: {}", mouse_state.scroll_x, mouse_state.scroll_y); + UpdateScreen::DontRedraw +} + fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { open_file_dialog(None, None) .and_then(|path| fs::read_to_string(path.clone()).ok()) diff --git a/src/app.rs b/src/app.rs index 604a60836..d207a80ad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -209,6 +209,8 @@ impl<'a, T: Layout> App<'a, T> { window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user window.update_from_user_window_state(self.app_state.windows[idx].state.clone()); + // Reset the scroll amount to 0 (for the next frame) + window.clear_scroll_state(); if frame_event_info.should_redraw_window || force_redraw_cache[idx] > 0 { // Call the Layout::layout() fn, get the DOM @@ -433,14 +435,14 @@ impl<'a, T: Layout> App<'a, T> { } /// Get the contents of the system clipboard as a string - pub(crate) fn get_clipboard_string(&mut self) + pub fn get_clipboard_string(&mut self) -> Result { self.app_state.get_clipboard_string() } /// Set the contents of the system clipboard as a string - pub(crate) fn set_clipboard_string(&mut self, contents: String) + pub fn set_clipboard_string(&mut self, contents: String) -> Result<(), ClipboardError> { self.app_state.set_clipboard_string(contents) @@ -521,6 +523,9 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> Win frame_event_info.new_dpi_factor = Some(*dpi); frame_event_info.should_redraw_window = true; }, + WindowEvent::MouseWheel { .. } => { + frame_event_info.should_hittest = true; + } WindowEvent::Closed => { return WindowCloseEvent::AboutToClose; } diff --git a/src/app_state.rs b/src/app_state.rs index 1aa52643c..c98567062 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -237,15 +237,15 @@ impl<'a, T: Layout> AppState<'a, T> { } /// Get the contents of the system clipboard as a string - pub(crate) fn get_clipboard_string(&mut self) - -> Result + pub fn get_clipboard_string(&mut self) + -> Result { self.resources.get_clipboard_string() } /// Set the contents of the system clipboard as a string - pub(crate) fn set_clipboard_string(&mut self, contents: String) - -> Result<(), ClipboardError> + pub fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> { self.resources.set_clipboard_string(contents) } diff --git a/src/dom.rs b/src/dom.rs index 3082a3d18..708fe907f 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -107,7 +107,7 @@ impl NodeType { } /// OpenGL texture, use `ReadOnlyWindow::create_texture` to create a texture -/// +/// /// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` /// when you are done with your OpenGL drawing, otherwise webrender will render /// to the texture, not the window, so your texture will actually never show up. @@ -126,11 +126,11 @@ impl Texture { /// Prepares the texture for drawing - you can only draw /// on a framebuffer, the texture itself is readonly from the - /// OpenGL drivers point of view. - /// + /// OpenGL drivers point of view. + /// /// **WARNING**: Don't forget to call `ReadOnlyWindow::unbind_framebuffer()` /// when you are done with your OpenGL drawing, otherwise webrender will render - /// to the texture instead of the window, so your texture will actually + /// to the texture instead of the window, so your texture will actually /// never show up on the screen, since it is never rendered. /// If you use a `Texture` and you get a blank screen, this is probably why. pub fn as_surface<'a>(&'a self) -> SimpleFrameBuffer<'a> { @@ -164,12 +164,32 @@ pub enum On { /// Mouse cursor has is over element and is pressed /// (not good for "click" events - use `MouseUp` instead) MouseDown, + /// (Specialization of `MouseDown`). Fires only if the left mouse button + /// has been pressed while cursor was over the element + LeftMouseDown, + /// (Specialization of `MouseDown`). Fires only if the middle mouse button + /// has been pressed while cursor was over the element + MiddleMouseDown, + /// (Specialization of `MouseDown`). Fires only if the right mouse button + /// has been pressed while cursor was over the element + RightMouseDown, /// Mouse button has been released while cursor was over the element MouseUp, + /// (Specialization of `MouseUp`). Fires only if the left mouse button has + /// been released while cursor was over the element + LeftMouseUp, + /// (Specialization of `MouseUp`). Fires only if the middle mouse button has + /// been released while cursor was over the element + MiddleMouseUp, + /// (Specialization of `MouseUp`). Fires only if the right mouse button has + /// been released while cursor was over the element + RightMouseUp, /// Mouse cursor has entered the element MouseEnter, /// Mouse cursor has left the element MouseLeave, + /// Mousewheel / touchpad scrolling + Scroll, } #[derive(PartialEq, Eq)] diff --git a/src/window.rs b/src/window.rs index 5522fa1a3..59e68b3e2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,8 @@ //! Window creation module use std::{ - time::Duration, - fmt, + time::Duration, + fmt, rc::Rc }; use webrender::{ @@ -27,7 +27,7 @@ use cassowary::{ use { dom::Texture, css::{Css, FakeCss}, - window_state::{WindowState, WindowPosition}, + window_state::{WindowState, MouseState, KeyboardState, WindowPosition}, display_list::SolvedLayout, traits::Layout, cache::{EditVariableCache, DomTreeCache}, @@ -68,6 +68,16 @@ impl FakeWindow { inner: self.read_only_window.clone() } } + + /// Returns a copy of the current windows mouse state. We don't want the library + /// user to be able to modify this state, only to read it + pub fn get_mouse_state(&self) -> MouseState { + self.state.mouse_state.clone() + } + + pub fn get_keyboard_state(&self) -> KeyboardState { + self.state.keyboard_state.clone() + } } /// Read-only window which can be used to create / draw @@ -105,7 +115,7 @@ impl ReadOnlyWindow { } } - /// Unbind the current framebuffer manually. Is also executed on `Drop`. + /// Unbind the current framebuffer manually. Is also executed on `Drop`. /// /// TODO: Is it necessary to expose this or is it enough to just /// unbind the framebuffer on drop? @@ -786,6 +796,12 @@ impl Window { frame_event_info.should_redraw_window = true; } } + + /// Resets the mouse states `scroll_x` and `scroll_y` to 0 + pub(crate) fn clear_scroll_state(&mut self) { + self.state.mouse_state.scroll_x = 0.0; + self.state.mouse_state.scroll_y = 0.0; + } } impl Drop for Window { diff --git a/src/window_state.rs b/src/window_state.rs index 58e758b90..817934a1c 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -34,12 +34,16 @@ pub struct MouseState pub mouse_cursor_type: MouseCursor, //// Where is the mouse cursor? Set to `None` if the window is not focused pub mouse_cursor: Option<(i32, i32)>, - //// Is the left MB down? + //// Is the left mouse button down? pub left_down: bool, - //// Is the right MB down? + //// Is the right mouse button down? pub right_down: bool, - //// Is the middle MB down? + //// Is the middle mouse button down? pub middle_down: bool, + /// Scroll amount in pixels in the horizontal direction. Gets reset to 0 after every frame + pub scroll_x: f32, + /// Scroll amount in pixels in the vertical direction. Gets reset to 0 after every frame + pub scroll_y: f32, } impl Default for MouseState { @@ -51,6 +55,8 @@ impl Default for MouseState { left_down: false, right_down: false, middle_down: false, + scroll_x: 0.0, + scroll_y: 0.0, } } } @@ -173,18 +179,21 @@ impl WindowState Left => { if !self.mouse_state.left_down { events_vec.push(On::MouseDown); + events_vec.push(On::LeftMouseDown); } self.mouse_state.left_down = true; }, Right => { if !self.mouse_state.right_down { events_vec.push(On::MouseDown); + events_vec.push(On::RightMouseDown); } self.mouse_state.right_down = true; }, Middle => { if !self.mouse_state.middle_down { events_vec.push(On::MouseDown); + events_vec.push(On::MiddleMouseDown); } self.mouse_state.middle_down = true; }, @@ -196,27 +205,37 @@ impl WindowState Left => { if self.mouse_state.left_down { events_vec.push(On::MouseUp); + events_vec.push(On::LeftMouseUp); } self.mouse_state.left_down = false; }, Right => { if self.mouse_state.right_down { events_vec.push(On::MouseUp); + events_vec.push(On::RightMouseUp); } self.mouse_state.right_down = false; }, Middle => { if self.mouse_state.middle_down { events_vec.push(On::MouseUp); + events_vec.push(On::MiddleMouseUp); } self.mouse_state.middle_down = false; }, _ => { } } }, - _ => { - // TODO - } + MouseWheel { delta, .. } => { + let (scroll_x_px, scroll_y_px) = match delta { + MouseScrollDelta::PixelDelta(x, y) => (*x, *y), + MouseScrollDelta::LineDelta(x, y) => (x * 100.0, y * 100.0), + }; + self.mouse_state.scroll_x = scroll_x_px; + self.mouse_state.scroll_y = scroll_y_px; + events_vec.push(On::Scroll); + }, + _ => { } } self.previous_window_state = Some(previous_state); From d50ba42ffbae22abe7f658614c57f17344a423aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 8 Jul 2018 22:49:52 +0200 Subject: [PATCH 127/868] Implemented panning for SVG widget --- examples/debug.rs | 45 ++++++++++++++++++++++++-------- src/app.rs | 28 ++++++++++---------- src/widgets.rs | 63 ++++++++++++++++++++++++++++++++------------- src/window.rs | 19 +++++++++++--- src/window_state.rs | 29 ++++++++++++++++++--- 5 files changed, 133 insertions(+), 51 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 1a46075cb..a00f1ed3d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -12,29 +12,46 @@ const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); #[derive(Debug)] pub struct MyAppData { - pub svg: Option<(SvgCache, Vec)>, + pub map: Option, +} + +#[derive(Debug)] +pub struct Map { + pub cache: SvgCache, + pub layers: Vec, + pub zoom: f32, + pub pan_horz: f32, + pub pan_vert: f32, } impl Layout for MyAppData { fn layout(&self, info: WindowInfo) -> Dom { - if let Some((svg_cache, svg_layers)) = &self.svg { - Svg::with_layers(svg_layers).dom(&info.window, &svg_cache) + if let Some(map) = &self.map { + Svg::with_layers(map.layers.clone()) + .with_pan(map.pan_horz, map.pan_vert) + .with_zoom(map.zoom) + .dom(&info.window, &map.cache) + .with_callback(On::Scroll, Callback(scroll_map_contents)) } else { Dom::new(NodeType::Div) .with_class("__azul-native-button") .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) - .with_callback(On::Scroll, Callback(on_scroll)) } } } -fn on_scroll(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { - let mouse_state = app_state.windows[event.window].get_mouse_state(); - // TODO: copy mouse state of window to mouse state of FakeWindow - println!("scroll!: x: {}, y: {}", mouse_state.scroll_x, mouse_state.scroll_y); - UpdateScreen::DontRedraw +fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + app_state.data.modify(|data| { + if let Some(map) = data.map.as_mut() { + let mouse_state = app_state.windows[event.window].get_mouse_state(); + map.pan_horz += mouse_state.scroll_x; + map.pan_vert += mouse_state.scroll_y; + } + }); + + UpdateScreen::Redraw } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { @@ -43,7 +60,13 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv .and_then(|contents| { let mut svg_cache = SvgCache::empty(); let svg_layers = svg_cache.add_svg(&contents).ok()?; - app_state.data.modify(|data| data.svg = Some((svg_cache, svg_layers))); + app_state.data.modify(|data| data.map = Some(Map { + cache: svg_cache, + layers: svg_layers, + zoom: 1.0, + pan_horz: 0.0, + pan_vert: 0.0, + })); Some(UpdateScreen::Redraw) }) .unwrap_or_else(|| { @@ -55,7 +78,7 @@ fn main() { // Parse and validate the CSS let css = Css::new_from_string(TEST_CSS).unwrap(); - let mut app = App::new(MyAppData { svg: None }); + let mut app = App::new(MyAppData { map: None }); app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); diff --git a/src/app.rs b/src/app.rs index d207a80ad..5bcf173c7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -168,6 +168,7 @@ impl<'a, T: Layout> App<'a, T> { closed_windows.push(idx); continue 'window_loop; } + window.state.update_mouse_cursor_position(event); } if frame_event_info.should_hittest { @@ -493,23 +494,14 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> Win use glium::glutin::WindowEvent; match event { - Event::WindowEvent { - window_id, - event - } => { + Event::WindowEvent { event, .. } => { match event { WindowEvent::MouseInput { .. } => { frame_event_info.should_hittest = true; }, - WindowEvent::CursorMoved { - device_id, - position, - modifiers, - } => { + WindowEvent::CursorMoved { position, .. } => { frame_event_info.should_hittest = true; frame_event_info.cur_cursor_pos = *position; - - let (_, _, _) = (window_id, device_id, modifiers); }, WindowEvent::Resized(w, h) => { frame_event_info.new_window_size = Some((*w, *h)); @@ -525,10 +517,10 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> Win }, WindowEvent::MouseWheel { .. } => { frame_event_info.should_hittest = true; - } + }, WindowEvent::Closed => { return WindowCloseEvent::AboutToClose; - } + }, _ => { }, } }, @@ -553,9 +545,9 @@ fn do_hit_test_and_call_callbacks( use webrender::api::WorldPoint; use window::WindowEvent; use dom::Callback; + use window_state::{KeyboardState, MouseState}; - let cursor_x = info.cur_cursor_pos.0 as f32; - let cursor_y = info.cur_cursor_pos.1 as f32; + let (cursor_x, cursor_y) = window.state.mouse_state.cursor_pos.and_then(|(x, y)| Some((x as f32, y as f32))).unwrap_or((0.0, 0.0)); let point = WorldPoint::new(cursor_x, cursor_y); let hit_test_results = window.internal.api.hit_test( @@ -567,6 +559,9 @@ fn do_hit_test_and_call_callbacks( let mut should_update_screen = UpdateScreen::DontRedraw; let callbacks_filter_list = window.state.determine_callbacks(event); + // TODO: this should be refactored - currently very stateful and error-prone! + app_state.windows[window_id.id].set_keyboard_state(&window.state.keyboard_state); + app_state.windows[window_id.id].set_mouse_state(&window.state.mouse_state); // NOTE: for some reason hit_test_results is empty... // ... but only when the mouse is relased - possible timing issue? @@ -592,6 +587,9 @@ fn do_hit_test_and_call_callbacks( } } + app_state.windows[window_id.id].set_keyboard_state(&KeyboardState::default()); + app_state.windows[window_id.id].set_mouse_state(&MouseState::default()); + if should_update_screen == UpdateScreen::Redraw { info.should_redraw_window = true; // TODO: THIS IS PROBABLY THE WRONG PLACE TO DO THIS!!! diff --git a/src/widgets.rs b/src/widgets.rs index 5958a12fa..73f426d72 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -21,7 +21,7 @@ pub enum ButtonContent { } impl Button { - pub fn with_label(text: S) + pub fn with_label(text: S) -> Self where S: Into { Self { @@ -29,15 +29,15 @@ impl Button { } } - pub fn with_image(image: ImageId) - -> Self + pub fn with_image(image: ImageId) + -> Self { Self { content: ButtonContent::Image(image), } } - pub fn dom(self) + pub fn dom(self) -> Dom where T: Layout { use self::ButtonContent::*; @@ -52,12 +52,29 @@ impl Button { // --- svg -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct Svg { + /// Currently active layers pub layers: Vec, + /// Pan (horizontal, vertical) in pixels + pub pan: (f32, f32), + /// 1.0 = default zoom + pub zoom: f32, + /// Whether an FXAA shader should be applied to the resulting OpenGL texture pub enable_fxaa: bool, } +impl Default for Svg { + fn default() -> Self { + Self { + layers: Vec::new(), + pan: (0.0, 0.0), + zoom: 1.0, + enable_fxaa: false, + } + } +} + use glium::{Texture2d, draw_parameters::DrawParameters, index::PrimitiveType, IndexBuffer, Surface}; use std::sync::Mutex; @@ -67,31 +84,39 @@ use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; impl Svg { - // todo: remove this later #[inline] - pub fn empty() - -> Self + pub fn with_layers(layers: Vec) + -> Self { - Self { layers: Vec::new(), enable_fxaa: true } + Self { layers: layers, .. Default::default() } } #[inline] - pub fn with_layers(layers: &Vec) - -> Self + pub fn with_pan(mut self, horz: f32, vert: f32) + -> Self { - Self { layers: layers.clone(), enable_fxaa: true } + self.pan = (horz, vert); + self } #[inline] - pub fn with_fxaa(mut self, enable_fxaa: bool) - -> Self + pub fn with_zoom(mut self, zoom: f32) + -> Self + { + self.zoom = zoom; + self + } + + #[inline] + pub fn with_fxaa(mut self, enable_fxaa: bool) + -> Self { self.enable_fxaa = enable_fxaa; self } /// Renders the SVG to an OpenGL texture and creates the DOM - pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) + pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom where T: Layout { const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; @@ -108,12 +133,14 @@ impl Svg { let z_index: f32 = 0.5; let bbox: TypedRect = TypedRect { - origin: TypedPoint2D::new(0.0, 0.0), + origin: TypedPoint2D::new(0.0, 0.0), size: TypedSize2D::new(800.0, 600.0), }; let shader = svg_cache.init_shader(window); let offset = (400.0_f32, 200.0_f32); + println!("pan: {:?}, zoom: {:?}", self.pan, self.zoom); + { let mut surface = tex.as_surface(); @@ -184,13 +211,13 @@ pub struct Label { } impl Label { - pub fn new(text: S) + pub fn new(text: S) -> Self where S: Into { Self { text: text.into() } } - pub fn dom(self) + pub fn dom(self) -> Dom where T: Layout { Dom::new(NodeType::Label(self.text)) diff --git a/src/window.rs b/src/window.rs index 59e68b3e2..3cd7fa3f9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -69,15 +69,26 @@ impl FakeWindow { } } - /// Returns a copy of the current windows mouse state. We don't want the library - /// user to be able to modify this state, only to read it - pub fn get_mouse_state(&self) -> MouseState { - self.state.mouse_state.clone() + pub(crate) fn set_keyboard_state(&mut self, kb: &KeyboardState) { + self.state.keyboard_state = kb.clone(); } + pub(crate) fn set_mouse_state(&mut self, mouse: &MouseState) { + self.state.mouse_state = *mouse; + } + + /// Returns a copy of the current keyboard keyboard state. We don't want the library + /// user to be able to modify this state, only to read it. pub fn get_keyboard_state(&self) -> KeyboardState { self.state.keyboard_state.clone() } + + /// Returns a copy of the current windows mouse state. We don't want the library + /// user to be able to modify this state, only to read it + pub fn get_mouse_state(&self) -> MouseState { + self.state.mouse_state + } + } /// Read-only window which can be used to create / draw diff --git a/src/window_state.rs b/src/window_state.rs index 817934a1c..080f43856 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -32,8 +32,8 @@ pub struct MouseState { /// Current mouse cursor type pub mouse_cursor_type: MouseCursor, - //// Where is the mouse cursor? Set to `None` if the window is not focused - pub mouse_cursor: Option<(i32, i32)>, + //// Where is the mouse cursor currently? Set to `None` if the window is not focused + pub cursor_pos: Option<(f64, f64)>, //// Is the left mouse button down? pub left_down: bool, //// Is the right mouse button down? @@ -51,7 +51,7 @@ impl Default for MouseState { fn default() -> Self { Self { mouse_cursor_type: MouseCursor::Default, - mouse_cursor: Some((0, 0)), + cursor_pos: Some((0.0, 0.0)), left_down: false, right_down: false, middle_down: false, @@ -241,6 +241,29 @@ impl WindowState self.previous_window_state = Some(previous_state); events_vec } + + /// After the initial events are filtered, this will update the mouse + /// cursor position, if the event is a `CursorMoved` and set it to `None` + /// if the cursor has left the window + pub(crate) fn update_mouse_cursor_position(&mut self, event: &Event) { + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CursorMoved { position, .. } => { + self.mouse_state.cursor_pos = Some(*position); + }, + WindowEvent::CursorLeft { .. } => { + self.mouse_state.cursor_pos = None; + }, + WindowEvent::CursorEntered { .. } => { + self.mouse_state.cursor_pos = Some((0.0, 0.0)) + }, + _ => { } + } + }, + _ => { }, + } + } } fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { From 171c0921433fd669990c32304c55d772847b5fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 10 Jul 2018 15:35:27 +0200 Subject: [PATCH 128/868] Fix OpenGL crash due to missing texture, activate GL_DEBUG in debug mode --- src/display_list.rs | 4 ++-- src/widgets.rs | 11 ++--------- src/window.rs | 12 ++++++------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 5fa14c204..aafe128bd 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -442,6 +442,8 @@ fn displaylist_handle_rect( image_type: ExternalImageType::TextureHandle(TextureTarget::Default), }); + ACTIVE_GL_TEXTURES.lock().unwrap().insert(external_image_id, ActiveTexture { texture: texture.clone() }); + resource_updates.push(ResourceUpdate::AddImage( AddImage { key, descriptor, data, tiling: None } )); @@ -453,8 +455,6 @@ fn displaylist_handle_rect( ImageRendering::Auto, AlphaType::Alpha, key); - - ACTIVE_GL_TEXTURES.lock().unwrap().insert(external_image_id, ActiveTexture { texture: texture.clone() }); }, } diff --git a/src/widgets.rs b/src/widgets.rs index 73f426d72..165437e9d 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -121,8 +121,6 @@ impl Svg { { const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; - window.make_current(); - let tex = window.create_texture(800, 600); tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); @@ -137,9 +135,6 @@ impl Svg { size: TypedSize2D::new(800.0, 600.0), }; let shader = svg_cache.init_shader(window); - let offset = (400.0_f32, 200.0_f32); - - println!("pan: {:?}, zoom: {:?}", self.pan, self.zoom); { let mut surface = tex.as_surface(); @@ -164,7 +159,7 @@ impl Svg { color.color.blue as f32, color.alpha as f32 ), - offset: (offset.0 as f32, offset.1 as f32) + offset: (self.pan.0, self.pan.1) }; surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); @@ -185,7 +180,7 @@ impl Svg { stroke_color.color.blue as f32, stroke_color.alpha as f32 ), - offset: (offset.0 as f32, offset.1 as f32) + offset: (self.pan.0, self.pan.1) }; surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); @@ -197,8 +192,6 @@ impl Svg { // TODO: apply FXAA shader } - window.unbind_framebuffer(); - Dom::new(NodeType::GlTexture(tex)) } } diff --git a/src/window.rs b/src/window.rs index 3cd7fa3f9..0fca0fc58 100644 --- a/src/window.rs +++ b/src/window.rs @@ -581,12 +581,12 @@ impl Window { opengles_version: (3, 0), }) .with_gl_profile(GlProfile::Core); - /* + #[cfg(debug_assertions)] { builder = builder.with_gl_debug_flag(true); } - */ - /*#[cfg(not(debug_assertions))] */ { + + #[cfg(not(debug_assertions))] { builder = builder.with_gl_debug_flag(false); } @@ -609,9 +609,9 @@ impl Window { gl_window.window().set_position(x as i32, y as i32); } - // #[cfg(debug_assertions)] - // let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; - // #[cfg(not(debug_assertions))] + #[cfg(debug_assertions)] + let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; + #[cfg(not(debug_assertions))] let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; let device_pixel_ratio = display.gl_window().hidpi_factor(); From b578db3e52dfeaaeeb6bc49130fc6b53c686b500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 10 Jul 2018 16:18:09 +0200 Subject: [PATCH 129/868] Prefix shaders based on the target capabilities See #5 - should fix crash in SVG shader compilation on macOS. The constants (#version 150 and #version 300 es) are taken from webrender - they are the minimum OpenGL version that webrender supports, so it doesn't make any sense to go lower than that. --- src/svg.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/svg.rs b/src/svg.rs index d0644e7df..913978831 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -9,7 +9,7 @@ use std::{ use glium::{ backend::Facade, DrawParameters, IndexBuffer, VertexBuffer, Display, - Texture2d, Program, + Texture2d, Program, Api, }; use lyon::{ tessellation::{ @@ -56,8 +56,10 @@ pub fn new_view_box_id() -> SvgViewBoxId { SvgViewBoxId(SVG_VIEW_BOX_ID.fetch_add(1, Ordering::SeqCst)) } +const SHADER_VERSION_GL: &str = "#version 150"; +const SHADER_VERSION_GLES: &str = "#version 300 es"; + const SVG_VERTEX_SHADER: &str = " - #version 130 in vec2 xy; in vec2 normal; @@ -71,10 +73,16 @@ const SVG_VERTEX_SHADER: &str = " gl_Position = vec4(vec2(-1.0) + ((xy - bbox_origin) / bbox_size) + (offset / bbox_size), z_index, 1.0); }"; +fn prefix_gl_version(shader: &str, gl: Api) -> String { + match gl { + Api::Gl => format!("{}\n{}", SHADER_VERSION_GL, shader), + Api::GlEs => format!("{}\n{}", SHADER_VERSION_GLES, shader), + } +} + const SVG_FRAGMENT_SHADER: &str = " - #version 130 - uniform vec4 color; + uniform vec4 color; out vec4 out_color; void main() { @@ -89,7 +97,7 @@ const SVG_FRAGMENT_SHADER: &str = " // - `uv` // - `source` const SVG_FXAA_VERTEX_SHADER: &str = " - #version 130 + precision mediump float; out vec2 v_rgbNW; @@ -166,7 +174,6 @@ const SVG_FXAA_VERTEX_SHADER: &str = " // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. const SVG_FXAA_FRAG_SHADER: &str = " - #version 130 #define FXAA_REDUCE_MIN (1.0/ 128.0) #define FXAA_REDUCE_MUL (1.0 / 8.0) @@ -246,8 +253,12 @@ pub struct SvgShader { impl SvgShader { pub fn new(display: &F) -> Self { + let current_gl_api = display.get_context().get_opengl_version().0; + let vertex_source_prefixed = prefix_gl_version(SVG_VERTEX_SHADER, current_gl_api); + let fragment_source_prefixed = prefix_gl_version(SVG_FRAGMENT_SHADER, current_gl_api); + Self { - program: Rc::new(Program::from_source(display, SVG_VERTEX_SHADER, SVG_FRAGMENT_SHADER, None).unwrap()), + program: Rc::new(Program::from_source(display, &vertex_source_prefixed, &fragment_source_prefixed, None).unwrap()), } } } From 543dbc6a29f80d04989034e2ff808a53b5452188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 10 Jul 2018 16:32:12 +0200 Subject: [PATCH 130/868] Fix wrong sleep times (see #6) --- src/app.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5bcf173c7..7f61f7613 100644 --- a/src/app.rs +++ b/src/app.rs @@ -245,10 +245,10 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.clean_up_finished_tasks(); // Wait until 16ms have passed - let time_end = Instant::now(); - let diff = time_end - time_start; - if diff < Duration::from_millis(16) { - thread::sleep(diff); + let diff = time_start.elapsed(); + const FRAME_TIME: Duration = Duration::from_millis(16); + if diff < FRAME_TIME { + thread::sleep(FRAME_TIME - diff); } } From 5066298ba74b3a7bacc0c5ed7e1d97decbad6df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 10 Jul 2018 17:39:33 +0200 Subject: [PATCH 131/868] Fix webrender to not mess with the active shader Some external toolkits (such as glium) can't see that webrender has changed the shader under their feet. glium, for example, assumes that the active shader program hasn't changed, but in reality, webrender sets the active shader to 0 after a draw call, so glium::use_program() doesn't do anything because glium thinks that the original shader is still active. Generally, to be a good citizen in OpenGL-land, it's not a good idea to leave and state changes behind after one frame - the webrenders rendering should be invisible to any other toolkits, otherwise workarounds around this no-cleanup "feature" would be required. Currently, this depends on a fork of webrender, should be merged into webrender main in the next few days. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 089c64f86..cc19c9887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,8 @@ git = "https://github.com/RazrFalcon/resvg.git" rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] -git = "https://github.com/servo/webrender" -rev = "686eca6e7ce3501b3bd5349d33684aaead73857d" +git = "https://github.com/fschutt/webrender" +branch = "fix_webrender_messing_with_active_shader" [features] # The reason we do this is because doctests don't get cfg(test) From 2a9e4ce4866d3473df7ffc72cd58ec1056bb45c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 11 Jul 2018 15:43:48 +0200 Subject: [PATCH 132/868] Fix webrenders not cleaning up the active shader See https://github.com/servo/webrender/pull/2880 --- Cargo.toml | 4 ++-- src/app.rs | 20 ++++++++++++++++++-- src/window.rs | 27 +++++++++++++++------------ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cc19c9887..389e44ce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,8 @@ git = "https://github.com/RazrFalcon/resvg.git" rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] -git = "https://github.com/fschutt/webrender" -branch = "fix_webrender_messing_with_active_shader" +git = "https://github.com/servo/webrender" +rev = "a30fd2286f424e528e3bde502d1a11ed5ef7ec31" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/src/app.rs b/src/app.rs index 7f61f7613..1fa710b3b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,9 +4,9 @@ use std::{ sync::{Arc, Mutex, PoisonError}, }; use glium::{SwapBuffersError, glutin::Event}; -use webrender::api::{RenderApi, HitTestFlags}; +use webrender::api::{RenderApi, HitTestFlags, DevicePixel}; use image::ImageError; -use euclid::TypedScale; +use euclid::{TypedScale, TypedSize2D}; use { images::ImageType, errors::{FontError, ClipboardError}, @@ -645,7 +645,23 @@ fn render( window.internal.api.send_transaction(window.internal.document_id, txn); window.renderer.as_mut().unwrap().update(); + + render_inner(window, framebuffer_size); +} + +// See: https://github.com/servo/webrender/pull/2880 +// webrender doesn't reset the active shader back to what it was, but rather sets it +// to zero, which glium doesn't know about, so on the next frame it tries to draw with shader 0 +fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D) { + + use glium::backend::Facade; + use gleam::gl; + use window::get_gl_context; + + let mut current_program = [0_i32]; + unsafe { get_gl_context(&window.display).unwrap().get_integer_v(gl::CURRENT_PROGRAM, &mut current_program) }; window.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); + get_gl_context(&window.display).unwrap().use_program(current_program[0] as u32); } // Empty test, for some reason codecov doesn't detect any files (and therefore diff --git a/src/window.rs b/src/window.rs index 0fca0fc58..f86bd0e93 100644 --- a/src/window.rs +++ b/src/window.rs @@ -17,7 +17,7 @@ use glium::{ MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, backend::{Context, Facade, glutin::DisplayCreationError}, }; -use gleam::gl; +use gleam::gl::{self, Gl}; use euclid::TypedScale; use cassowary::{ Variable, Solver, @@ -645,19 +645,10 @@ impl Window { let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); DeviceUintSize::new(width, height) }; + let notifier = Box::new(Notifier::new(events_loop.create_proxy())); - let gl = match display.gl_window().get_api() { - glutin::Api::OpenGl => unsafe { - gl::GlFns::load_with(|symbol| - display.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::OpenGlEs => unsafe { - gl::GlesFns::load_with(|symbol| - display.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::WebGl => return Err(WindowCreateError::WebGlNotSupported), - }; + let gl = get_gl_context(&display)?; let opts_native = get_renderer_opts(true, device_pixel_ratio, Some(options.background)); let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); @@ -815,6 +806,18 @@ impl Window { } } +pub(crate) fn get_gl_context(display: &Display) -> Result, WindowCreateError> { + match display.gl_window().get_api() { + glutin::Api::OpenGl => Ok(unsafe { + gl::GlFns::load_with(|symbol| display.gl_window().get_proc_address(symbol) as *const _) + }), + glutin::Api::OpenGlEs => Ok(unsafe { + gl::GlesFns::load_with(|symbol| display.gl_window().get_proc_address(symbol) as *const _) + }), + glutin::Api::WebGl => Err(WindowCreateError::WebGlNotSupported), + } +} + impl Drop for Window { fn drop(&mut self) { // self.background_thread.take().unwrap().join(); From 4662e13abfe76ddb225fa76cf19a3e439495f286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 11 Jul 2018 17:34:35 +0200 Subject: [PATCH 133/868] Send updated display size to webrender on app startup --- src/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.rs b/src/app.rs index 1fa710b3b..f31c83c8b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -294,6 +294,7 @@ impl<'a, T: Layout> App<'a, T> { for (idx, window) in windows.iter_mut().enumerate() { ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); + Self::update_display(&window); render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &mut app_state.resources, true); } From 6464915503412bd29986727581d52f02ced27263 Mon Sep 17 00:00:00 2001 From: whmountains <8680147+whmountains@users.noreply.github.com> Date: Wed, 11 Jul 2018 10:49:21 -0500 Subject: [PATCH 134/868] Handle initial draw inside main window loop. Removes the need for a separate `do_first_redraw` function. Fixes #5 --- src/app.rs | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1fa710b3b..8964f1400 100644 --- a/src/app.rs +++ b/src/app.rs @@ -141,9 +141,8 @@ impl<'a, T: Layout> App<'a, T> { use window::{ReadOnlyWindow, WindowInfo}; let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); - let mut ui_description_cache = Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); - - let mut force_redraw_cache = vec![0_usize; self.windows.len()]; + let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; + let mut force_redraw_cache = vec![1_usize; self.windows.len()]; while !self.windows.is_empty() { @@ -283,23 +282,6 @@ impl<'a, T: Layout> App<'a, T> { ).collect() } - /// First repaint, otherwise the window would be black on startup - fn do_first_redraw( - windows: &mut [Window], - app_state: &mut AppState<'a, T>, - ui_state_cache: &[UiState]) - -> Vec> - { - let mut ui_description_cache = vec![UiDescription::default(); windows.len()]; - - for (idx, window) in windows.iter_mut().enumerate() { - ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); - render(window, &WindowId { id: idx, }, &ui_description_cache[idx], &mut app_state.resources, true); - } - - ui_description_cache - } - /// Add an image to the internal resources /// /// ## Returns @@ -670,4 +652,4 @@ fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D #[test] fn __codecov_test_app_file() { -} \ No newline at end of file +} From 9fed1e626f9166cf3f30c8d570c57e1595a8897b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 11 Jul 2018 18:33:31 +0200 Subject: [PATCH 135/868] Fix unit tests TODO: Find a better way to update the resources than redrawing the window. --- src/app.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8964f1400..124f6dc7b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -453,8 +453,11 @@ impl<'a, T: Layout> App<'a, T> { .. Default::default() }; self.create_window(hidden_create_options, Css::native()).unwrap(); - let ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); - Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); + // TODO: do_first_redraw shouldn't exist, need to find a better way to update the resources + // This will make App::delete_font doc-test fail if run without `no-opengl-tests`. + // + // let ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); + // Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); } } From e47f5918e7b702788607678c8c4670cde2fac2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 12 Jul 2018 01:51:49 +0200 Subject: [PATCH 136/868] (Hopefully) fixed SVG shader on macOS --- src/svg.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/svg.rs b/src/svg.rs index 913978831..506f90d69 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -61,6 +61,11 @@ const SHADER_VERSION_GLES: &str = "#version 300 es"; const SVG_VERTEX_SHADER: &str = " + precision highp float; + + #define attribute in + #define varying out + in vec2 xy; in vec2 normal; @@ -82,6 +87,11 @@ fn prefix_gl_version(shader: &str, gl: Api) -> String { const SVG_FRAGMENT_SHADER: &str = " + precision highp float; + + #define attribute in + #define varying out + uniform vec4 color; out vec4 out_color; From ee6f52e245e3b84a11629b784815616fc337d509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 12 Jul 2018 06:40:22 +0200 Subject: [PATCH 137/868] Added default font loading, implemented text alignment via justify-content - The descriptors "sans-serif", "serif" and "monospace" are now loaded on startup - Text alignment now works similar to CSS: If the parent has flex-direction: column and justify-content: XXX, the text gets vertically aligned. If it has flex-direction: row, it gets horizontally aligned and if it additionally has text-align: center, the text will get horizontally aligned. --- Cargo.toml | 1 + README.md | 5 ++- examples/debug.rs | 6 +-- examples/test_content.css | 10 ++--- src/display_list.rs | 77 +++++++++++++++++++++++++++----- src/font.rs | 4 +- src/lib.rs | 1 + src/resources.rs | 92 ++++++++++++++++++++++++++++++++++++--- src/text_layout.rs | 2 +- 9 files changed, 166 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 389e44ce2..f60ed13ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ lazy_static = "1.0.1" palette = "0.4.0" tinyfiledialogs = "3.3.5" clipboard2 = "0.1.0" +font-loader = "0.7.0" [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" diff --git a/README.md b/README.md index fb83d34b5..66c85921c 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,11 @@ CMake is used during the build process to compile servo-freetype. For interfacing with the system clipboard, you also need `xorg-dev` or `libxcb-xkb-dev`. +Since azul uses the system-native fonts by default, you'll also need +`libfontconfig1-dev` and your users will need `libfontconfig` installed. + ``` -sudo apt install cmake libxcb-xkb-dev +sudo apt install cmake libxcb-xkb-dev libfontconfig libfontconfig1-dev ``` ## Design diff --git a/examples/debug.rs b/examples/debug.rs index a00f1ed3d..fbbc4d9bb 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -8,7 +8,6 @@ use azul::dialogs::*; use std::fs; const TEST_CSS: &str = include_str!("test_content.css"); -const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); #[derive(Debug)] pub struct MyAppData { @@ -35,8 +34,7 @@ impl Layout for MyAppData { .dom(&info.window, &map.cache) .with_callback(On::Scroll, Callback(scroll_map_contents)) } else { - Dom::new(NodeType::Div) - .with_class("__azul-native-button") + Button::with_label("Load SVG").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } @@ -79,8 +77,6 @@ fn main() { // Parse and validate the CSS let css = Css::new_from_string(TEST_CSS).unwrap(); let mut app = App::new(MyAppData { map: None }); - - app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } diff --git a/examples/test_content.css b/examples/test_content.css index 1c33a0261..b76616c3b 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -3,21 +3,17 @@ border: 1px solid #b7b7b7; border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; - /*background: image("Cat01");*/ background: linear-gradient(#fcfcfc, #efefef); width: [[ my_id | 200px ]]; height: 200px; min-height: 400px; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-around; - align-items: center; text-align: center; - align-content: center; + flex-direction: column; + justify-content: center; } * { font-size: 12px; - font-family: "Webly Sleeky UI", sans-serif; + font-family: sans-serif; color: black; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index aafe128bd..989a835ca 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -309,7 +309,6 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { for rect_idx in self.rectangles.linear_iter() { - let display_rectangle = &self.rectangles[rect_idx].data; let arena = self.ui_descr.ui_descr_arena.borrow(); let node_type = &arena[rect_idx].data.node_type; @@ -319,7 +318,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // temporary: fill the whole window with each rectangle displaylist_handle_rect( &mut builder, - display_rectangle, + rect_idx, + &self.rectangles, node_type, full_screen_rect, /* replace this with the real bounds */ full_screen_rect, @@ -334,9 +334,10 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } -fn displaylist_handle_rect( +fn displaylist_handle_rect<'a>( builder: &mut DisplayListBuilder, - rect: &DisplayRectangle, + rect_idx: NodeId, + arena: &Arena>, html_node: &NodeType, bounds: TypedRect, full_screen_rect: TypedRect, @@ -344,6 +345,8 @@ fn displaylist_handle_rect( render_api: &RenderApi, resource_updates: &mut Vec) { + let rect = &arena[rect_idx].data; + let info = LayoutPrimitiveInfo { rect: bounds, clip_rect: bounds, @@ -397,6 +400,8 @@ fn displaylist_handle_rect( builder, &rect.style); + let (horz_alignment, vert_alignment) = determine_text_alignment(rect_idx, arena); + // handle the special content of the node match html_node { Div => { /* nothing special to do */ }, @@ -409,7 +414,9 @@ fn displaylist_handle_rect( app_resources, &render_api, &bounds, - resource_updates); + resource_updates, + horz_alignment, + vert_alignment); }, Text(text_id) => { push_text( @@ -420,7 +427,9 @@ fn displaylist_handle_rect( app_resources, &render_api, &bounds, - resource_updates); + resource_updates, + horz_alignment, + vert_alignment); }, Image(image_id) => { push_image(&info, builder, &bounds, app_resources, image_id); @@ -463,6 +472,46 @@ fn displaylist_handle_rect( } } +/// For a given rectangle, determines what text alignment should be used +fn determine_text_alignment<'a>(rect_idx: NodeId, arena: &Arena>) +-> (TextAlignmentHorz, TextAlignmentVert) +{ + let mut horz_alignment = TextAlignmentHorz::default(); + let mut vert_alignment = TextAlignmentVert::default(); + + let rect = &arena[rect_idx]; + + if let Some((Some(flex_direction), Some(justify_content))) = rect.parent.and_then(|parent| { + let parent = &arena[parent]; + Some((parent.data.layout.direction, parent.data.layout.justify_content)) + }) { + use css_parser::{LayoutDirection::*, LayoutJustifyContent::*}; + + match flex_direction { + Horizontal => { + horz_alignment = match justify_content { + Start => TextAlignmentHorz::Left, + End => TextAlignmentHorz::Right, + Center | SpaceBetween | SpaceAround => TextAlignmentHorz::Center, + }; + }, + Vertical => { + vert_alignment = match justify_content { + Start => TextAlignmentVert::Top, + End => TextAlignmentVert::Bottom, + Center | SpaceBetween | SpaceAround => TextAlignmentVert::Center, + }; + }, + } + } + + if let Some(text_align) = rect.data.style.text_align { + horz_alignment = text_align; + } + + (horz_alignment, vert_alignment) +} + #[inline] fn push_rect( info: &PrimitiveInfo, @@ -481,7 +530,9 @@ fn push_text<'a>( app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, - resource_updates: &mut Vec) + resource_updates: &mut Vec, + horz_alignment: TextAlignmentHorz, + vert_alignment: TextAlignmentVert) { use dom::NodeType::*; use euclid::{TypedPoint2D, Length}; @@ -507,10 +558,8 @@ fn push_text<'a>( None => return, }; - let vert_alignment = TextAlignmentVert::Center; // TODO let line_height = style.line_height; - let horz_alignment = style.text_align.unwrap_or(TextAlignmentHorz::default()); let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); let scrollbar_style = ScrollbarInfo { @@ -543,8 +592,16 @@ fn push_text<'a>( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); + use text_layout::TextOverflow; + // If the rectangle should have a scrollbar, push a scrollbar onto the display list - push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) + // TODO !!! + if let TextOverflow::IsOverflowing(amount_vert) = scrollbar_info.vertical { + push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) + } + if let TextOverflow::IsOverflowing(amount_horz) = scrollbar_info.horizontal { + push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) + } } /// Adds a scrollbar to the left or bottom side of a rectangle. diff --git a/src/font.rs b/src/font.rs index 318e05fcc..5515fa0b7 100644 --- a/src/font.rs +++ b/src/font.rs @@ -35,9 +35,9 @@ impl From for FontError { } /// Read font data to get font information, v_metrics, glyph info etc. -pub(crate) fn rusttype_load_font<'a>(data: Vec) -> Result, FontError> { +pub(crate) fn rusttype_load_font<'a>(data: Vec, index: Option) -> Result, FontError> { let collection = FontCollection::from_bytes(data)?; - let font = collection.clone().into_font().unwrap_or(collection.font_at(0)?); + let font = collection.clone().into_font().unwrap_or(collection.font_at(index.and_then(|i| Some(i as usize)).unwrap_or(0))?); Ok(font) } diff --git a/src/lib.rs b/src/lib.rs index 0773c1b68..a60d02828 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,7 @@ extern crate unicode_normalization; extern crate harfbuzz_rs; extern crate tinyfiledialogs; extern crate clipboard2; +extern crate font_loader; #[cfg(not(target_os = "linux"))] extern crate nfd; diff --git a/src/resources.rs b/src/resources.rs index 49937de14..76fbfbc94 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -60,10 +60,13 @@ pub(crate) struct AppResources<'a> { impl<'a> Default for AppResources<'a> { fn default() -> Self { + let mut default_font_data = FastHashMap::default(); + load_system_fonts(&mut default_font_data); + Self { css_ids_to_image_ids: FastHashMap::default(), fonts: FastHashMap::default(), - font_data: FastHashMap::default(), + font_data: default_font_data, images: FastHashMap::default(), text_cache: TextCache::default(), clipboard: SystemClipboard::new().unwrap(), @@ -71,6 +74,83 @@ impl<'a> Default for AppResources<'a> { } } +fn load_system_fonts<'a>(fonts: &mut FastHashMap, FontState)>) { + + use font_loader::system_fonts::{self, FontPropertyBuilder}; + use css_parser::Font::BuiltinFont; + use font::rusttype_load_font; + + // preferred ordering in which the fonts should be loaded + // technically this ignores the fontconfig attribute, since font_loader + // doesn't respect that, currently + + // serif + let sysfonts = system_fonts::query_all(); + + let mut serif_builder = FontPropertyBuilder::new(); + for font in &sysfonts { + match &**font { + "Times New Roman" => { serif_builder = serif_builder.family("Times New Roman"); break; } + "Times" => { serif_builder = serif_builder.family("Times"); break; } + "Georgia" => { serif_builder = serif_builder.family("Georgia"); break; } + "DejaVu Serif" => { serif_builder = serif_builder.family("DejaVu Serif"); break; } + "GNU Unifont" => { serif_builder = serif_builder.family("GNU Unifont"); break; } + other => { serif_builder = serif_builder.family(other); } + } + } + + if let Some((font_bytes, idx)) = system_fonts::get(&serif_builder.build()) { + match rusttype_load_font(font_bytes.clone(), Some(idx)) { + Ok(f) => { fonts.insert(BuiltinFont("serif"), (f, FontState::ReadyForUpload(font_bytes))); }, + Err(e) => println!("error loading serif font: {:?}", e), + } + } + + // sans-serif + let mut sans_serif_builder = FontPropertyBuilder::new(); + for font in &sysfonts { + match &**font { + "Segoe UI" => { sans_serif_builder = sans_serif_builder.family("Segoe UI"); break; } + "Segoe UI Semibold" => { sans_serif_builder = sans_serif_builder.family("Segoe UI Semibold"); break; } + "Arial" => { sans_serif_builder = sans_serif_builder.family("Arial"); break; } + "Helvetica" => { sans_serif_builder = sans_serif_builder.family("Helvetica"); break; } + "Helvetica Neue" => { sans_serif_builder = sans_serif_builder.family("Helvetica Neue"); break; } + "Verdana" => { sans_serif_builder = sans_serif_builder.family("Verdana"); break; } + other => { sans_serif_builder = sans_serif_builder.family(other); } + } + } + + if let Some((font_bytes, idx)) = system_fonts::get(&sans_serif_builder.build()) { + match rusttype_load_font(font_bytes.clone(), Some(idx)) { + Ok(f) => { fonts.insert(BuiltinFont("sans-serif"), (f, FontState::ReadyForUpload(font_bytes))); }, + Err(e) => println!("error loading sans-serif font: {:?}", e), + } + } + + // monospace + let mut monospace_builder = FontPropertyBuilder::new(); + for font in system_fonts::query_specific(&mut FontPropertyBuilder::new().monospace().build()) { + match &*font { + "Consolas" => { monospace_builder = monospace_builder.family("Consolas"); break; }, + "Courier New" => { monospace_builder = monospace_builder.family("Courier New"); break; } + "Lucida Console" => { monospace_builder = monospace_builder.family("Lucida Console"); break; } + "Noto Mono" => { monospace_builder = monospace_builder.family("Noto Mono"); break; } + "Ubuntu Mono" => { monospace_builder = monospace_builder.family("Ubuntu Mono"); break; } + "Liberation Mono" => { monospace_builder = monospace_builder.family("Liberation Mono"); break; } + "Droid Sans Mono" => { monospace_builder = monospace_builder.family("Droid Sans Mono"); break; } + "DejaVu Sans Mono" => { monospace_builder = monospace_builder.family("DejaVu Sans Mono"); break; } + other => { monospace_builder = monospace_builder.family(other); } + } + } + + if let Some((font_bytes, idx)) = system_fonts::get(&monospace_builder.build()) { + match rusttype_load_font(font_bytes.clone(), Some(idx)) { + Ok(f) => { fonts.insert(BuiltinFont("monospace"), (f, FontState::ReadyForUpload(font_bytes))); }, + Err(e) => println!("error loading monospace font: {:?}", e), + } + } +} + impl<'a> AppResources<'a> { /// See `AppState::add_image()` @@ -146,7 +226,7 @@ impl<'a> AppResources<'a> { Vacant(v) => { let mut font_data = Vec::::new(); data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; - let parsed_font = font::rusttype_load_font(font_data.clone())?; + let parsed_font = font::rusttype_load_font(font_data.clone(), None)?; v.insert((parsed_font, FontState::ReadyForUpload(font_data))); Ok(Some(())) }, @@ -211,14 +291,14 @@ impl<'a> AppResources<'a> { self.text_cache.clear_all_texts(); } - pub(crate) fn get_clipboard_string(&mut self) - -> Result + pub(crate) fn get_clipboard_string(&mut self) + -> Result { self.clipboard.get_string_contents() } - pub(crate) fn set_clipboard_string(&mut self, contents: String) - -> Result<(), ClipboardError> + pub(crate) fn set_clipboard_string(&mut self, contents: String) + -> Result<(), ClipboardError> { self.clipboard.set_string_contents(contents) } diff --git a/src/text_layout.rs b/src/text_layout.rs index 6b19534fc..dad09faff 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -7,7 +7,7 @@ use { resources::AppResources, display_list::TextInfo, css_parser::{ - TextAlignmentHorz, FontSize, BackgroundColor, + TextAlignmentHorz, FontSize, BackgroundColor, Font as FontId, TextAlignmentVert, LineHeight, LayoutOverflow }, }; From a89fca674ca720997969277e835df6c860f39580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 12 Jul 2018 15:59:31 +0200 Subject: [PATCH 138/868] Improved font rendering --- Cargo.toml | 4 +- README.md | 11 +++--- examples/debug.rs | 2 +- examples/test_content.css | 8 ++-- src/display_list.rs | 7 +++- src/resources.rs | 77 ++++++--------------------------------- src/text_layout.rs | 19 +++++++--- src/window_state.rs | 2 +- 8 files changed, 44 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f60ed13ca..b0316804c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ glium = "0.21.0" gleam = "0.5" euclid = "0.17" image = "0.19.0" -rusttype = "0.5.2" +rusttype = "0.6.2" app_units = "0.6" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" @@ -43,7 +43,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "a30fd2286f424e528e3bde502d1a11ed5ef7ec31" +rev = "091e9c53acd71fea20da18ea3a3d5eec7ac6c7d5" [features] # The reason we do this is because doctests don't get cfg(test) diff --git a/README.md b/README.md index 66c85921c..96c4a5e25 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,15 @@ engine for rendering and a CSS / DOM model for layout and rendering On Linux, you currently need to install `cmake` before you can use azul. CMake is used during the build process to compile servo-freetype. -For interfacing with the system clipboard, you also need `xorg-dev` or -`libxcb-xkb-dev`. +For interfacing with the system clipboard, you also need `libxcb-xkb-dev`. +Since azul uses the system-native fonts by default, you'll also need +`libfontconfig1-dev` (which includes expat and freetype2). -Since azul uses the system-native fonts by default, you'll also need -`libfontconfig1-dev` and your users will need `libfontconfig` installed. +Your users will need to install `libfontconfig` and +`libxcb-xkb1` installed (remember this for packaging rpm or deb packages). ``` -sudo apt install cmake libxcb-xkb-dev libfontconfig libfontconfig1-dev +sudo apt install cmake libxcb-xkb-dev libfontconfig1-dev ``` ## Design diff --git a/examples/debug.rs b/examples/debug.rs index fbbc4d9bb..70a752ea0 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -34,7 +34,7 @@ impl Layout for MyAppData { .dom(&info.window, &map.cache) .with_callback(On::Scroll, Callback(scroll_map_contents)) } else { - Button::with_label("Load SVG").dom() + Button::with_label("Azul App").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } diff --git a/examples/test_content.css b/examples/test_content.css index b76616c3b..9cee7e844 100644 --- a/examples/test_content.css +++ b/examples/test_content.css @@ -1,7 +1,6 @@ .__azul-native-button { - background-color: #fcfcfc; border: 1px solid #b7b7b7; - border-radius: 4px; + border-radius: 5.5px; box-shadow: 0px 0px 3px #c5c5c5ad; background: linear-gradient(#fcfcfc, #efefef); width: [[ my_id | 200px ]]; @@ -13,7 +12,8 @@ } * { - font-size: 12px; + font-size: 14px; font-family: sans-serif; - color: black; + color: #4c4c4c; + background-color: #e7e7e7; } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 989a835ca..3f10821b5 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -584,7 +584,12 @@ fn push_text<'a>( ); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); - let flags = FontInstanceFlags::SUBPIXEL_BGR; + let mut flags = FontInstanceFlags::empty(); + flags.set(FontInstanceFlags::SUBPIXEL_BGR, true); + flags.set(FontInstanceFlags::FONT_SMOOTHING, true); + flags.set(FontInstanceFlags::FORCE_AUTOHINT, true); + flags.set(FontInstanceFlags::LCD_VERTICAL, true); + let options = GlyphOptions { render_mode: FontRenderMode::Subpixel, flags: flags, diff --git a/src/resources.rs b/src/resources.rs index 76fbfbc94..aa8423513 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -80,75 +80,20 @@ fn load_system_fonts<'a>(fonts: &mut FastHashMap attribute, since font_loader - // doesn't respect that, currently - - // serif - let sysfonts = system_fonts::query_all(); - - let mut serif_builder = FontPropertyBuilder::new(); - for font in &sysfonts { - match &**font { - "Times New Roman" => { serif_builder = serif_builder.family("Times New Roman"); break; } - "Times" => { serif_builder = serif_builder.family("Times"); break; } - "Georgia" => { serif_builder = serif_builder.family("Georgia"); break; } - "DejaVu Serif" => { serif_builder = serif_builder.family("DejaVu Serif"); break; } - "GNU Unifont" => { serif_builder = serif_builder.family("GNU Unifont"); break; } - other => { serif_builder = serif_builder.family(other); } - } - } - - if let Some((font_bytes, idx)) = system_fonts::get(&serif_builder.build()) { - match rusttype_load_font(font_bytes.clone(), Some(idx)) { - Ok(f) => { fonts.insert(BuiltinFont("serif"), (f, FontState::ReadyForUpload(font_bytes))); }, - Err(e) => println!("error loading serif font: {:?}", e), - } - } - - // sans-serif - let mut sans_serif_builder = FontPropertyBuilder::new(); - for font in &sysfonts { - match &**font { - "Segoe UI" => { sans_serif_builder = sans_serif_builder.family("Segoe UI"); break; } - "Segoe UI Semibold" => { sans_serif_builder = sans_serif_builder.family("Segoe UI Semibold"); break; } - "Arial" => { sans_serif_builder = sans_serif_builder.family("Arial"); break; } - "Helvetica" => { sans_serif_builder = sans_serif_builder.family("Helvetica"); break; } - "Helvetica Neue" => { sans_serif_builder = sans_serif_builder.family("Helvetica Neue"); break; } - "Verdana" => { sans_serif_builder = sans_serif_builder.family("Verdana"); break; } - other => { sans_serif_builder = sans_serif_builder.family(other); } - } - } - - if let Some((font_bytes, idx)) = system_fonts::get(&sans_serif_builder.build()) { - match rusttype_load_font(font_bytes.clone(), Some(idx)) { - Ok(f) => { fonts.insert(BuiltinFont("sans-serif"), (f, FontState::ReadyForUpload(font_bytes))); }, - Err(e) => println!("error loading sans-serif font: {:?}", e), - } - } - - // monospace - let mut monospace_builder = FontPropertyBuilder::new(); - for font in system_fonts::query_specific(&mut FontPropertyBuilder::new().monospace().build()) { - match &*font { - "Consolas" => { monospace_builder = monospace_builder.family("Consolas"); break; }, - "Courier New" => { monospace_builder = monospace_builder.family("Courier New"); break; } - "Lucida Console" => { monospace_builder = monospace_builder.family("Lucida Console"); break; } - "Noto Mono" => { monospace_builder = monospace_builder.family("Noto Mono"); break; } - "Ubuntu Mono" => { monospace_builder = monospace_builder.family("Ubuntu Mono"); break; } - "Liberation Mono" => { monospace_builder = monospace_builder.family("Liberation Mono"); break; } - "Droid Sans Mono" => { monospace_builder = monospace_builder.family("Droid Sans Mono"); break; } - "DejaVu Sans Mono" => { monospace_builder = monospace_builder.family("DejaVu Sans Mono"); break; } - other => { monospace_builder = monospace_builder.family(other); } + fn insert_font<'b>(fonts: &mut FastHashMap, FontState)>, target: &'static str) { + if let Some((font_bytes, idx)) = system_fonts::get(&FontPropertyBuilder::new().family(target).build()) { + match rusttype_load_font(font_bytes.clone(), Some(idx)) { + Ok(f) => { fonts.insert(BuiltinFont(target), (f, FontState::ReadyForUpload(font_bytes))); }, + Err(e) => println!("error loading {} font: {:?}", target, e), + } } } - if let Some((font_bytes, idx)) = system_fonts::get(&monospace_builder.build()) { - match rusttype_load_font(font_bytes.clone(), Some(idx)) { - Ok(f) => { fonts.insert(BuiltinFont("monospace"), (f, FontState::ReadyForUpload(font_bytes))); }, - Err(e) => println!("error loading monospace font: {:?}", e), - } - } + insert_font(fonts, "serif"); + insert_font(fonts, "sans-serif"); + insert_font(fonts, "monospace"); + insert_font(fonts, "cursive"); + insert_font(fonts, "fantasy"); } impl<'a> AppResources<'a> { diff --git a/src/text_layout.rs b/src/text_layout.rs index dad09faff..978a1ed37 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -201,7 +201,7 @@ pub(crate) fn get_glyphs<'a>( let v_metrics_scaled = target_font.0.v_metrics(font_size_with_line_height); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - let offset_top = v_metrics_scaled.ascent; + let offset_top = v_metrics_scaled.ascent / 2.0; let font_metrics = FontMetrics { vertical_advance: v_advance_scaled, @@ -380,22 +380,29 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: // Regular character use rusttype::Point; - let g = font.glyph(cur_char).scaled(font_size); + let g = font.glyph(cur_char); + + // calculate the real width + let glyph_metrics = g.standalone().get_data().unwrap(); + let extents = glyph_metrics.extents.unwrap(); + let horz_rect_width = extents.max.x - extents.min.x; + let horiz_advance = horz_rect_width as f32 * glyph_metrics.scale_for_1_pixel * font_size.x; + let id = g.id(); if let Some(last) = last_glyph { - word_caret += font.pair_kerning(font_size, last, g.id()); + word_caret += font.pair_kerning(font_size, last, id); } - let g = g.positioned(Point { x: word_caret, y: 0.0 }); + let word_caret_saved = word_caret; + last_glyph = Some(id); - let horiz_advance = g.unpositioned().h_metrics().advance_width; word_caret += horiz_advance; cur_word_length += horiz_advance; glyphs_in_this_word.push(GlyphInstance { index: id.0, - point: TypedPoint2D::new(g.position().x, g.position().y), + point: TypedPoint2D::new(word_caret_saved, 0.0), }); chars_in_this_word.push(cur_char); diff --git a/src/window_state.rs b/src/window_state.rs index 080f43856..5cc341809 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -232,7 +232,7 @@ impl WindowState MouseScrollDelta::LineDelta(x, y) => (x * 100.0, y * 100.0), }; self.mouse_state.scroll_x = scroll_x_px; - self.mouse_state.scroll_y = scroll_y_px; + self.mouse_state.scroll_y = -scroll_y_px; // TODO: "natural scrolling"? events_vec.push(On::Scroll); }, _ => { } From ef989ce70522f5329f52f52f3463a994607b6d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 13 Jul 2018 00:17:41 +0200 Subject: [PATCH 139/868] Added DPI scaling (upgraded winit) + fixed projection for SVG Also implemented zooming for the SVG component, moved styles to other folder. DPI handling not tested yet. --- Cargo.toml | 2 +- assets/native_linux.css | 0 assets/native_macos.css | 0 assets/native_windows.css | 0 examples/debug.rs | 37 +++-- src/app.rs | 39 +++-- src/css.rs | 6 +- src/display_list.rs | 8 +- src/lib.rs | 1 + .../styles/native_linux.css | 36 ++--- src/styles/native_macos.css | 19 +++ src/styles/native_windows.css | 19 +++ src/svg.rs | 5 +- src/widgets.rs | 11 +- src/window.rs | 59 ++++---- src/window_state.rs | 135 +++++++++++++----- 16 files changed, 258 insertions(+), 119 deletions(-) delete mode 100644 assets/native_linux.css delete mode 100644 assets/native_macos.css delete mode 100644 assets/native_windows.css rename examples/test_content.css => src/styles/native_linux.css (96%) create mode 100644 src/styles/native_macos.css create mode 100644 src/styles/native_windows.css diff --git a/Cargo.toml b/Cargo.toml index b0316804c..eee61609c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ readme = "README.md" cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" -glium = "0.21.0" +glium = "0.22.0" gleam = "0.5" euclid = "0.17" image = "0.19.0" diff --git a/assets/native_linux.css b/assets/native_linux.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/assets/native_macos.css b/assets/native_macos.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/assets/native_windows.css b/assets/native_windows.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/debug.rs b/examples/debug.rs index 70a752ea0..37482bebd 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -7,8 +7,6 @@ use azul::widgets::*; use azul::dialogs::*; use std::fs; -const TEST_CSS: &str = include_str!("test_content.css"); - #[derive(Debug)] pub struct MyAppData { pub map: Option, @@ -18,9 +16,9 @@ pub struct MyAppData { pub struct Map { pub cache: SvgCache, pub layers: Vec, - pub zoom: f32, - pub pan_horz: f32, - pub pan_vert: f32, + pub zoom: f64, + pub pan_horz: f64, + pub pan_vert: f64, } impl Layout for MyAppData { @@ -29,11 +27,13 @@ impl Layout for MyAppData { { if let Some(map) = &self.map { Svg::with_layers(map.layers.clone()) - .with_pan(map.pan_horz, map.pan_vert) - .with_zoom(map.zoom) + .with_pan(map.pan_horz as f32, map.pan_vert as f32) + .with_zoom(map.zoom as f32) .dom(&info.window, &map.cache) .with_callback(On::Scroll, Callback(scroll_map_contents)) } else { + // TODO: If this is changed to Label::new(), the text is cut off at the top + // because of the (offset_top / 2.0) - see text_layout.rs file Button::with_label("Azul App").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } @@ -43,9 +43,21 @@ impl Layout for MyAppData { fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { + let mouse_state = app_state.windows[event.window].get_mouse_state(); - map.pan_horz += mouse_state.scroll_x; - map.pan_vert += mouse_state.scroll_y; + let keyboard_state = app_state.windows[event.window].get_keyboard_state(); + + if keyboard_state.shift_down { + map.pan_horz += mouse_state.scroll_y; + } else if keyboard_state.ctrl_down { + if mouse_state.scroll_y.is_sign_positive() { + map.zoom /= 2.0; + } else { + map.zoom *= 2.0; + } + } else { + map.pan_vert += mouse_state.scroll_y; + } } }); @@ -73,10 +85,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { - - // Parse and validate the CSS - let css = Css::new_from_string(TEST_CSS).unwrap(); let mut app = App::new(MyAppData { map: None }); - app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); -} +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 124f6dc7b..178fed86b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, Mutex, PoisonError}, }; use glium::{SwapBuffersError, glutin::Event}; +use glium::glutin::dpi::{LogicalPosition, LogicalSize}; use webrender::api::{RenderApi, HitTestFlags, DevicePixel}; use image::ImageError; use euclid::{TypedScale, TypedSize2D}; @@ -64,9 +65,9 @@ pub(crate) struct FrameEventInfo { pub(crate) should_redraw_window: bool, pub(crate) should_swap_window: bool, pub(crate) should_hittest: bool, - pub(crate) cur_cursor_pos: (f64, f64), - pub(crate) new_window_size: Option<(u32, u32)>, - pub(crate) new_dpi_factor: Option, + pub(crate) cur_cursor_pos: LogicalPosition, + pub(crate) new_window_size: Option, + pub(crate) new_dpi_factor: Option, pub(crate) is_resize_event: bool, } @@ -76,7 +77,7 @@ impl Default for FrameEventInfo { should_redraw_window: false, should_swap_window: false, should_hittest: false, - cur_cursor_pos: (0.0, 0.0), + cur_cursor_pos: LogicalPosition::new(0.0, 0.0), new_window_size: None, new_dpi_factor: None, is_resize_event: false, @@ -168,6 +169,8 @@ impl<'a, T: Layout> App<'a, T> { continue 'window_loop; } window.state.update_mouse_cursor_position(event); + window.state.update_keyboard_modifiers(event); + window.state.update_keyboard_pressed_chars(event); } if frame_event_info.should_hittest { @@ -260,10 +263,11 @@ impl<'a, T: Layout> App<'a, T> { use euclid::TypedSize2D; let mut txn = Transaction::new(); - let framebuffer_size = TypedSize2D::new(window.state.size.width, window.state.size.height); + let physical_fb_dimensions = window.state.size.dimensions.to_physical(window.state.size.hidpi_factor); + let framebuffer_size = TypedSize2D::new(physical_fb_dimensions.width as u32, physical_fb_dimensions.height as u32); let bounds = DeviceUintRect::new(DeviceUintPoint::new(0, 0), framebuffer_size); - txn.set_window_parameters(framebuffer_size, bounds, window.state.size.hidpi_factor); + txn.set_window_parameters(framebuffer_size, bounds, window.state.size.hidpi_factor as f32); window.internal.api.send_transaction(window.internal.document_id, txn); } @@ -477,6 +481,7 @@ enum WindowCloseEvent { fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> WindowCloseEvent { use glium::glutin::WindowEvent; + use glium::glutin::dpi::LogicalSize; match event { Event::WindowEvent { event, .. } => { @@ -488,22 +493,22 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> Win frame_event_info.should_hittest = true; frame_event_info.cur_cursor_pos = *position; }, - WindowEvent::Resized(w, h) => { - frame_event_info.new_window_size = Some((*w, *h)); + WindowEvent::Resized(wh) => { + frame_event_info.new_window_size = Some(*wh); frame_event_info.is_resize_event = true; frame_event_info.should_redraw_window = true; }, WindowEvent::Refresh => { frame_event_info.should_redraw_window = true; }, - WindowEvent::HiDPIFactorChanged(dpi) => { + WindowEvent::HiDpiFactorChanged(dpi) => { frame_event_info.new_dpi_factor = Some(*dpi); frame_event_info.should_redraw_window = true; }, WindowEvent::MouseWheel { .. } => { frame_event_info.should_hittest = true; }, - WindowEvent::Closed => { + WindowEvent::CloseRequested => { return WindowCloseEvent::AboutToClose; }, _ => { }, @@ -532,7 +537,12 @@ fn do_hit_test_and_call_callbacks( use dom::Callback; use window_state::{KeyboardState, MouseState}; - let (cursor_x, cursor_y) = window.state.mouse_state.cursor_pos.and_then(|(x, y)| Some((x as f32, y as f32))).unwrap_or((0.0, 0.0)); + let (cursor_x, cursor_y) = window.state.mouse_state.cursor_pos + .and_then(|pos| { + let physical_position = pos.to_physical(window.state.size.hidpi_factor); + Some((physical_position.x as f32, physical_position.y as f32)) + }).unwrap_or((0.0, 0.0)); + let point = WorldPoint::new(cursor_x, cursor_y); let hit_test_results = window.internal.api.hit_test( @@ -614,8 +624,11 @@ fn render( let mut txn = Transaction::new(); - let framebuffer_size = TypedSize2D::new(window.state.size.width, window.state.size.height); - let layout_size = framebuffer_size.to_f32() / TypedScale::new(window.state.size.hidpi_factor); + let LogicalSize { width, height } = window.state.size.dimensions; + let layout_size = TypedSize2D::new(width as f32, height as f32); + + let framebuffer_size_physical = window.state.size.dimensions.to_physical(window.state.size.hidpi_factor); + let framebuffer_size = TypedSize2D::new(framebuffer_size_physical.width as u32, framebuffer_size_physical.height as u32); txn.set_display_list( window.internal.epoch, diff --git a/src/css.rs b/src/css.rs index 90b6c5a4b..3059f6539 100644 --- a/src/css.rs +++ b/src/css.rs @@ -8,11 +8,11 @@ use { }; #[cfg(target_os="windows")] -const NATIVE_CSS_WINDOWS: &str = include_str!("../assets/native_windows.css"); +const NATIVE_CSS_WINDOWS: &str = include_str!("styles/native_windows.css"); #[cfg(target_os="linux")] -const NATIVE_CSS_LINUX: &str = include_str!("../assets/native_linux.css"); +const NATIVE_CSS_LINUX: &str = include_str!("styles/native_linux.css"); #[cfg(target_os="macos")] -const NATIVE_CSS_MACOS: &str = include_str!("../assets/native_macos.css"); +const NATIVE_CSS_MACOS: &str = include_str!("styles/native_macos.css"); /// All the keys that, when changed, can trigger a re-layout const RELAYOUT_RULES: [&str; 13] = [ diff --git a/src/display_list.rs b/src/display_list.rs index 3f10821b5..63401bc66 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -297,10 +297,10 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { css.needs_relayout = false; - let framebuffer_size = LayoutSize::new(window_size.width as f32, window_size.height as f32); - let hidpi_factor = TypedScale::new(window_size.hidpi_factor); - let whole_window_layout_size = framebuffer_size.to_f32() / hidpi_factor; - let mut builder = DisplayListBuilder::with_capacity(pipeline_id, whole_window_layout_size, self.rectangles.nodes_len()); + use glium::glutin::dpi::LogicalSize; + + let LogicalSize { width, height } = window_size.dimensions; + let mut builder = DisplayListBuilder::with_capacity(pipeline_id, TypedSize2D::new(width as f32, height as f32), self.rectangles.nodes_len()); let mut resource_updates = Vec::::new(); let full_screen_rect = LayoutRect::new(LayoutPoint::zero(), builder.content_size());; diff --git a/src/lib.rs b/src/lib.rs index a60d02828..e2bb7ad52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -144,6 +144,7 @@ pub mod prelude { }; pub use svg::{SvgLayerId, SvgLayer, SvgCache}; + pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; } /// Re-exports of errors diff --git a/examples/test_content.css b/src/styles/native_linux.css similarity index 96% rename from examples/test_content.css rename to src/styles/native_linux.css index 9cee7e844..72140ee2f 100644 --- a/examples/test_content.css +++ b/src/styles/native_linux.css @@ -1,19 +1,19 @@ -.__azul-native-button { - border: 1px solid #b7b7b7; - border-radius: 5.5px; - box-shadow: 0px 0px 3px #c5c5c5ad; - background: linear-gradient(#fcfcfc, #efefef); - width: [[ my_id | 200px ]]; - height: 200px; - min-height: 400px; - text-align: center; - flex-direction: column; - justify-content: center; -} - -* { - font-size: 14px; - font-family: sans-serif; - color: #4c4c4c; - background-color: #e7e7e7; +.__azul-native-button { + border: 1px solid #b7b7b7; + border-radius: 5.5px; + box-shadow: 0px 0px 3px #c5c5c5ad; + background: linear-gradient(#fcfcfc, #efefef); + width: [[ my_id | 200px ]]; + height: 200px; + min-height: 400px; + text-align: center; + flex-direction: column; + justify-content: center; +} + +* { + font-size: 14px; + font-family: sans-serif; + color: #4c4c4c; + background-color: #e7e7e7; } \ No newline at end of file diff --git a/src/styles/native_macos.css b/src/styles/native_macos.css new file mode 100644 index 000000000..72140ee2f --- /dev/null +++ b/src/styles/native_macos.css @@ -0,0 +1,19 @@ +.__azul-native-button { + border: 1px solid #b7b7b7; + border-radius: 5.5px; + box-shadow: 0px 0px 3px #c5c5c5ad; + background: linear-gradient(#fcfcfc, #efefef); + width: [[ my_id | 200px ]]; + height: 200px; + min-height: 400px; + text-align: center; + flex-direction: column; + justify-content: center; +} + +* { + font-size: 14px; + font-family: sans-serif; + color: #4c4c4c; + background-color: #e7e7e7; +} \ No newline at end of file diff --git a/src/styles/native_windows.css b/src/styles/native_windows.css new file mode 100644 index 000000000..72140ee2f --- /dev/null +++ b/src/styles/native_windows.css @@ -0,0 +1,19 @@ +.__azul-native-button { + border: 1px solid #b7b7b7; + border-radius: 5.5px; + box-shadow: 0px 0px 3px #c5c5c5ad; + background: linear-gradient(#fcfcfc, #efefef); + width: [[ my_id | 200px ]]; + height: 200px; + min-height: 400px; + text-align: center; + flex-direction: column; + justify-content: center; +} + +* { + font-size: 14px; + font-family: sans-serif; + color: #4c4c4c; + background-color: #e7e7e7; +} \ No newline at end of file diff --git a/src/svg.rs b/src/svg.rs index 506f90d69..907fcce3f 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -73,9 +73,12 @@ const SVG_VERTEX_SHADER: &str = " uniform vec2 bbox_size; uniform vec2 offset; uniform float z_index; + uniform float zoom; void main() { - gl_Position = vec4(vec2(-1.0) + ((xy - bbox_origin) / bbox_size) + (offset / bbox_size), z_index, 1.0); + vec2 position_centered = (xy - bbox_origin) / bbox_size; + vec2 position_zoomed = position_centered * vec2(zoom); + gl_Position = vec4(vec2(-1.0) + position_zoomed + (offset / bbox_size), z_index, 1.0); }"; fn prefix_gl_version(shader: &str, gl: Api) -> String { diff --git a/src/widgets.rs b/src/widgets.rs index 165437e9d..05ef85cb9 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -121,7 +121,8 @@ impl Svg { { const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; - let tex = window.create_texture(800, 600); + let (window_width, window_height) = window.get_physical_size(); + let tex = window.create_texture(window_width as u32, window_height as u32); tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); let draw_options = DrawParameters { @@ -132,7 +133,7 @@ impl Svg { let z_index: f32 = 0.5; let bbox: TypedRect = TypedRect { origin: TypedPoint2D::new(0.0, 0.0), - size: TypedSize2D::new(800.0, 600.0), + size: TypedSize2D::new(window_width as f32, window_height as f32), }; let shader = svg_cache.init_shader(window); @@ -159,7 +160,8 @@ impl Svg { color.color.blue as f32, color.alpha as f32 ), - offset: (self.pan.0, self.pan.1) + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, }; surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); @@ -180,7 +182,8 @@ impl Svg { stroke_color.color.blue as f32, stroke_color.alpha as f32 ), - offset: (self.pan.0, self.pan.1) + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, }; surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); diff --git a/src/window.rs b/src/window.rs index f86bd0e93..7a5acb147 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,7 @@ use glium::{ IncompatibleOpenGl, Display, debug::DebugCallbackBehavior, glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, - MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, + MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder, dpi::LogicalSize}, backend::{Context, Facade, glutin::DisplayCreationError}, }; use gleam::gl::{self, Gl}; @@ -27,7 +27,7 @@ use cassowary::{ use { dom::Texture, css::{Css, FakeCss}, - window_state::{WindowState, MouseState, KeyboardState, WindowPosition}, + window_state::{WindowState, MouseState, KeyboardState}, display_list::SolvedLayout, traits::Layout, cache::{EditVariableCache, DomTreeCache}, @@ -108,6 +108,12 @@ use glium::vertex::BufferCreationError as VertexBufferCreationError; use glium::index::BufferCreationError as IndexBufferCreationError; impl ReadOnlyWindow { + + pub fn get_physical_size(&self) -> (u32, u32) { + let hidpi = self.inner.gl_window().get_hidpi_factor(); + self.inner.gl_window().get_inner_size().unwrap().to_physical(hidpi).into() + } + // Since webrender is asynchronous, we can't let the user draw // directly onto the frame or the texture since that has to be timed // with webrender @@ -538,10 +544,19 @@ impl Window { /// Creates a new window pub fn new(options: WindowCreateOptions, css: Css) -> Result { + use glium::glutin::dpi::{LogicalPosition, LogicalSize}; + let events_loop = EventsLoop::new(); + let monitor = match options.monitor { + WindowMonitorTarget::Primary => events_loop.get_primary_monitor(), + WindowMonitorTarget::Custom(ref id) => id.clone(), + }; + + let hidpi_factor = monitor.get_hidpi_factor(); + let mut window = WindowBuilder::new() - .with_dimensions(options.state.size.width, options.state.size.height) + .with_dimensions(options.state.size.dimensions) .with_title(options.state.title.clone()) .with_decorations(options.state.has_decorations) .with_visibility(options.state.is_visible) @@ -558,20 +573,15 @@ impl Window { // like setting the taskbar icon, setting the titlebar icon, etc. if options.state.is_fullscreen { - let monitor = match options.monitor { - WindowMonitorTarget::Primary => events_loop.get_primary_monitor(), - WindowMonitorTarget::Custom(ref id) => id.clone(), - }; - window = window.with_fullscreen(Some(monitor)); } - if let Some((min_w, min_h)) = options.state.size.min_dimensions { - window = window.with_min_dimensions(min_w, min_h); + if let Some(min_dim) = options.state.size.min_dimensions { + window = window.with_min_dimensions(min_dim); } - if let Some((max_w, max_h)) = options.state.size.max_dimensions { - window = window.with_max_dimensions(max_w, max_h); + if let Some(max_dim) = options.state.size.max_dimensions { + window = window.with_max_dimensions(max_dim); } fn create_context_builder<'a>(vsync: bool, srgb: bool) -> ContextBuilder<'a> { @@ -596,6 +606,7 @@ impl Window { if srgb { builder = builder.with_srgb(true); } + builder } @@ -605,8 +616,8 @@ impl Window { .or_else(|_| GlWindow::new(window.clone(), create_context_builder(false, true), &events_loop)) .or_else(|_| GlWindow::new(window, create_context_builder(false, false), &events_loop))?; - if let Some(WindowPosition { x, y }) = options.state.position { - gl_window.window().set_position(x as i32, y as i32); + if let Some(pos) = options.state.position { + gl_window.window().set_position(pos); } #[cfg(debug_assertions)] @@ -614,7 +625,7 @@ impl Window { #[cfg(not(debug_assertions))] let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; - let device_pixel_ratio = display.gl_window().hidpi_factor(); + let device_pixel_ratio = display.gl_window().get_hidpi_factor(); // this exists because RendererOptions isn't Clone-able fn get_renderer_opts(native: bool, device_pixel_ratio: f32, clear_color: Option) -> RendererOptions { @@ -641,8 +652,7 @@ impl Window { } let framebuffer_size = { - #[allow(deprecated)] - let (width, height) = display.gl_window().get_inner_size_pixels().unwrap(); + let (width, height) = display.gl_window().get_inner_size().unwrap().to_physical(hidpi_factor).into(); DeviceUintSize::new(width, height) }; @@ -650,8 +660,8 @@ impl Window { let gl = get_gl_context(&display)?; - let opts_native = get_renderer_opts(true, device_pixel_ratio, Some(options.background)); - let opts_osmesa = get_renderer_opts(false, device_pixel_ratio, Some(options.background)); + let opts_native = get_renderer_opts(true, device_pixel_ratio as f32, Some(options.background)); + let opts_osmesa = get_renderer_opts(false, device_pixel_ratio as f32, Some(options.background)); use self::RendererType::*; let (mut renderer, sender) = match options.renderer_type { @@ -674,7 +684,7 @@ impl Window { let document_id = api.add_document(framebuffer_size, 0); let epoch = Epoch(0); let pipeline_id = PipelineId(0, 0); - let layout_size = framebuffer_size.to_f32() / TypedScale::new(device_pixel_ratio); + let layout_size = framebuffer_size.to_f32() / TypedScale::new(device_pixel_ratio as f32); /* let (sender, receiver) = channel(); let thread = Builder::new().name(options.title.clone()).spawn(move || Self::handle_event(receiver))?; @@ -774,12 +784,12 @@ impl Window { } if old_state.size.min_dimensions != new_state.size.min_dimensions { - window.set_min_dimensions(new_state.size.min_dimensions); + window.set_min_dimensions(new_state.size.min_dimensions.and_then(|dim| Some(dim.into()))); old_state.size.min_dimensions = new_state.size.min_dimensions; } if old_state.size.max_dimensions != new_state.size.max_dimensions { - window.set_max_dimensions(new_state.size.max_dimensions); + window.set_max_dimensions(new_state.size.max_dimensions.and_then(|dim| Some(dim.into()))); old_state.size.max_dimensions = new_state.size.max_dimensions; } } @@ -787,9 +797,8 @@ impl Window { pub(crate) fn update_from_external_window_state(&mut self, frame_event_info: &mut FrameEventInfo) { use webrender::api::{DeviceUintSize, WorldPoint, LayoutSize}; - if let Some((w, h)) = frame_event_info.new_window_size { - self.state.size.width = w; - self.state.size.height = h; + if let Some(new_size) = frame_event_info.new_window_size { + self.state.size.dimensions = new_size; frame_event_info.should_redraw_window = true; } diff --git a/src/window_state.rs b/src/window_state.rs index 5cc341809..ae47d6264 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -4,26 +4,51 @@ use glium::glutin::{ Window, Event, WindowEvent, KeyboardInput, ElementState, MouseCursor, VirtualKeyCode, MouseButton, MouseScrollDelta, TouchPhase, + ModifiersState, dpi::{LogicalPosition, LogicalSize}, }; +use std::collections::HashSet; use { dom::On, menu::{ApplicationMenu, ContextMenu}, }; const DEFAULT_TITLE: &str = "Azul App"; -const DEFAULT_WIDTH: u32 = 800; -const DEFAULT_HEIGHT: u32 = 600; +const DEFAULT_WIDTH: f64 = 800.0; +const DEFAULT_HEIGHT: f64 = 600.0; /// Determines which keys are pressed currently (modifiers, etc.) #[derive(Debug, Default, Clone)] pub struct KeyboardState { - /// Modifier keys that are currently actively pressed during this frame - pub modifiers: Vec, - /// Hidden keys, such as the "n" in CTRL + n. Always lowercase - pub hidden_keys: Vec, - /// Actual keys pressed during this frame (i.e. regular text input) - pub keys: Vec, + // Modifier keys that are currently actively pressed during this frame + // + // Note: These are tracked seperately by glium to prevent missing state changes + // when the window isn't focused + + /// Shift key + pub shift_down: bool, + /// Ctrl key + pub ctrl_down: bool, + /// Alt key + pub alt_down: bool, + /// `Super / Windows / Command` key + pub super_down: bool, + /// Currently pressed keys + pub current_keys: HashSet, +} + +impl KeyboardState { + + fn update_from_modifier_state(&mut self, state: ModifiersState) { + self.shift_down = state.shift; + self.ctrl_down = state.ctrl; + self.alt_down = state.alt; + self.super_down = state.logo; + } + + pub(crate) fn clear_keys(&mut self) { + self.current_keys.clear(); + } } /// Mouse position on the screen @@ -33,7 +58,7 @@ pub struct MouseState /// Current mouse cursor type pub mouse_cursor_type: MouseCursor, //// Where is the mouse cursor currently? Set to `None` if the window is not focused - pub cursor_pos: Option<(f64, f64)>, + pub cursor_pos: Option, //// Is the left mouse button down? pub left_down: bool, //// Is the right mouse button down? @@ -41,9 +66,9 @@ pub struct MouseState //// Is the middle mouse button down? pub middle_down: bool, /// Scroll amount in pixels in the horizontal direction. Gets reset to 0 after every frame - pub scroll_x: f32, + pub scroll_x: f64, /// Scroll amount in pixels in the vertical direction. Gets reset to 0 after every frame - pub scroll_y: f32, + pub scroll_y: f64, } impl Default for MouseState { @@ -51,7 +76,7 @@ impl Default for MouseState { fn default() -> Self { Self { mouse_cursor_type: MouseCursor::Default, - cursor_pos: Some((0.0, 0.0)), + cursor_pos: Some(LogicalPosition::new(0.0, 0.0)), left_down: false, right_down: false, middle_down: false, @@ -76,7 +101,7 @@ pub struct WindowState /// The current context menu for this window pub context_menu: Option, /// The x and y position, or None to let the WM decide where to put the window (default) - pub position: Option, + pub position: Option, /// The state of the mouse pub(crate) mouse_state: MouseState, /// Size of the window + max width / max height: 800 x 600 by default @@ -95,34 +120,23 @@ pub struct WindowState pub is_always_on_top: bool, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct WindowPosition { - /// X position from the left side of the screen - pub x: u32, - /// Y position from the top of the screen - pub y: u32, -} - - #[derive(Debug, Copy, Clone)] pub struct WindowSize { - /// Width of the window - pub width: u32, - /// Height of the window - pub height: u32, + /// Width and height of the window, in logical + /// units (may not correspond to the physical on-screen size) + pub dimensions: LogicalSize, /// DPI factor of the window - pub hidpi_factor: f32, + pub hidpi_factor: f64, /// Minimum dimensions of the window - pub min_dimensions: Option<(u32, u32)>, + pub min_dimensions: Option, /// Maximum dimensions of the window - pub max_dimensions: Option<(u32, u32)>, + pub max_dimensions: Option, } impl Default for WindowSize { fn default() -> Self { Self { - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, + dimensions: LogicalSize::new(DEFAULT_WIDTH, DEFAULT_HEIGHT), hidpi_factor: 1.0, min_dimensions: None, max_dimensions: None, @@ -163,6 +177,7 @@ impl WindowState use glium::glutin::WindowEvent::*; use glium::glutin::{ElementState, MouseButton }; use glium::glutin::MouseButton::*; + use glium::glutin::dpi::LogicalPosition; let event = if let WindowEvent { event, .. } = event { event } else { return Vec::new(); }; @@ -172,7 +187,6 @@ impl WindowState let mut events_vec = Vec::::new(); - // TODO: right mouse down / middle mouse down? match event { MouseInput { state: ElementState::Pressed, button, .. } => { match button { @@ -228,10 +242,10 @@ impl WindowState }, MouseWheel { delta, .. } => { let (scroll_x_px, scroll_y_px) = match delta { - MouseScrollDelta::PixelDelta(x, y) => (*x, *y), - MouseScrollDelta::LineDelta(x, y) => (x * 100.0, y * 100.0), + MouseScrollDelta::PixelDelta(LogicalPosition { x, y }) => (*x, *y), + MouseScrollDelta::LineDelta(x, y) => (*x as f64 * 100.0, *y as f64 * 100.0), }; - self.mouse_state.scroll_x = scroll_x_px; + self.mouse_state.scroll_x = -scroll_x_px; self.mouse_state.scroll_y = -scroll_y_px; // TODO: "natural scrolling"? events_vec.push(On::Scroll); }, @@ -242,6 +256,27 @@ impl WindowState events_vec } + pub(crate) fn update_keyboard_modifiers(&mut self, event: &Event) { + let modifiers = match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::KeyboardInput { input: KeyboardInput { modifiers, .. }, .. } | + WindowEvent::CursorMoved { modifiers, .. } | + WindowEvent::MouseWheel { modifiers, .. } | + WindowEvent::MouseInput { modifiers, .. } => { + Some(modifiers) + }, + _ => None, + } + }, + _ => None, + }; + + if let Some(modifiers) = modifiers { + self.keyboard_state.update_from_modifier_state(*modifiers); + } + } + /// After the initial events are filtered, this will update the mouse /// cursor position, if the event is a `CursorMoved` and set it to `None` /// if the cursor has left the window @@ -256,7 +291,7 @@ impl WindowState self.mouse_state.cursor_pos = None; }, WindowEvent::CursorEntered { .. } => { - self.mouse_state.cursor_pos = Some((0.0, 0.0)) + self.mouse_state.cursor_pos = Some(LogicalPosition::new(0.0, 0.0)) }, _ => { } } @@ -264,6 +299,34 @@ impl WindowState _ => { }, } } + + /// Updates self.keyboard_state to reflect what characters are currently held down + pub(crate) fn update_keyboard_pressed_chars(&mut self, event: &Event) { + use glium::glutin::KeyboardInput; + + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, virtual_keycode: Some(vk), .. }, .. } => { + if let Some(ch) = virtual_key_code_to_char(*vk) { + self.keyboard_state.current_keys.insert(ch); + } + }, + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Released, virtual_keycode: Some(vk), .. }, .. } => { + if let Some(ch) = virtual_key_code_to_char(*vk) { + self.keyboard_state.current_keys.remove(&ch); + } + }, + WindowEvent::Focused(false) => { + self.keyboard_state.clear_keys(); + }, + _ => { }, + } + }, + _ => { } + } + + } } fn update_mouse_cursor(window: &Window, old: &MouseCursor, new: &MouseCursor) { From 2a757988d2f470b6d527188986bd857762c26d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 13 Jul 2018 06:03:02 +0200 Subject: [PATCH 140/868] Fixed HiDPI issue on Linux --- examples/debug.rs | 2 +- src/app.rs | 1 - src/window.rs | 16 +++++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 37482bebd..c1ce4ee07 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -34,7 +34,7 @@ impl Layout for MyAppData { } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file - Button::with_label("Azul App").dom() + Button::with_label("SVG Datei öffnen...").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } diff --git a/src/app.rs b/src/app.rs index 178fed86b..ee8616b8a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -626,7 +626,6 @@ fn render( let LogicalSize { width, height } = window.state.size.dimensions; let layout_size = TypedSize2D::new(width as f32, height as f32); - let framebuffer_size_physical = window.state.size.dimensions.to_physical(window.state.size.hidpi_factor); let framebuffer_size = TypedSize2D::new(framebuffer_size_physical.width as u32, framebuffer_size_physical.height as u32); diff --git a/src/window.rs b/src/window.rs index 7a5acb147..2990961ca 100644 --- a/src/window.rs +++ b/src/window.rs @@ -542,7 +542,7 @@ pub(crate) struct WindowInternal { impl Window { /// Creates a new window - pub fn new(options: WindowCreateOptions, css: Css) -> Result { + pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { use glium::glutin::dpi::{LogicalPosition, LogicalSize}; @@ -554,6 +554,7 @@ impl Window { }; let hidpi_factor = monitor.get_hidpi_factor(); + options.state.size.hidpi_factor = hidpi_factor; let mut window = WindowBuilder::new() .with_dimensions(options.state.size.dimensions) @@ -592,13 +593,13 @@ impl Window { }) .with_gl_profile(GlProfile::Core); - #[cfg(debug_assertions)] { + /*#[cfg(debug_assertions)] { builder = builder.with_gl_debug_flag(true); } - #[cfg(not(debug_assertions))] { + #[cfg(not(debug_assertions))] {*/ builder = builder.with_gl_debug_flag(false); - } + // } if vsync { builder = builder.with_vsync(true); @@ -620,9 +621,9 @@ impl Window { gl_window.window().set_position(pos); } - #[cfg(debug_assertions)] + /*#[cfg(debug_assertions)] let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; - #[cfg(not(debug_assertions))] + #[cfg(not(debug_assertions))]*/ let display = Display::with_debug(gl_window, DebugCallbackBehavior::Ignore)?; let device_pixel_ratio = display.gl_window().get_hidpi_factor(); @@ -685,6 +686,7 @@ impl Window { let epoch = Epoch(0); let pipeline_id = PipelineId(0, 0); let layout_size = framebuffer_size.to_f32() / TypedScale::new(device_pixel_ratio as f32); + /* let (sender, receiver) = channel(); let thread = Builder::new().name(options.title.clone()).spawn(move || Self::handle_event(receiver))?; @@ -699,7 +701,7 @@ impl Window { solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); renderer.set_external_image_handler(Box::new(Compositor::default())); - + let window = Window { events_loop: events_loop, state: options.state, From b9dfde4192f86370d19a7abb0c35483f6cfa2c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 13 Jul 2018 17:14:20 +0200 Subject: [PATCH 141/868] Refactored widgets into their own files Merged SVG widget and implementation because it's annoying constantly having to switch back and forth between the two files to adjust shaders --- src/app_state.rs | 1 - src/dom.rs | 1 - src/lib.rs | 10 +- src/resources.rs | 1 - src/widgets.rs | 250 --------------------------------------- src/widgets/button.rs | 52 ++++++++ src/widgets/label.rs | 31 +++++ src/widgets/mod.rs | 8 ++ src/{ => widgets}/svg.rs | 154 ++++++++++++++++++++++-- 9 files changed, 241 insertions(+), 267 deletions(-) delete mode 100644 src/widgets.rs create mode 100644 src/widgets/button.rs create mode 100644 src/widgets/label.rs create mode 100644 src/widgets/mod.rs rename src/{ => widgets}/svg.rs (87%) diff --git a/src/app_state.rs b/src/app_state.rs index c98567062..23ec22f72 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -15,7 +15,6 @@ use { resources::AppResources, images::ImageType, font::FontError, - svg::{SvgLayerId, SvgLayer, SvgParseError}, css_parser::{Font as FontId, FontSize, PixelValue}, errors::ClipboardError, }; diff --git a/src/dom.rs b/src/dom.rs index 708fe907f..15ce1e1e1 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -10,7 +10,6 @@ use webrender::api::ColorU; use glium::{Texture2d, framebuffer::SimpleFrameBuffer}; use { window::WindowEvent, - svg::SvgLayerId, images::ImageId, cache::DomHash, text_cache::TextId, diff --git a/src/lib.rs b/src/lib.rs index e2bb7ad52..b8453e8a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,8 +65,6 @@ pub mod traits; pub mod window; /// Async IO / task system pub mod task; -/// SVG / path flattering module (lyon) -pub mod svg; /// Built-in widgets pub mod widgets; /// Bindings to the native file-chooser, color picker, etc. dialogs @@ -142,8 +140,6 @@ pub mod prelude { Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, ExtendMode, PixelValue, PercentageValue, }; - - pub use svg::{SvgLayerId, SvgLayer, SvgCache}; pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; } @@ -158,10 +154,12 @@ pub mod errors { }; pub use simplecss::Error as CssSyntaxError; pub use css::{CssParseError, DynamicCssParseError}; - pub use svg::SvgParseError; pub use font::FontError; - pub use window::WindowCreateError; pub use image::ImageError; + // TODO: re-export the sub-types of ClipboardError! pub use clipboard2::ClipboardError; + + pub use window::WindowCreateError; + pub use widgets::svg::SvgParseError; } diff --git a/src/resources.rs b/src/resources.rs index aa8423513..59ae6edfb 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -19,7 +19,6 @@ use std::collections::hash_map::Entry::*; use app_units::Au; use css_parser; use css_parser::Font::ExternalFont; -use svg::{SvgLayerId, SvgLayer, SvgParseError}; use text_cache::TextId; use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; diff --git a/src/widgets.rs b/src/widgets.rs deleted file mode 100644 index 05ef85cb9..000000000 --- a/src/widgets.rs +++ /dev/null @@ -1,250 +0,0 @@ -use { - svg::{SvgCache, SvgLayerId}, - window::ReadOnlyWindow, - traits::Layout, - dom::{Dom, NodeType}, - images::ImageId, -}; - -// --- button - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Button { - pub content: ButtonContent, -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum ButtonContent { - Image(ImageId), - // Buttons should only contain short amounts of text - Text(String), -} - -impl Button { - pub fn with_label(text: S) - -> Self where S: Into - { - Self { - content: ButtonContent::Text(text.into()), - } - } - - pub fn with_image(image: ImageId) - -> Self - { - Self { - content: ButtonContent::Image(image), - } - } - - pub fn dom(self) - -> Dom where T: Layout - { - use self::ButtonContent::*; - let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); - button_root.add_child(match self.content { - Text(s) => Dom::new(NodeType::Label(s)), - Image(i) => Dom::new(NodeType::Image(i)), - }); - button_root - } -} - -// --- svg - -#[derive(Debug, Clone, PartialEq)] -pub struct Svg { - /// Currently active layers - pub layers: Vec, - /// Pan (horizontal, vertical) in pixels - pub pan: (f32, f32), - /// 1.0 = default zoom - pub zoom: f32, - /// Whether an FXAA shader should be applied to the resulting OpenGL texture - pub enable_fxaa: bool, -} - -impl Default for Svg { - fn default() -> Self { - Self { - layers: Vec::new(), - pan: (0.0, 0.0), - zoom: 1.0, - enable_fxaa: false, - } - } -} - -use glium::{Texture2d, draw_parameters::DrawParameters, - index::PrimitiveType, IndexBuffer, Surface}; -use std::sync::Mutex; -use svg::{SvgVert, SvgWorldPixel, SvgShader}; -use webrender::api::{ColorU, ColorF}; -use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; - -impl Svg { - - #[inline] - pub fn with_layers(layers: Vec) - -> Self - { - Self { layers: layers, .. Default::default() } - } - - #[inline] - pub fn with_pan(mut self, horz: f32, vert: f32) - -> Self - { - self.pan = (horz, vert); - self - } - - #[inline] - pub fn with_zoom(mut self, zoom: f32) - -> Self - { - self.zoom = zoom; - self - } - - #[inline] - pub fn with_fxaa(mut self, enable_fxaa: bool) - -> Self - { - self.enable_fxaa = enable_fxaa; - self - } - - /// Renders the SVG to an OpenGL texture and creates the DOM - pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) - -> Dom where T: Layout - { - const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; - - let (window_width, window_height) = window.get_physical_size(); - let tex = window.create_texture(window_width as u32, window_height as u32); - tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); - - let draw_options = DrawParameters { - primitive_restart_index: true, - .. Default::default() - }; - - let z_index: f32 = 0.5; - let bbox: TypedRect = TypedRect { - origin: TypedPoint2D::new(0.0, 0.0), - size: TypedSize2D::new(window_width as f32, window_height as f32), - }; - let shader = svg_cache.init_shader(window); - - { - let mut surface = tex.as_surface(); - - for layer_id in &self.layers { - - use palette::Srgba; - let style = svg_cache.get_style(layer_id); - - if let Some(color) = style.fill { - let color: ColorF = color.into(); - let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); - let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, - color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; - - surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); - } - - if let Some((stroke_color, _)) = style.stroke { - let stroke_color: ColorF = stroke_color.into(); - let (stroke_vertex_buffer, stroke_index_buffer) = svg_cache.get_stroke_vertices_and_indices(window, layer_id); - let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - stroke_color.color.red as f32, - stroke_color.color.green as f32, - stroke_color.color.blue as f32, - stroke_color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; - - surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); - } - } - } - - if self.enable_fxaa { - // TODO: apply FXAA shader - } - - Dom::new(NodeType::GlTexture(tex)) - } -} - -// --- label - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct Label { - pub text: String, -} - -impl Label { - pub fn new(text: S) - -> Self where S: Into - { - Self { text: text.into() } - } - - pub fn dom(self) - -> Dom where T: Layout - { - Dom::new(NodeType::Label(self.text)) - } -} - -// -- checkbox (TODO) - -/// State of a checkbox (disabled, checked, etc.) -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub enum CheckboxState { - /// `[■]` - Active, - /// `[✔]` - Checked, - /// Greyed out checkbox - Disabled { - /// Should the checkbox fire on a mouseover / mouseup, etc. event - /// - /// This can be useful for showing warnings / tooltips / help messages - /// as to why this checkbox is disabled - fire_on_click: bool, - }, - /// `[ ]` - Unchecked -} - -// Empty test, for some reason codecov doesn't detect any files (and therefore -// doesn't report codecov % correctly) except if they have at least one test in -// the file. This is an empty test, which should be updated later on -#[test] -fn __codecov_test_widgets_file() { - -} \ No newline at end of file diff --git a/src/widgets/button.rs b/src/widgets/button.rs new file mode 100644 index 000000000..600f040d8 --- /dev/null +++ b/src/widgets/button.rs @@ -0,0 +1,52 @@ +use { + traits::Layout, + dom::{Dom, NodeType}, + images::ImageId, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Button { + pub content: ButtonContent, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum ButtonContent { + Image(ImageId), + // Buttons should only contain short amounts of text + Text(String), +} + +impl Button { + pub fn with_label(text: S) + -> Self where S: Into + { + Self { + content: ButtonContent::Text(text.into()), + } + } + + pub fn with_image(image: ImageId) + -> Self + { + Self { + content: ButtonContent::Image(image), + } + } + + pub fn dom(self) + -> Dom where T: Layout + { + use self::ButtonContent::*; + let mut button_root = Dom::new(NodeType::Div).with_class("__azul-native-button"); + button_root.add_child(match self.content { + Text(s) => Dom::new(NodeType::Label(s)), + Image(i) => Dom::new(NodeType::Image(i)), + }); + button_root + } +} + +#[test] +fn __codecov_test_widget_button_file() { + +} \ No newline at end of file diff --git a/src/widgets/label.rs b/src/widgets/label.rs new file mode 100644 index 000000000..158f81e79 --- /dev/null +++ b/src/widgets/label.rs @@ -0,0 +1,31 @@ +use { + traits::Layout, + dom::{Dom, NodeType}, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Label { + pub text: String, +} + +impl Label { + pub fn new(text: S) + -> Self where S: Into + { + Self { text: text.into() } + } + + pub fn dom(self) + -> Dom where T: Layout + { + Dom::new(NodeType::Label(self.text)) + } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_widgets_label_file() { + +} \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 000000000..668748757 --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,8 @@ +pub mod svg; +pub mod button; +pub mod label; + +// Re-export widgets +pub use self::svg::{Svg, SvgLayerId, SvgLayer, SvgCache}; +pub use self::button::{Button, ButtonContent}; +pub use self::label::Label; \ No newline at end of file diff --git a/src/svg.rs b/src/widgets/svg.rs similarity index 87% rename from src/svg.rs rename to src/widgets/svg.rs index 907fcce3f..95d972221 100644 --- a/src/svg.rs +++ b/src/widgets/svg.rs @@ -7,9 +7,9 @@ use std::{ hash::{Hash, Hasher}, }; use glium::{ - backend::Facade, + backend::Facade, index::PrimitiveType, DrawParameters, IndexBuffer, VertexBuffer, Display, - Texture2d, Program, Api, + Texture2d, Program, Api, Surface, }; use lyon::{ tessellation::{ @@ -30,9 +30,10 @@ use resvg::usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF}; use { FastHashMap, - dom::Callback, + dom::{Dom, NodeType, Callback}, traits::Layout, id_tree::NonZeroUsizeHack, + window::ReadOnlyWindow, }; /// In order to store / compare SVG files, we have to @@ -892,7 +893,7 @@ mod svg_to_lyon { }; use resvg::usvg::{self, ViewBox, Transform, Tree, Path, PathSegment, Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; - use svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, + use widgets::svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, SvgLayerType, SvgStyle, SvgCallbacks, SvgParseError, SvgTransformId, new_svg_transform_id, new_view_box_id, SvgViewBoxId, LayerType}; use traits::Layout; @@ -1016,10 +1017,147 @@ mod svg_to_lyon { } } -// Empty test, for some reason codecov doesn't detect any files (and therefore -// doesn't report codecov % correctly) except if they have at least one test in -// the file. This is an empty test, which should be updated later on +#[derive(Debug, Clone, PartialEq)] +pub struct Svg { + /// Currently active layers + pub layers: Vec, + /// Pan (horizontal, vertical) in pixels + pub pan: (f32, f32), + /// 1.0 = default zoom + pub zoom: f32, + /// Whether an FXAA shader should be applied to the resulting OpenGL texture + pub enable_fxaa: bool, +} + +impl Default for Svg { + fn default() -> Self { + Self { + layers: Vec::new(), + pan: (0.0, 0.0), + zoom: 1.0, + enable_fxaa: false, + } + } +} + +impl Svg { + + #[inline] + pub fn with_layers(layers: Vec) + -> Self + { + Self { layers: layers, .. Default::default() } + } + + #[inline] + pub fn with_pan(mut self, horz: f32, vert: f32) + -> Self + { + self.pan = (horz, vert); + self + } + + #[inline] + pub fn with_zoom(mut self, zoom: f32) + -> Self + { + self.zoom = zoom; + self + } + + #[inline] + pub fn with_fxaa(mut self, enable_fxaa: bool) + -> Self + { + self.enable_fxaa = enable_fxaa; + self + } + + /// Renders the SVG to an OpenGL texture and creates the DOM + pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) + -> Dom where T: Layout + { + const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; + + let (window_width, window_height) = window.get_physical_size(); + let tex = window.create_texture(window_width as u32, window_height as u32); + tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); + + let draw_options = DrawParameters { + primitive_restart_index: true, + .. Default::default() + }; + + let z_index: f32 = 0.5; + let bbox: TypedRect = TypedRect { + origin: TypedPoint2D::new(0.0, 0.0), + size: TypedSize2D::new(window_width as f32, window_height as f32), + }; + let shader = svg_cache.init_shader(window); + + { + let mut surface = tex.as_surface(); + + for layer_id in &self.layers { + + use palette::Srgba; + let style = svg_cache.get_style(layer_id); + + if let Some(color) = style.fill { + let color: ColorF = color.into(); + let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); + let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, + color.alpha as f32 + ), + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, + }; + + surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + } + + if let Some((stroke_color, _)) = style.stroke { + let stroke_color: ColorF = stroke_color.into(); + let (stroke_vertex_buffer, stroke_index_buffer) = svg_cache.get_stroke_vertices_and_indices(window, layer_id); + let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + stroke_color.color.red as f32, + stroke_color.color.green as f32, + stroke_color.color.blue as f32, + stroke_color.alpha as f32 + ), + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, + }; + + surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + } + } + } + + if self.enable_fxaa { + // TODO: apply FXAA shader + } + + Dom::new(NodeType::GlTexture(tex)) + } +} + #[test] -fn __codecov_test_svg_file() { +fn __codecov_test_widget_svg_file() { } \ No newline at end of file From edafb6c1bcc396f61b5884bdaa41e64818de427d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 13 Jul 2018 19:33:23 +0200 Subject: [PATCH 142/868] Fixed styles on Linux + added SVG layer ID generator --- src/styles/native_linux.css | 4 ++-- src/widgets/svg.rs | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/styles/native_linux.css b/src/styles/native_linux.css index 72140ee2f..42a3b7877 100644 --- a/src/styles/native_linux.css +++ b/src/styles/native_linux.css @@ -1,6 +1,6 @@ .__azul-native-button { border: 1px solid #b7b7b7; - border-radius: 5.5px; + border-radius: 4px; box-shadow: 0px 0px 3px #c5c5c5ad; background: linear-gradient(#fcfcfc, #efefef); width: [[ my_id | 200px ]]; @@ -12,7 +12,7 @@ } * { - font-size: 14px; + font-size: 10px; font-family: sans-serif; color: #4c4c4c; background-color: #e7e7e7; diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 95d972221..328d03eaa 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -36,20 +36,17 @@ use { window::ReadOnlyWindow, }; -/// In order to store / compare SVG files, we have to -pub(crate) static SVG_BLOB_ID: AtomicUsize = AtomicUsize::new(0); +static SVG_LAYER_ID: AtomicUsize = AtomicUsize::new(0); +static SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); +static SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct SvgTransformId(NonZeroUsizeHack); -const SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); - pub fn new_svg_transform_id() -> SvgTransformId { SvgTransformId(NonZeroUsizeHack::new(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst))) } -const SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct SvgViewBoxId(usize); @@ -57,6 +54,13 @@ pub fn new_view_box_id() -> SvgViewBoxId { SvgViewBoxId(SVG_VIEW_BOX_ID.fetch_add(1, Ordering::SeqCst)) } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct SvgLayerId(usize); + +pub fn new_svg_layer_id() -> SvgLayerId { + SvgLayerId(SVG_LAYER_ID.fetch_add(1, Ordering::SeqCst)) +} + const SHADER_VERSION_GL: &str = "#version 150"; const SHADER_VERSION_GLES: &str = "#version 300 es"; @@ -257,9 +261,6 @@ const SVG_FXAA_FRAG_SHADER: &str = " } "; -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct SvgLayerId(usize); - #[derive(Debug, Clone)] pub struct SvgShader { pub program: Rc, @@ -393,7 +394,7 @@ impl SvgCache { pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { // TODO: set tolerance based on zoom - let new_svg_id = SvgLayerId(SVG_BLOB_ID.fetch_add(1, Ordering::SeqCst)); + let new_svg_id = new_svg_layer_id(); let ((vertex_buf, index_buf), opt_stroke) = tesselate_layer_data(&layer.data, 0.01, layer.style.stroke.and_then(|s| Some(s.1.clone()))); From 08e684ad87382f9cc4bebae289a37820f32f8e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 13 Jul 2018 22:15:38 +0200 Subject: [PATCH 143/868] Added vectorization for glyphs SvgLayerType::Text now stores the path events for a single character --- Cargo.toml | 2 +- src/widgets/svg.rs | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eee61609c..7a901f748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ glium = "0.22.0" gleam = "0.5" euclid = "0.17" image = "0.19.0" -rusttype = "0.6.2" +rusttype = { git = "https://github.com/fschutt/rusttype" } app_units = "0.6" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 328d03eaa..08ed269b3 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -738,12 +738,15 @@ impl Into for SvgLineJoin { /// One "layer" is simply one or more polygons that get drawn using the same style /// i.e. one SVG `` element +/// +/// Note: If you want to draw text in a SVG element, you need to convert the character +/// of the font to a `Vec), Circle(SvgCircle), Rect(SvgRect), - Text(String), + Text(Vec), } #[derive(Debug, Copy, Clone)] @@ -757,8 +760,25 @@ implement_vertex!(SvgVert, xy, normal); #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; +use rusttype::{Font, GlyphId}; impl SvgLayerType { + + pub fn from_character(ch: char, font: &Font) -> (GlyphId, Self) { + let glyph = font.glyph(ch); + let path_events = glyph + .standalone() + .get_data() + .unwrap().shape + .as_ref() + .unwrap() + .iter() + .map(svg_to_lyon::glyph_to_path_events) + .collect(); + + (glyph.id(), SvgLayerType::Text(path_events)) + } + pub fn tesselate(&self, tolerance: f32, stroke: Option) -> (VertexBuffers, Option>) { @@ -770,7 +790,7 @@ impl SvgLayerType { }); match self { - SvgLayerType::Polygon(p) => { + SvgLayerType::Polygon(p) | SvgLayerType::Text(p) => { let mut builder = Builder::with_capacity(p.len()).flattened(tolerance); for event in p { builder.path_event(*event); @@ -855,8 +875,7 @@ impl SvgLayerType { } )); } - }, - SvgLayerType::Text(_t) => { }, + } } if stroke.is_some() { @@ -900,6 +919,7 @@ mod svg_to_lyon { use traits::Layout; use webrender::api::ColorU; use FastHashMap; + use rusttype::Vertex; pub fn parse_from, T: Layout>(svg_source: S, view_boxes: &mut FastHashMap) -> Result<(Vec>, FastHashMap), SvgParseError> { @@ -1016,6 +1036,18 @@ mod svg_to_lyon { a: (s.opacity.value() * 255.0) as u8 }, opts) } + + // Convert a Rusttype glyph to a Vec of PathEvents, + // in order to turn a glyph into a polygon + pub fn glyph_to_path_events(vertex: &Vertex) + -> PathEvent + { use rusttype::VertexType; + match vertex.vertex_type() { + VertexType::CurveTo => PathEvent::QuadraticTo(Point::new(vertex.cx as f32, vertex.cy as f32), Point::new(vertex.x as f32, vertex.y as f32)), + VertexType::MoveTo => PathEvent::MoveTo(Point::new(vertex.x as f32, vertex.y as f32)), + VertexType::LineTo => PathEvent::LineTo(Point::new(vertex.x as f32, vertex.y as f32)), + } + } } #[derive(Debug, Clone, PartialEq)] From 9f02f2d6d1ebb6123013fb641e73b7556d2862c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 14 Jul 2018 01:56:01 +0200 Subject: [PATCH 144/868] Added storage for font bytes + API to query font --- src/app.rs | 2 +- src/app_state.rs | 8 +++++++- src/css_parser.rs | 21 +++++++++++---------- src/display_list.rs | 15 ++++++++------- src/font.rs | 6 +++--- src/lib.rs | 3 ++- src/resources.rs | 27 ++++++++++++++++----------- src/text_cache.rs | 4 ++-- src/text_layout.rs | 2 +- src/widgets/svg.rs | 9 +++++---- 10 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/app.rs b/src/app.rs index ee8616b8a..a43451e43 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,7 +12,7 @@ use { images::ImageType, errors::{FontError, ClipboardError}, window::{Window, WindowCreateOptions, WindowCreateError, WindowId}, - css_parser::{Font as FontId, PixelValue, FontSize}, + css_parser::{FontId, PixelValue, FontSize}, text_cache::TextId, dom::UpdateScreen, window::FakeWindow, diff --git a/src/app_state.rs b/src/app_state.rs index 23ec22f72..10e5a2f7d 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, Mutex}, }; use image::ImageError; +use rusttype::Font; use { FastHashMap, text_cache::TextId, @@ -15,7 +16,7 @@ use { resources::AppResources, images::ImageType, font::FontError, - css_parser::{Font as FontId, FontSize, PixelValue}, + css_parser::{FontId, FontSize, PixelValue}, errors::ClipboardError, }; @@ -82,6 +83,7 @@ impl<'a, T: Layout> AppState<'a, T> { { self.resources.add_image(id, data, image_type) } + /// Checks if an image is currently registered and ready-to-use pub fn has_image>(&mut self, id: S) -> bool @@ -151,6 +153,10 @@ impl<'a, T: Layout> AppState<'a, T> { self.resources.has_font(id) } + pub fn get_font<'b>(&'b self, id: &FontId) -> Option<(&'b Font<'a>, &'b Vec)> { + self.resources.get_font(id) + } + /// Deletes a font from the internal app resources. /// /// ## Arguments diff --git a/src/css_parser.rs b/src/css_parser.rs index 614c8405a..2d2ab08b2 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1681,11 +1681,11 @@ typed_pixel_value_parser!(parse_css_font_size, FontSize); #[derive(Debug, PartialEq, Clone)] pub struct FontFamily { // parsed fonts, in order, i.e. "Webly Sleeky UI", "monospace", etc. - pub(crate) fonts: Vec + pub(crate) fonts: Vec } #[derive(Debug, PartialEq, Eq, Clone, Hash)] -pub enum Font { +pub enum FontId { BuiltinFont(&'static str), ExternalFont(String), } @@ -1722,12 +1722,14 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result fonts.push(Font::BuiltinFont("serif")), - "sans-serif" => fonts.push(Font::BuiltinFont("sans-serif")), - "monospace" => fonts.push(Font::BuiltinFont("monospace")), + "serif" => fonts.push(FontId::BuiltinFont("serif")), + "sans-serif" => fonts.push(FontId::BuiltinFont("sans-serif")), + "monospace" => fonts.push(FontId::BuiltinFont("monospace")), + "fantasy" => fonts.push(FontId::BuiltinFont("fantasy")), + "cursive" => fonts.push(FontId::BuiltinFont("cursive")), _ => return Err(CssFontFamilyParseError::UnrecognizedBuiltinFont(font)), } } @@ -2146,8 +2148,8 @@ mod css_tests { fn test_parse_css_font_family_1() { assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { fonts: vec![ - Font::ExternalFont("Webly Sleeky UI".into()), - Font::BuiltinFont("monospace"), + FontId::ExternalFont("Webly Sleeky UI".into()), + FontId::BuiltinFont("monospace"), ] })); } @@ -2156,10 +2158,9 @@ mod css_tests { fn test_parse_css_font_family_2() { assert_eq!(parse_css_font_family("'Webly Sleeky UI'"), Ok(FontFamily { fonts: vec![ - Font::ExternalFont("Webly Sleeky UI".into()), + FontId::ExternalFont("Webly Sleeky UI".into()), ] })); - } #[test] diff --git a/src/display_list.rs b/src/display_list.rs index 63401bc66..29ea54d36 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -31,7 +31,7 @@ use { }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); -const DEFAULT_BUILTIN_FONT_SANS_SERIF: css_parser::Font = Font::BuiltinFont("sans-serif"); +const DEFAULT_BUILTIN_FONT_SANS_SERIF: FontId = FontId::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: Layout + 'a> { pub(crate) ui_descr: &'a UiDescription, @@ -201,12 +201,13 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { resource_updates: &mut Vec) { use font::FontState; + use css_parser::FontId; - let mut updated_fonts = Vec::<(::css_parser::Font, Vec)>::new(); - let mut to_delete_fonts = Vec::<(::css_parser::Font, Option<(FontKey, Vec)>)>::new(); + let mut updated_fonts = Vec::<(FontId, Vec)>::new(); + let mut to_delete_fonts = Vec::<(FontId, Option<(FontKey, Vec)>)>::new(); for (key, value) in app_resources.font_data.iter() { - match value.1 { + match value.2 { FontState::ReadyForUpload(ref bytes) => { updated_fonts.push((key.clone(), bytes.clone())); }, @@ -238,7 +239,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { for (resource_key, data) in updated_fonts.into_iter() { let key = api.generate_font_key(); resource_updates.push(ResourceUpdate::AddFont(AddFont::Raw(key, data, 0))); // TODO: use the index better? - app_resources.font_data.get_mut(&resource_key).unwrap().1 = FontState::Uploaded(key); + app_resources.font_data.get_mut(&resource_key).unwrap().2 = FontState::Uploaded(key); } } @@ -879,7 +880,7 @@ fn push_border( #[inline] fn push_font( - font_id: &css_parser::Font, + font_id: &FontId, font_size_app_units: Au, resource_updates: &mut Vec, app_resources: &mut AppResources, @@ -893,7 +894,7 @@ fn push_font( return None; } - let &(ref font, ref font_state) = match app_resources.font_data.get(font_id) { + let &(ref font, _, ref font_state) = match app_resources.font_data.get(font_id) { Some(f) => f, None => return None, }; diff --git a/src/font.rs b/src/font.rs index 5515fa0b7..9c5024ec6 100644 --- a/src/font.rs +++ b/src/font.rs @@ -35,10 +35,10 @@ impl From for FontError { } /// Read font data to get font information, v_metrics, glyph info etc. -pub(crate) fn rusttype_load_font<'a>(data: Vec, index: Option) -> Result, FontError> { - let collection = FontCollection::from_bytes(data)?; +pub(crate) fn rusttype_load_font<'a>(data: Vec, index: Option) -> Result<(Font<'a>, Vec), FontError> { + let collection = FontCollection::from_bytes(data.clone())?; let font = collection.clone().into_font().unwrap_or(collection.font_at(index.and_then(|i| Some(i as usize)).unwrap_or(0))?); - Ok(font) + Ok((font, data)) } // Empty test, for some reason codecov doesn't detect any files (and therefore diff --git a/src/lib.rs b/src/lib.rs index b8453e8a0..559caf5d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,13 +134,14 @@ pub mod prelude { LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, LayoutMaxHeight, LayoutWrap, LayoutDirection, LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, - LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, + LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, FontId, LayoutPixel, TypedSize2D, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, ExtendMode, PixelValue, PercentageValue, }; pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; + pub use rusttype::Font; } /// Re-exports of errors diff --git a/src/resources.rs b/src/resources.rs index 59ae6edfb..0c90d3f4e 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -18,9 +18,10 @@ use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; use std::collections::hash_map::Entry::*; use app_units::Au; use css_parser; -use css_parser::Font::ExternalFont; +use css_parser::FontId::{self, ExternalFont}; use text_cache::TextId; use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; +use rusttype::Font; /// Font and image keys /// @@ -46,7 +47,7 @@ pub(crate) struct AppResources<'a> { // but we also need access to the font metrics. So we first parse the font // to make sure that nothing is going wrong. In the next draw call, we // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: FastHashMap, FontState)>, + pub(crate) font_data: FastHashMap, Vec, FontState)>, // After we've looked up the FontKey in the font_data map, we can then access // the font instance key (if there is any). If there is no font instance key, // we first need to create one. @@ -73,16 +74,16 @@ impl<'a> Default for AppResources<'a> { } } -fn load_system_fonts<'a>(fonts: &mut FastHashMap, FontState)>) { +fn load_system_fonts<'a>(fonts: &mut FastHashMap, Vec, FontState)>) { use font_loader::system_fonts::{self, FontPropertyBuilder}; - use css_parser::Font::BuiltinFont; + use css_parser::FontId::BuiltinFont; use font::rusttype_load_font; - fn insert_font<'b>(fonts: &mut FastHashMap, FontState)>, target: &'static str) { + fn insert_font<'b>(fonts: &mut FastHashMap, Vec, FontState)>, target: &'static str) { if let Some((font_bytes, idx)) = system_fonts::get(&FontPropertyBuilder::new().family(target).build()) { match rusttype_load_font(font_bytes.clone(), Some(idx)) { - Ok(f) => { fonts.insert(BuiltinFont(target), (f, FontState::ReadyForUpload(font_bytes))); }, + Ok((f, b)) => { fonts.insert(BuiltinFont(target), (f, b, FontState::ReadyForUpload(font_bytes))); }, Err(e) => println!("error loading {} font: {:?}", target, e), } } @@ -170,13 +171,17 @@ impl<'a> AppResources<'a> { Vacant(v) => { let mut font_data = Vec::::new(); data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; - let parsed_font = font::rusttype_load_font(font_data.clone(), None)?; - v.insert((parsed_font, FontState::ReadyForUpload(font_data))); + let (parsed_font, fd) = font::rusttype_load_font(font_data.clone(), None)?; + v.insert((parsed_font, fd, FontState::ReadyForUpload(font_data))); Ok(Some(())) }, } } + pub fn get_font<'b>(&'b self, id: &FontId) -> Option<(&'b Font<'a>, &'b Vec)> { + self.font_data.get(id).and_then(|(font, bytes, _)| Some((font, bytes))) + } + /// Checks if a font is currently registered and ready-to-use pub(crate) fn has_font>(&mut self, id: S) -> bool @@ -192,13 +197,13 @@ impl<'a> AppResources<'a> { match self.font_data.get_mut(&ExternalFont(id.into())) { None => None, Some(v) => { - let to_delete_font_key = match v.1 { + let to_delete_font_key = match v.2 { FontState::Uploaded(ref font_key) => { Some(font_key.clone()) }, _ => None, }; - v.1 = FontState::AboutToBeDeleted(to_delete_font_key); + v.2 = FontState::AboutToBeDeleted(to_delete_font_key); Some(()) } } @@ -214,7 +219,7 @@ impl<'a> AppResources<'a> { /// Calculates the widths for the words, then stores the widths of the words + the actual words /// /// This leads to a faster layout cycle, but has an upfront performance cost - pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &css_parser::Font, font_size: FontSize) + pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) -> TextId { use rusttype::Scale; diff --git a/src/text_cache.rs b/src/text_cache.rs index 74d5e44e3..9b1224425 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -4,7 +4,7 @@ use std::{ }; use { FastHashMap, - css_parser::{Font, FontSize}, + css_parser::{FontId, FontSize}, text_layout::SemanticWordItem, }; @@ -29,7 +29,7 @@ pub(crate) enum LargeString { /// The `Vec` stores the individual word, so we don't need /// to store it again. The `words` is stored in an Rc, so that we don't need to /// duplicate it for every font size. - Cached { font: Font, size: FontSize, words: Rc> }, + Cached { font: FontId, size: FontSize, words: Rc> }, } /// Cache for accessing large amounts of text diff --git a/src/text_layout.rs b/src/text_layout.rs index 978a1ed37..573320881 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -8,7 +8,7 @@ use { display_list::TextInfo, css_parser::{ TextAlignmentHorz, FontSize, BackgroundColor, - Font as FontId, TextAlignmentVert, LineHeight, LayoutOverflow + FontId, TextAlignmentVert, LineHeight, LayoutOverflow }, }; diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 08ed269b3..dc9c07d65 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -312,12 +312,13 @@ impl Default for SvgCache { impl SvgCache { + /// Creates an empty SVG cache pub fn empty() -> Self { Self::default() } /// Builds and compiles the SVG shader if the shader isn't already present - pub fn init_shader(&self, display: &F) -> SvgShader { + fn init_shader(&self, display: &F) -> SvgShader { let mut shader_lock = self.shader.lock().unwrap(); if shader_lock.is_none() { *shader_lock = Some(SvgShader::new(display)); @@ -325,7 +326,7 @@ impl SvgCache { shader_lock.as_ref().and_then(|s| Some(s.clone())).unwrap() } - pub fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) -> &'a (VertexBuffer, IndexBuffer) { use std::collections::hash_map::Entry::*; @@ -346,7 +347,7 @@ impl SvgCache { /// /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` /// in sync, a panic should never happen - pub fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) + fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) -> &'a (VertexBuffer, IndexBuffer) { use std::collections::hash_map::Entry::*; @@ -373,7 +374,7 @@ impl SvgCache { }) } - pub fn get_style(&self, id: &SvgLayerId) + fn get_style(&self, id: &SvgLayerId) -> SvgStyle { self.layers.get(id).as_ref().unwrap().style From 3ea2204a1a4ef12be1dc77bb7970d2eb861789ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 14 Jul 2018 05:03:46 +0200 Subject: [PATCH 145/868] Added logging + optional MsgBox on panic --- .travis.yml | 1 + Cargo.toml | 9 +- examples/debug.rs | 2 +- src/app.rs | 48 ++++++++++- src/dialogs.rs | 4 +- src/display_list.rs | 4 +- src/lib.rs | 14 ++- src/logging.rs | 201 ++++++++++++++++++++++++++++++++++++++++++++ src/resources.rs | 2 +- src/window.rs | 2 +- 10 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 src/logging.rs diff --git a/.travis.yml b/.travis.yml index 0a4f2413d..375640b81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ matrix: script: - cargo clean - cargo build --verbose --examples + - cargo build --no-default-features - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" matrix: diff --git a/Cargo.toml b/Cargo.toml index 7a901f748..61b52cc2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,9 @@ palette = "0.4.0" tinyfiledialogs = "3.3.5" clipboard2 = "0.1.0" font-loader = "0.7.0" +log = "0.4.1" +fern = { version = "0.5.5", optional = true } +backtrace = { version = "0.3.9", optional = true } [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" @@ -46,6 +49,8 @@ git = "https://github.com/servo/webrender" rev = "091e9c53acd71fea20da18ea3a3d5eec7ac6c7d5" [features] +default = ["logging"] + # The reason we do this is because doctests don't get cfg(test) # See: https://github.com/rust-lang/cargo/issues/4669 doc-test = [] @@ -54,4 +59,6 @@ doc-test = [] # use OpenGL 3.2, so the tests will fail # # To actually test the library, run cargo --test --features=doc-test -no-opengl-tests = [] \ No newline at end of file +no-opengl-tests = [] + +logging = ["fern", "backtrace"] \ No newline at end of file diff --git a/examples/debug.rs b/examples/debug.rs index c1ce4ee07..4bf42c54b 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -85,7 +85,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { - let mut app = App::new(MyAppData { map: None }); + let mut app = App::new(MyAppData { map: None }, AppConfig::default()); app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index a43451e43..0775f3778 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,8 @@ use glium::glutin::dpi::{LogicalPosition, LogicalSize}; use webrender::api::{RenderApi, HitTestFlags, DevicePixel}; use image::ImageError; use euclid::{TypedScale, TypedSize2D}; +#[cfg(feature = "logging")] +use log::LevelFilter; use { images::ImageType, errors::{FontError, ClipboardError}, @@ -85,10 +87,52 @@ impl Default for FrameEventInfo { } } +#[derive(Debug, Clone)] +#[cfg_attr(not(feature = "logging"), derive(Copy))] +pub struct AppConfig { + /// If enabled, logs error and info messages. + /// + /// Default is `Some(LevelFilter::Error)` to log all errors by default + #[cfg(feature = "logging")] + pub enable_logging: Option, + /// Path to the output log if the logger is enabled + #[cfg(feature = "logging")] + pub log_file_path: Option, + /// If the app crashes / panics, a window with a message box pops up + /// Additionally, the error + backtrace gets logged to the output + /// file (if logging is enabled). + #[cfg(feature = "logging")] + pub enable_visual_panic_hook: bool, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + #[cfg(feature = "logging")] + enable_logging: Some(LevelFilter::Error), + #[cfg(feature = "logging")] + log_file_path: None, + #[cfg(feature = "logging")] + enable_visual_panic_hook: true, + } + } +} + impl<'a, T: Layout> App<'a, T> { + #[allow(unused_variables)] /// Create a new, empty application. This does not open any windows. - pub fn new(initial_data: T) -> Self { + pub fn new(initial_data: T, config: AppConfig) -> Self { + #[cfg(feature = "logging")] { + if let Some(log_level) = config.enable_logging { + ::logging::set_up_logging(config.log_file_path, log_level); + } + + if config.enable_visual_panic_hook { + ::logging::set_up_panic_hooks(); + } + } + Self { windows: Vec::new(), app_state: AppState::new(initial_data), @@ -368,7 +412,7 @@ impl<'a, T: Layout> App<'a, T> { /// # } /// # /// # fn main() { - /// let mut app = App::new(MyAppData { }); + /// let mut app = App::new(MyAppData { }, AppConfig::default()); /// app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); /// app.delete_font("Webly Sleeky UI"); /// // NOTE: The font isn't immediately removed, only in the next draw call diff --git a/src/dialogs.rs b/src/dialogs.rs index bdab3dceb..7823a33b1 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,5 +1,5 @@ -use tinyfiledialogs::MessageBoxIcon; -use tinyfiledialogs::DefaultColorValue; +pub use tinyfiledialogs::MessageBoxIcon; +pub use tinyfiledialogs::DefaultColorValue; /// Ok or cancel result, returned from the `msg_box_ok_cancel` function #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/src/display_list.rs b/src/display_list.rs index 29ea54d36..4c944c849 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -890,7 +890,7 @@ fn push_font( use font::FontState; if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { - eprintln!("warning: too big or too small font size"); + error!("warning: too big or too small font size"); return None; } @@ -923,7 +923,7 @@ fn push_font( Some(*font_instance_key) }, _ => { - eprintln!("warning: trying to use font {:?} that isn't available", font_id); + error!("warning: trying to use font {:?} that isn't available", font_id); None }, } diff --git a/src/lib.rs b/src/lib.rs index 559caf5d4..6d2b27150 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,12 @@ extern crate harfbuzz_rs; extern crate tinyfiledialogs; extern crate clipboard2; extern crate font_loader; +#[macro_use(error, debug, info, log)] +extern crate log; +#[cfg(feature = "logging")] +extern crate fern; +#[cfg(feature = "logging")] +extern crate backtrace; #[cfg(not(target_os = "linux"))] extern crate nfd; @@ -109,6 +115,9 @@ mod compositor; // mod platform_ext; /// Module for caching long texts (including their layout / character positions) across multiple frames mod text_cache; +/// Default logger, can be turned off with `feature = "logging"` +#[cfg(feature = "logging")] +mod logging; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; @@ -116,7 +125,7 @@ type FastHashSet = ::std::collections::HashSet, log_level: LevelFilter) { + + use fern::InitError; + use std::error::Error; + + /// Sets up the global logger + fn set_up_logging_internal(log_file_path: Option, log_level: LevelFilter) + -> Result<(), InitError> + { + + use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + use log::LevelFilter; + use fern::{Dispatch, log_file}; + + let log_location = { + use std::env; + + let mut exe_location = env::current_exe() + .map_err(|_| InitError::Io(IoError::new(IoErrorKind::Other, + "Executable has no executable path (?), can't open log file")))?; + + exe_location.pop(); + exe_location.push(log_file_path.unwrap_or(String::from("error.log"))); + exe_location + }; + + Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}] {}", + record.level(), + record.target(), + message + )) + }) + .level(log_level) + .chain(::std::io::stdout()) + .chain(log_file(log_location)?) + .apply()?; + Ok(()) + } + + match set_up_logging_internal(log_file_path, log_level) { + Ok(_) => { }, + Err(e) => match e { + InitError::Io(e) => { + println!("[WARN] Logging IO init error: \r\nkind: {:?}\r\n\r\ndescription:\r\n{}\r\n\r\ncause:\r\n{:?}\r\n", + e.kind(), e.description(), e.cause()); + }, + InitError::SetLoggerError(e) => { + println!("[WARN] Logging initalization error: \r\ndescription:\r\n{}\r\n\r\ncause:\r\n{:?}\r\n", + e.description(), e.cause()); + } + } + } +} + +/// In the (rare) case of a panic, print it to the stdout, log it to the file and +/// prompt the user with a message box. +pub(crate) fn set_up_panic_hooks() { + + use std::panic::{self, PanicInfo}; + + fn panic_fn(panic_info: &PanicInfo) { + + use std::thread; + + let payload = panic_info.payload(); + let location = panic_info.location(); + + let payload_str = format!("{:?}", payload); + let panic_str = payload.downcast_ref::() + .and_then(|s| Some(s.as_ref())) + .or_else(|| + payload.downcast_ref::<&str>() + .and_then(|s| Some(*s)) + ) + .unwrap_or(payload_str.as_str()); + + let location_str = location.and_then(|loc| Some(format!("{} at line {}", loc.file(), loc.line()))); + let backtrace_str_old = format_backtrace(&Backtrace::new()); + let backtrace_str = backtrace_str_old + .lines() + .filter(|l| !l.is_empty()) + .skip(11) + .collect::>(); + let backtrace_len = backtrace_str.len(); + let mut backtrace_str_new = Vec::<&str>::new(); + + for (i, st) in backtrace_str.into_iter().enumerate() { + if i > backtrace_len - 10 { + break; + } else { + backtrace_str_new.push(st); + } + } + + let backtrace_str = backtrace_str_new.join("\r\n"); + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + + let error_str = format!( + "We are sorry, an unexpected panic ocurred, the program has to exit.\r\n\ + Please report this error and attach the log file found in the directory of the executable.\r\n\ + \r\n\ + The error ocurred in: {} in thread {}\r\n\ + \r\n\ + Error information:\r\n\ + {}\r\n\ + \r\n\ + Backtrace:\r\n\ + \r\n\ + {}\r\n", + location_str.unwrap_or(format!("")), thread_name, panic_str, backtrace_str); + + #[cfg(target_os = "linux")] + let mut error_str_clone = error_str.clone(); + #[cfg(target_os = "linux")] { + error_str_clone = error_str_clone.replace("<", "<"); + error_str_clone = error_str_clone.replace(">", ">"); + } + + // TODO: invoke external app crash handler with the location to the log file + error!("{}", error_str); + + #[cfg(not(target_os = "linux"))] + msg_box_ok("Unexpected fatal error", &error_str, ::tinyfiledialogs::MessageBoxIcon::Error); + #[cfg(target_os = "linux")] + msg_box_ok("Unexpected fatal error", &error_str_clone, ::tinyfiledialogs::MessageBoxIcon::Error); + } + + panic::set_hook(Box::new(panic_fn)); +} + +fn format_backtrace(backtrace: &Backtrace) -> String { + + fn format_frame(frame: &BacktraceFrame) -> String { + + use std::ffi::OsStr; + + let ip = frame.ip(); + let symbols = frame.symbols(); + + const UNRESOLVED_FN_STR: &str = "unresolved function"; + + if symbols.is_empty() { + return format!("{} @ {:?}", UNRESOLVED_FN_STR, ip); + } + + // skip the first 10 symbols because they belong to the + // backtrace library and aren't relevant for debugging + symbols.iter().map(|symbol| { + + let mut nice_string = String::new(); + + if let Some(name) = symbol.name() { + let name_demangled = format!("{}", name); + let name_demangled_new = name_demangled.rsplit("::").skip(1).map(|e| e.to_string()).collect::>(); + let name_demangled = name_demangled_new.into_iter().rev().collect::>().join("::"); + nice_string.push_str(&name_demangled); + } else { + nice_string.push_str(UNRESOLVED_FN_STR); + } + + let mut file_string = String::new(); + if let Some(file) = symbol.filename() { + let origin_file_name = file.file_name() + .unwrap_or(OsStr::new("unresolved file name")) + .to_string_lossy(); + file_string.push_str(&format!("{}", origin_file_name)); + } + + if let Some(line) = symbol.lineno() { + file_string.push_str(&format!(":{}", line)); + } + + if !file_string.is_empty() { + nice_string.push_str(" @ "); + nice_string.push_str(&file_string); + if !nice_string.ends_with("\n") { + nice_string.push_str("\n"); + } + } + + nice_string + + }).collect::>().join("") + } + + backtrace + .frames() + .iter() + .map(|frame| format_frame(frame)) + .collect::>() + .join("\r\n") +} \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index 0c90d3f4e..8da7effa8 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -84,7 +84,7 @@ fn load_system_fonts<'a>(fonts: &mut FastHashMap, if let Some((font_bytes, idx)) = system_fonts::get(&FontPropertyBuilder::new().family(target).build()) { match rusttype_load_font(font_bytes.clone(), Some(idx)) { Ok((f, b)) => { fonts.insert(BuiltinFont(target), (f, b, FontState::ReadyForUpload(font_bytes))); }, - Err(e) => println!("error loading {} font: {:?}", target, e), + Err(e) => error!("Error loading {} font: {:?}", target, e), } } } diff --git a/src/window.rs b/src/window.rs index 2990961ca..8c5609a0a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -414,7 +414,7 @@ impl RenderNotifier for Notifier { fn wake_up(&self) { #[cfg(not(target_os = "android"))] - self.events_loop_proxy.wakeup().unwrap_or_else(|_| { eprintln!("couldn't wakeup event loop"); }); + self.events_loop_proxy.wakeup().unwrap_or_else(|_| { error!("couldn't wakeup event loop"); }); } fn new_frame_ready(&self, _id: DocumentId, _scrolled: bool, _composite_needed: bool, _render_time: Option) { From 092afca268cc3aeffb59258b71ed4b6d25a0d353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 15 Jul 2018 04:19:55 +0200 Subject: [PATCH 146/868] Added FromIterator for Dom You can now write: let dom = (0..5).map(|i| NodeData::new(NodeType::Label(format!("{}", i)), .. Default::default()) ).collect(); or: let dom = (0..5).map(|i| NodeType::Label(i.to_string())).collect() Both will generate the same DOM, but the first method allows you to customize the ID or class of the iterated item. This enables very terse code when working with lists. The resulting DOM has all resulting element as siblings, meaning they have no parent. Also added checks if the number of DOM nodes is 0 (if no nodes are present in the DOM). --- examples/debug.rs | 7 +- src/dom.rs | 156 +++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 2 +- src/logging.rs | 23 ++----- src/widgets/mod.rs | 2 +- 5 files changed, 164 insertions(+), 26 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 4bf42c54b..b5b1c2551 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -35,7 +35,7 @@ impl Layout for MyAppData { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file Button::with_label("SVG Datei öffnen...").dom() - .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) + .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } } @@ -69,6 +69,11 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { let mut svg_cache = SvgCache::empty(); + + let (font, _) = app_state.get_font(&FontId::BuiltinFont("sans-serif"))?; + let ch = SvgLayerType::from_character('a', font); + println!("ch: {:?}", ch); + let svg_layers = svg_cache.add_svg(&contents).ok()?; app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, diff --git a/src/dom.rs b/src/dom.rs index 15ce1e1e1..39dc68e1c 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -191,8 +191,7 @@ pub enum On { Scroll, } -#[derive(PartialEq, Eq)] -pub(crate) struct NodeData { +pub struct NodeData { /// `div` pub node_type: NodeType, /// `#main` @@ -205,6 +204,30 @@ pub(crate) struct NodeData { pub tag: Option, } +impl PartialEq for NodeData { + fn eq(&self, other: &Self) -> bool { + self.node_type == other.node_type && + self.id == other.id && + self.classes == other.classes && + self.events == other.events && + self.tag == other.tag + } +} + +impl Eq for NodeData { } + +impl Default for NodeData { + fn default() -> Self { + NodeData { + node_type: NodeType::Div, + id: None, + classes: Vec::new(), + events: CallbackList::default(), + tag: None, + } + } +} + impl Hash for NodeData { fn hash(&self, state: &mut H) { self.node_type.hash(state); @@ -217,7 +240,7 @@ impl Hash for NodeData { } impl NodeData { - pub fn calculate_node_data_hash(&self) -> DomHash { + pub(crate) fn calculate_node_data_hash(&self) -> DomHash { use std::hash::Hash; use twox_hash::XxHash; let mut hasher = XxHash::default(); @@ -256,6 +279,17 @@ impl fmt::Debug for NodeData { } } +impl PartialEq for CallbackList { + fn eq(&self, rhs: &Self) -> bool { + if self.callbacks.len() != rhs.callbacks.len() { + return false; + } + self.callbacks.iter().all(|(key, val)| { + rhs.callbacks.get(key) == Some(val) + }) + } +} + impl CallbackList { fn special_clone(&self) -> Self { Self { @@ -311,9 +345,17 @@ impl fmt::Debug for Dom { } } -#[derive(Clone, PartialEq, Eq)] -pub(crate) struct CallbackList { - pub(crate) callbacks: BTreeMap> +#[derive(Clone, Eq)] +pub struct CallbackList { + pub callbacks: BTreeMap> +} + +impl Default for CallbackList { + fn default() -> Self { + Self { + callbacks: BTreeMap::default(), + } + } } impl Hash for CallbackList { @@ -338,6 +380,58 @@ impl CallbackList { } } +use std::iter::FromIterator; + +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { + let mut c = Dom::new(NodeType::Div); + for i in iter { + c.add_child(i); + } + c + } +} + +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { + use id_tree::Node; + + let mut nodes = Vec::new(); + let mut idx = 0; + + for i in iter { + let node = Node { + data: i, + parent: None, + previous_sibling: if idx == 0 { None } else { Some(NodeId::new(idx - 1)) }, + next_sibling: Some(NodeId::new(idx + 1)), + last_child: None, + first_child: None, + }; + nodes.push(node); + idx += 1; + } + + let nodes_len = nodes.len(); + if nodes_len > 0 { + if let Some(last) = nodes.get_mut(nodes_len - 1) { + last.next_sibling = None; + } + } else { + // WARNING: nodes can be empty, so the root + // could point to an invalid node! + } + + Dom { head: NodeId::new(0), root: NodeId::new(0), arena: Rc::new(RefCell::new(Arena { nodes })) } + } +} + +impl FromIterator for Dom { + fn from_iter>(iter: I) -> Self { + iter.into_iter().map(|i| NodeData { node_type: i, .. Default::default() }).collect() + } +} + impl Dom { /// Creates an empty DOM @@ -359,6 +453,15 @@ impl Dom { let self_len = self.arena.borrow().nodes_len(); let sibling_len = sibling.arena.borrow().nodes_len(); + if sibling_len == 0 { + return; // No nodes to append, nothing to do + } + + if self_len == 0 { + *self = sibling; + return; + } + let mut self_arena = self.arena.borrow_mut(); let mut sibling_arena = sibling.arena.borrow_mut(); @@ -415,6 +518,15 @@ impl Dom { let self_len = self.arena.borrow().nodes_len(); let child_len = child.arena.borrow().nodes_len(); + if child_len == 0 { + return; // No nodes to append, nothing to do + } + + if self_len == 0 { + *self = child; + return; + } + let mut self_arena = self.arena.borrow_mut(); let mut child_arena = child.arena.borrow_mut(); @@ -603,4 +715,36 @@ fn test_dom_sibling_1() { ].next_sibling().expect("first child has no second sibling") ].first_child().expect("second sibling has no first child") ].data.id); +} + +#[test] +fn test_dom_from_iter_1() { + + use id_tree::Node; + + struct TestLayout { } + + impl Layout for TestLayout { + fn layout(&self) -> Dom { + (0..5).map(|e| NodeData::new(NodeType::Label(format!("{}", e + 1)))).collect() + } + } + + let dom = TestLayout{ }.layout(); + let arena = dom.arena.borrow(); + + assert_eq!(arena.nodes.last(), Some(&Node { + parent: None, + previous_sibling: Some(NodeId::new(3)), + next_sibling: None, + first_child: None, + last_child: None, + data: NodeData { + node_type: NodeType::Label(String::from("5")), + id: None, + classes: Vec::new(), + tag: None, + events: CallbackList::default(), + } + })); } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6d2b27150..f615adba5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,7 @@ pub mod prelude { pub use app::{App, AppConfig}; pub use app_state::AppState; pub use css::{Css, FakeCss}; - pub use dom::{Dom, NodeType, Callback, On, UpdateScreen}; + pub use dom::{Dom, NodeType, NodeData, Callback, On, UpdateScreen}; pub use traits::{Layout, ModifyAppState}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, diff --git a/src/logging.rs b/src/logging.rs index ccd68d5f8..8ed307333 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -8,8 +8,8 @@ pub(crate) fn set_up_logging(log_file_path: Option, log_level: LevelFilt use std::error::Error; /// Sets up the global logger - fn set_up_logging_internal(log_file_path: Option, log_level: LevelFilter) - -> Result<(), InitError> + fn set_up_logging_internal(log_file_path: Option, log_level: LevelFilter) + -> Result<(), InitError> { use std::io::{Error as IoError, ErrorKind as IoErrorKind}; @@ -18,7 +18,7 @@ pub(crate) fn set_up_logging(log_file_path: Option, log_level: LevelFilt let log_location = { use std::env; - + let mut exe_location = env::current_exe() .map_err(|_| InitError::Io(IoError::new(IoErrorKind::Other, "Executable has no executable path (?), can't open log file")))?; @@ -87,19 +87,8 @@ pub(crate) fn set_up_panic_hooks() { .lines() .filter(|l| !l.is_empty()) .skip(11) - .collect::>(); - let backtrace_len = backtrace_str.len(); - let mut backtrace_str_new = Vec::<&str>::new(); - - for (i, st) in backtrace_str.into_iter().enumerate() { - if i > backtrace_len - 10 { - break; - } else { - backtrace_str_new.push(st); - } - } - - let backtrace_str = backtrace_str_new.join("\r\n"); + .collect::>() + .join("\r\n"); let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); @@ -152,7 +141,7 @@ fn format_backtrace(backtrace: &Backtrace) -> String { return format!("{} @ {:?}", UNRESOLVED_FN_STR, ip); } - // skip the first 10 symbols because they belong to the + // skip the first 10 symbols because they belong to the // backtrace library and aren't relevant for debugging symbols.iter().map(|symbol| { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 668748757..596d142f2 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,6 +3,6 @@ pub mod button; pub mod label; // Re-export widgets -pub use self::svg::{Svg, SvgLayerId, SvgLayer, SvgCache}; +pub use self::svg::{Svg, SvgLayerId, SvgLayer, SvgLayerType, SvgWorldPixel, SvgCache}; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file From 27bf84b2c05cfdd31ea7e3f615c050b40d4048a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 16 Jul 2018 07:00:11 +0200 Subject: [PATCH 147/868] Cleaned up Dom::add_child and overflow checking for NodeId --- .travis.yml | 10 +---- Cargo.toml | 2 +- src/dom.rs | 105 +++++++++++++++++++++++++------------------------ src/id_tree.rs | 41 ++++++++++++++----- 4 files changed, 87 insertions(+), 71 deletions(-) diff --git a/.travis.yml b/.travis.yml index 375640b81..2e0e41aac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,8 @@ language: rust cache: cargo rust: - - stable - - beta - - nightly + - 1.26.0 + - 1.27.1 os: - linux @@ -22,11 +21,6 @@ script: - cargo build --no-default-features - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" -matrix: - allow_failures: - - rust: nightly - - rust: beta - # before_install: # - sudo apt-get update diff --git a/Cargo.toml b/Cargo.toml index 61b52cc2d..f646b8adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "091e9c53acd71fea20da18ea3a3d5eec7ac6c7d5" +rev = "915ec3a047d5991284bf5655df3b043e6b9756fa" [features] default = ["logging"] diff --git a/src/dom.rs b/src/dom.rs index 39dc68e1c..1ae07b85f 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -15,7 +15,7 @@ use { text_cache::TextId, traits::Layout, app_state::AppState, - id_tree::{NodeId, Arena}, + id_tree::{NodeId, Node, Arena}, }; /// This is only accessed from the main thread, so it's safe to use @@ -448,7 +448,9 @@ impl Dom { /// Adds a sibling to the current DOM pub fn add_sibling(&mut self, sibling: Self) { - use id_tree::Node; + + // Note: for a more readable Python version of this algorithm, + // see: https://gist.github.com/fschutt/4b3bd9a2654b548a6eb0b6a8623bdc8a#file-dow_new_2-py-L32-L63 let self_len = self.arena.borrow().nodes_len(); let sibling_len = sibling.arena.borrow().nodes_len(); @@ -469,60 +471,63 @@ impl Dom { let node: &mut Node> = &mut sibling_arena[NodeId::new(node_id)]; - { - let mut b_node_parent_is_some = false; - if let Some(parent) = node.parent_mut() { - *parent = *parent + self_len; - b_node_parent_is_some = true; - } - if !b_node_parent_is_some { - node.parent = self_arena[self.head].parent; - } + // NOTE: we cannot directly match on the option, since it leads to borrwowing issues + // We can't do `node.parent` in the `None` branch, since Rust thinks we still have access + // to the borrowed value because `node.parent_mut()` lives too long + + if node.parent_mut().and_then(|parent| { + // Some(parent) - increase the parent by the current arena length + *parent += self_len; + Some(parent) + }).is_none() { + // No parent - insert the current arenas head as the parent of the node + node.parent = self_arena[self.head].parent; } - { - let mut b_node_previous_sibling_is_some = false; - if let Some(previous_sibling) = node.previous_sibling_mut() { - *previous_sibling = *previous_sibling + self_len; - b_node_previous_sibling_is_some = true; - } - if !b_node_previous_sibling_is_some { - node.previous_sibling = Some(self.head); - } + if node.previous_sibling_mut().and_then(|previous_sibling| { + *previous_sibling += self_len; + Some(previous_sibling) + }).is_none() { + node.previous_sibling = Some(self.head); } if let Some(next_sibling) = node.next_sibling_mut() { - *next_sibling = *next_sibling + self_len; + *next_sibling += self_len; } if let Some(first_child) = node.first_child_mut() { - *first_child = *first_child + self_len; + *first_child += self_len; } if let Some(last_child) = node.last_child_mut() { - *last_child = *last_child + self_len; + *last_child += self_len; } } let head_node_id = NodeId::new(self_len); self_arena[self.head].next_sibling = Some(head_node_id); self.head = head_node_id; + (&mut *self_arena).append(&mut sibling_arena); } /// Adds a child DOM to the current DOM pub fn add_child(&mut self, child: Self) { - use id_tree::Node; + // Note: for a more readable Python version of this algorithm, + // see: https://gist.github.com/fschutt/4b3bd9a2654b548a6eb0b6a8623bdc8a#file-dow_new_2-py-L65-L107 let self_len = self.arena.borrow().nodes_len(); let child_len = child.arena.borrow().nodes_len(); if child_len == 0 { - return; // No nodes to append, nothing to do + // No nodes to append, nothing to do + return; } if self_len == 0 { + // Self has no nodes, therefore all child nodes will + // replace the self nodes, so *self = child; return; } @@ -537,51 +542,47 @@ impl Dom { let node: &mut Node> = &mut child_arena[node_id]; // WARNING: Order of these blocks is important! - { - let mut b_node_previous_sibling_is_some = false; - if let Some(previous_sibling) = node.previous_sibling_mut() { - *previous_sibling = *previous_sibling + self_len; - b_node_previous_sibling_is_some = true; - } - if !b_node_previous_sibling_is_some { - let last_child = self_arena[self.head].last_child; - if last_child.is_some() && node.parent.is_none() { - node.previous_sibling = last_child; - self_arena[last_child.unwrap()].next_sibling = Some(node_id + self_len); - } + + if node.previous_sibling_mut().and_then(|previous_sibling| { + // Some(previous_sibling) - increase the parent ID by the current arena length + *previous_sibling += self_len; + Some(previous_sibling) + }).is_none() { + // None - set the current heads' last child as the new previous sibling + let last_child = self_arena[self.head].last_child; + if last_child.is_some() && node.parent.is_none() { + node.previous_sibling = last_child; + self_arena[last_child.unwrap()].next_sibling = Some(node_id + self_len); } } - { - let mut b_node_parent_is_some = false; - if let Some(parent) = node.parent_mut() { - *parent = *parent + self_len; - b_node_parent_is_some = true; - } - if !b_node_parent_is_some { - if node.next_sibling.is_none() { - // We have encountered the last root item - last_sibling = Some(node_id); - } - node.parent = Some(self.head); + if node.parent_mut().and_then(|parent| { + *parent += self_len; + Some(parent) + }).is_none() { + // Have we encountered the last root item? + if node.next_sibling.is_none() { + last_sibling = Some(node_id); } + node.parent = Some(self.head); } if let Some(next_sibling) = node.next_sibling_mut() { - *next_sibling = *next_sibling + self_len; + *next_sibling += self_len; } if let Some(first_child) = node.first_child_mut() { - *first_child = *first_child + self_len; + *first_child += self_len; } if let Some(last_child) = node.last_child_mut() { - *last_child = *last_child + self_len; + *last_child += self_len; } } self_arena[self.head].first_child.get_or_insert(NodeId::new(self_len)); self_arena[self.head].last_child = Some(last_sibling.unwrap() + self_len); + (&mut *self_arena).append(&mut child_arena); } diff --git a/src/id_tree.rs b/src/id_tree.rs index f44b34016..2b8443471 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -3,7 +3,7 @@ use std::{ mem, fmt, - ops::{Index, Add, IndexMut, Deref}, + ops::{Index, Add, AddAssign, IndexMut, Deref}, hash::{Hasher, Hash}, collections::BTreeMap, cmp::Ordering, @@ -29,17 +29,29 @@ use std::{ pub struct NonZeroUsizeHack(&'static ()); impl NonZeroUsizeHack { - /// **NOTE**: Panics on overflow, since having a pointer that is zero is - /// undefined behaviour (it would bascially be casted to a `None`, - /// which is incorrect, so we rather panic on overflow to prevent that. - #[inline(always)] + /// **NOTE**: In debug mode, it panics on overflow, since having a + /// pointer that is zero is undefined behaviour (it would bascially be + /// casted to a `None`), which is incorrect, so we rather panic on overflow + /// to prevent that. + /// + /// To trigger an overflow however, you'd need more that 4 billion DOM nodes - + /// it is more likely that you run out of RAM before you do that. The only thing + /// that could lead to an overflow would be a bug. Therefore, overflow-checking is + /// disabled in release mode. + #[cfg_attr(not(debug_assertions), inline(always))] pub fn new(value: usize) -> Self { // Add 1 on insertion - let (new_value, has_overflown) = value.overflowing_add(1); - if has_overflown { - panic!("Overflow when creating DOM Node with ID {}", value); - } else { - unsafe { NonZeroUsizeHack(&*(new_value as *const ())) } + #[cfg(debug_assertions)] { + let (new_value, has_overflown) = value.overflowing_add(1); + if has_overflown { + panic!("Overflow when creating DOM Node with ID {}", value); + } else { + unsafe { NonZeroUsizeHack(&*(new_value as *const ())) } + } + } + + #[cfg(not(debug_assertions))] { + unsafe { NonZeroUsizeHack(&*((value + 1) as *const ())) } } } @@ -105,7 +117,16 @@ impl NodeId { } } +impl AddAssign for NodeId { + fn add_assign(&mut self, other: usize) { + *self = NodeId { + index: self.index + other + }; + } +} + impl Add for NodeId { + type Output = NodeId; fn add(self, other: usize) -> NodeId { From 0ba5c1f435b4fbea18da90d4f8dc73f021d2ae4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 17 Jul 2018 07:37:01 +0200 Subject: [PATCH 148/868] Cleaned up text caching, currently fighting with lifetime problem --- src/app.rs | 14 ++-- src/app_state.rs | 2 +- src/css_parser.rs | 52 ++++--------- src/display_list.rs | 25 ++++--- src/resources.rs | 30 ++++++-- src/text_cache.rs | 29 ++++---- src/text_layout.rs | 175 +++++++++++++++++++++++++------------------- 7 files changed, 175 insertions(+), 152 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0775f3778..3c388cc81 100644 --- a/src/app.rs +++ b/src/app.rs @@ -90,8 +90,8 @@ impl Default for FrameEventInfo { #[derive(Debug, Clone)] #[cfg_attr(not(feature = "logging"), derive(Copy))] pub struct AppConfig { - /// If enabled, logs error and info messages. - /// + /// If enabled, logs error and info messages. + /// /// Default is `Some(LevelFilter::Error)` to log all errors by default #[cfg(feature = "logging")] pub enable_logging: Option, @@ -99,7 +99,7 @@ pub struct AppConfig { #[cfg(feature = "logging")] pub log_file_path: Option, /// If the app crashes / panics, a window with a message box pops up - /// Additionally, the error + backtrace gets logged to the output + /// Additionally, the error + backtrace gets logged to the output /// file (if logging is enabled). #[cfg(feature = "logging")] pub enable_visual_panic_hook: bool, @@ -132,7 +132,7 @@ impl<'a, T: Layout> App<'a, T> { ::logging::set_up_panic_hooks(); } } - + Self { windows: Vec::new(), app_state: AppState::new(initial_data), @@ -451,7 +451,7 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.add_text_uncached(text) } - pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) + pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) -> TextId { self.app_state.add_text_cached(text, font_id, font_size) @@ -640,11 +640,11 @@ fn do_hit_test_and_call_callbacks( } } -fn render( +fn render<'a, T: Layout>( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, - app_resources: &mut AppResources, + app_resources: &'a mut AppResources<'a>, has_window_size_changed: bool) { use webrender::api::*; diff --git a/src/app_state.rs b/src/app_state.rs index 10e5a2f7d..35d8e98b1 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -226,7 +226,7 @@ impl<'a, T: Layout> AppState<'a, T> { self.resources.add_text_uncached(text) } - pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) + pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: PixelValue) -> TextId { let font_size = FontSize(font_size); diff --git a/src/css_parser.rs b/src/css_parser.rs index 2d2ab08b2..0eaa5046d 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -17,11 +17,11 @@ pub(crate) const EM_HEIGHT: f32 = 16.0; /// Webrender measures in points, not in pixels! pub(crate) const PT_TO_PX: f32 = 96.0 / 72.0; -// In case no font size is specified for a node, this will be subsituted as the -// default font size +// In case no font size is specified for a node, +// this will be subsituted as the default font size pub(crate) const DEFAULT_FONT_SIZE: FontSize = FontSize(PixelValue { metric: CssMetric::Px, - number: 10.0, + number: 10_000, }); /// Implements `From` for `$a`, mapping it to the `$b::$enum_type` variant @@ -275,20 +275,23 @@ impl<'a> From for CssParsingError<'a> { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct InvalidValueErr<'a>(pub &'a str); -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct PixelValue { pub metric: CssMetric, - pub number: f32, + /// Has to be divided by 1000.0 + pub number: isize, } -impl PartialEq for PixelValue { - fn eq(&self, other: &Self) -> bool { - self.compare_equality_2digits(other) +impl PixelValue { + pub fn to_pixels(&self) -> f32 { + match self.metric { + CssMetric::Px => { self.number as f32 / 1000.0 }, + CssMetric::Pt => { (self.number as f32 / 1000.0) * PT_TO_PX }, + CssMetric::Em => { (self.number as f32 / 1000.0) * EM_HEIGHT }, + } } } -impl Eq for PixelValue { } - /// "100%" or "1.0" value #[derive(Debug, PartialEq, Copy, Clone)] pub struct PercentageValue { @@ -296,36 +299,13 @@ pub struct PercentageValue { pub number: f32, } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq)] pub enum CssMetric { Px, Pt, Em, } -impl PixelValue { - pub fn to_pixels(&self) -> f32 { - match self.metric { - CssMetric::Px => { self.number }, - CssMetric::Pt => { self.number * PT_TO_PX }, - CssMetric::Em => { self.number * EM_HEIGHT }, - } - } - - /// Compare the equality of two font sizes up to the 4th digit - /// - /// i.e. `1.234` == `1.235` because `123` == `123` - /// - /// Usually this precision is enough to determine if two font sizes are - /// "equal" since you can't really compare floating-point values - /// - /// Used for the `PartialEq` implementation - pub fn compare_equality_2digits(&self, other: &Self) -> bool { - (self.to_pixels() * 100.0) as usize == - (other.to_pixels() * 100.0) as usize - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CssBorderRadiusParseError<'a> { TooManyValues(&'a str), @@ -475,7 +455,7 @@ fn parse_pixel_value<'a>(input: &'a str) Ok(PixelValue { metric: unit, - number: number, + number: (number * 1000.0) as isize, }) } @@ -1673,7 +1653,7 @@ fn parse_line_height(input: &str) parse_percentage_value(input).and_then(|e| Ok(LineHeight(e))) } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] pub struct FontSize(pub PixelValue); typed_pixel_value_parser!(parse_css_font_size, FontSize); diff --git a/src/display_list.rs b/src/display_list.rs index 4c944c849..13df9dfe5 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -73,19 +73,20 @@ pub(crate) enum TextInfo<'a> { } impl<'a> TextInfo<'a> { - /// Returns if the inner text is empty. Returns false if the ID does not exist + /// Returns if the inner text is empty. + /// + /// Returns true if the TextInfo::Cached TextId does not exist + /// (since in that case, it is "empty", so to speak) fn is_empty_text(&self, app_resources: &AppResources) -> bool { use self::TextInfo::*; - use text_cache::LargeString; match self { Cached(text_id) => { - match app_resources.text_cache.cached_strings.get(text_id) { - Some(LargeString::Raw(r)) => r.is_empty(), - Some(LargeString::Cached { words, .. }) => words.is_empty(), - None => false, + match app_resources.text_cache.string_cache.get(text_id) { + Some(s) => s.is_empty(), + None => true, } } Uncached(s) => s.is_empty(), @@ -243,12 +244,12 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } - pub fn into_display_list_builder( + pub fn into_display_list_builder<'b>( &self, pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, - app_resources: &mut AppResources, + app_resources: &'b mut AppResources<'b>, render_api: &RenderApi, mut has_window_size_changed: bool, window_size: &WindowSize) @@ -335,14 +336,14 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } -fn displaylist_handle_rect<'a>( +fn displaylist_handle_rect<'a, 'b>( builder: &mut DisplayListBuilder, rect_idx: NodeId, arena: &Arena>, html_node: &NodeType, bounds: TypedRect, full_screen_rect: TypedRect, - app_resources: &mut AppResources, + app_resources: &'b mut AppResources<'b>, render_api: &RenderApi, resource_updates: &mut Vec) { @@ -523,12 +524,12 @@ fn push_rect( } #[inline] -fn push_text<'a>( +fn push_text<'a, 'b>( info: &PrimitiveInfo, text: &TextInfo<'a>, builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &mut AppResources, + app_resources: &'b mut AppResources<'b>, render_api: &RenderApi, bounds: &TypedRect, resource_updates: &mut Vec, diff --git a/src/resources.rs b/src/resources.rs index 8da7effa8..bb79e412f 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -212,24 +212,40 @@ impl<'a> AppResources<'a> { pub(crate) fn add_text_uncached>(&mut self, text: S) -> TextId { - use text_cache::LargeString; - self.text_cache.add_text(LargeString::Raw(text.into())) + self.text_cache.add_text(text) } /// Calculates the widths for the words, then stores the widths of the words + the actual words /// /// This leads to a faster layout cycle, but has an upfront performance cost - pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) + pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) -> TextId { use rusttype::Scale; - use text_cache::LargeString; use std::rc::Rc; - let font_size_no_line_height = Scale::uniform(font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT); - let rusttype_font = self.font_data.get(font_id).expect("in resources.add_text_cached(): could not get font for caching text"); + // First, insert the text into the text cache + let id = self.add_text_uncached(text); + self.cache_text(id, font_id.clone(), font_size); + id + } + + /// Promotes (and calculates all the metrics) for a given text ID. + pub(crate) fn cache_text(&mut self, id: TextId, font: FontId, size: FontSize) { + + use rusttype::Scale; + + // We need to assume that the actual string contents have already been stored in self.text_cache + // Otherwise, how would the TextId be valid? + let text = self.text_cache.string_cache.get(&id).expect("Invalid text Id"); + let font_size_no_line_height = Scale::uniform(size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT); + let rusttype_font = self.font_data.get(&font).expect("Invalid font ID"); let words = split_text_into_words(text.as_ref(), &rusttype_font.0, font_size_no_line_height); - self.text_cache.add_text(LargeString::Cached { font: font_id.clone(), size: font_size, words: Rc::new(words) }) + + self.text_cache.cached_strings + .entry(id).or_insert_with(|| FastHashMap::default()) + .entry(font).or_insert_with(|| FastHashMap::default()) + .insert(size, words); } pub(crate) fn delete_text(&mut self, id: TextId) { diff --git a/src/text_cache.rs b/src/text_cache.rs index 9b1224425..bb70c003f 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -23,34 +23,37 @@ pub struct TextId { inner: usize, } -#[derive(Debug, Clone)] -pub(crate) enum LargeString { - Raw(String), - /// The `Vec` stores the individual word, so we don't need - /// to store it again. The `words` is stored in an Rc, so that we don't need to - /// duplicate it for every font size. - Cached { font: FontId, size: FontSize, words: Rc> }, -} - /// Cache for accessing large amounts of text #[derive(Debug, Default, Clone)] pub(crate) struct TextCache { - /// Gives you the mapping from the TextID to the actual, UTF-8 String - pub(crate) cached_strings: FastHashMap, + /// Caches the layout of the strings / words. + /// + /// TextId -> FontId (to look up by font) + /// FontId -> FontSize (to categorize by size within a font) + /// FontSize -> layouted words (to cache the glyph widths on a per-font-size basis) + pub(crate) cached_strings: FastHashMap>>>, + /// Mapping from the TextID to the actual, UTF-8 String + /// + /// This is stored outside of the actual glyph calculation, because usually you don't + /// need the string, except for rebuilding a cached string (for example, when the font is changed) + pub(crate) string_cache: FastHashMap, } impl TextCache { - pub(crate) fn add_text(&mut self, text: LargeString) -> TextId { + /// Add a new, large text to the resources + pub(crate) fn add_text>(&mut self, text: S) -> TextId { let id = new_text_id(); - self.cached_strings.insert(id, text); + self.string_cache.insert(id, text.into()); id } pub(crate) fn delete_text(&mut self, id: TextId) { + self.string_cache.remove(&id); self.cached_strings.remove(&id); } pub(crate) fn clear_all_texts(&mut self) { + self.string_cache.clear(); self.cached_strings.clear(); } } diff --git a/src/text_layout.rs b/src/text_layout.rs index 573320881..3437d2ecc 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -18,11 +18,10 @@ pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; +type Words = Vec; + #[derive(Debug, Clone)] pub(crate) struct Word { - /// The original text. TODO: Move this out of here, - /// this field gets unnecessarily cloned - pub text: String, /// Glyphs, positions are relative to the first character of the word pub glyphs: Vec, /// The sum of the width of all the characters @@ -129,19 +128,18 @@ struct FontMetrics { offset_top: f32, } -// TODO: hacky hacky shit. Seperate the text itself from the representation -// so we don't have to clone the strings when we change or zoom the font -fn get_string_from_words(words: &[SemanticWordItem]) -> String { - use self::SemanticWordItem::*; - let mut target = String::with_capacity(words.len()); - for word in words { - match word { - Word(w) => target += &w.text, - Tab => target.push('\t'), - Return => target.push('\n'), +enum WordRef<'a> { + Owned(Words), + Borrowed(&'a Words), +} + +impl<'a> WordRef<'a> { + fn as_ref<'b: 'a>(&'b self) -> &'b Words { + match self { + WordRef::Owned(ref w) => w, + WordRef::Borrowed(w) => w, } } - target } /// ## Inputs @@ -167,14 +165,8 @@ fn get_string_from_words(words: &[SemanticWordItem]) -> String { /// - `TextOverflowPass2`: This is internally used for aligning text (horizontally / vertically), but /// it is necessary for drawing the scrollbars later on, to determine the height of the bar. Contains /// info about if the text has overflown the rectangle, and if yes, by how many pixels -/// -/// ## Notes -/// -/// This function is currently very expensive, since it doesn't cache the string. So it does many small -/// allocations. This should be cleaned up in the future by caching `BlobStrings` and only re-layouting -/// when it's absolutely necessary. -pub(crate) fn get_glyphs<'a>( - app_resources: &AppResources<'a>, +pub(crate) fn get_glyphs<'a, 'b>( + app_resources: &'b mut AppResources<'b>, bounds: &TypedRect, horiz_alignment: TextAlignmentHorz, vert_alignment: TextAlignmentVert, @@ -187,7 +179,6 @@ pub(crate) fn get_glyphs<'a>( -> (Vec, TextOverflowPass2) { use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; - use text_cache::LargeString; let target_font = app_resources.font_data.get(target_font_id) .expect("Drawing with invalid font!"); @@ -216,52 +207,8 @@ pub(crate) fn get_glyphs<'a>( // This function also normalizes the unicode characters and calculates kerning. // // NOTE: This should be revisited, the caching does unnecessary cloning. - let (word_scale_factor, mut words) = match text { - TextInfo::Cached(text_id) => { - match app_resources.text_cache.cached_strings.get(text_id) { - Some(LargeString::Cached { font, size, words }) => { - if font == target_font_id { - use std::rc::Rc; - // If the target font is the same as the initial font, but the font size differs, - // all we have to do is to scale the widths of the words on the words - let cloned_words: Vec = (&*(words.clone())).clone(); - if size == target_font_size { - (None, cloned_words) - } else { - (Some(target_font_size.0.to_pixels() / size.0.to_pixels()), cloned_words) - } - } else { - // generate new words struct based on the previous words - let new_words = split_text_into_words(&get_string_from_words(words), &target_font.0, font_size_no_line_height); - (None, new_words) - } - }, - Some(LargeString::Raw(s)) => { - (None, split_text_into_words(s, &target_font.0, font_size_no_line_height)) - }, - None => panic!("Invalid TextId \"{:?}\" encountered in text_layout::get_glyphs", text_id), - } - }, - TextInfo::Uncached(s) => (None, split_text_into_words(s, &target_font.0, font_size_no_line_height)), - }; - - // Scale the horizontal width of the words to match the new font size - // Since each word has a local origin (i.e. the first character of each word - // is at (0, 0)), we can simply scale the X position of each glyph by a - // certain factor. - // - // So if we previously had a 12pt font and now a 13pt font, - // we simply scale each glyph position by 13 / 12. This is faster than - // re-calculating the font metrics (from Rusttype) each time we scale a - // large amount of text. - if let Some(scale_factor) = word_scale_factor { - for word in words.iter_mut() { - if let SemanticWordItem::Word(ref mut w) = word { - w.glyphs.iter_mut().for_each(|g| g.point.x *= scale_factor); - w.total_width *= scale_factor; - } - } - } + let words = get_words_cached(text, &target_font.0, target_font_id, target_font_size, font_size_no_line_height, app_resources); + let words = words.as_ref(); // (2) Calculate the additions / subtractions that have to be take into account // let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, &target_font.0); @@ -301,6 +248,88 @@ pub(crate) fn get_glyphs<'a>( (positioned_glyphs, overflow_pass_2) } +fn get_words_cached<'a, 'b>( + text: &TextInfo<'a>, + font: &Font<'a>, + font_id: &FontId, + font_size: &FontSize, + font_size_no_line_height: Scale, + app_resources: &'b mut AppResources<'b>) +-> WordRef<'b> +{ + use std::collections::hash_map::Entry::*; + use FastHashMap; + + match text { + TextInfo::Cached(text_id) => { + + let mut should_words_be_scaled = false; + + match app_resources.text_cache.cached_strings.entry(*text_id) { + Occupied(font_hash_map) => { + + let font_size_map = font_hash_map.get_mut().entry(*font_id).or_insert_with(|| FastHashMap::default()); + let is_new_font = font_size_map.is_empty(); + + match font_size_map.entry(*font_size) { + Occupied(existing_font_size_words) => { + // Text ID, Font ID and font size already exist - return the cache + return WordRef::Borrowed(existing_font_size_words.get()); + } + Vacant(v) => { + if is_new_font { + v.insert(split_text_into_words(&app_resources.text_cache.string_cache[text_id], font, font_size_no_line_height)); + } else { + // If we can get the words from any other size, we can just scale them here + // ex. if an existing font size gets scaled. + should_words_be_scaled = true; + } + } + } + }, + Vacant(_) => panic!("Invalid TextId \"{:?}\" encountered in text_layout::get_words_cached", text_id), + } + + // We have an entry in the font size -> words cache already, but it's not the right font size + // instead of recalculating the words, we simply scale them up. + if should_words_be_scaled { + let words_cloned = { + let font_size_map = &app_resources.text_cache.cached_strings[&text_id][&font_id]; + let (ref old_font_size, ref next_words_for_font) = font_size_map.iter().next().unwrap(); + let mut words_cloned: Words = *next_words_for_font.clone(); + let scale_factor = font_size.0.to_pixels() / old_font_size.0.to_pixels(); + + scale_words(&mut words_cloned, scale_factor); + words_cloned + }; + + app_resources.text_cache.cached_strings[&text_id][&font_id].insert(*font_size, words_cloned); + } + + WordRef::Borrowed(&app_resources.text_cache.cached_strings[&text_id][&font_id][&font_size]) + }, + TextInfo::Uncached(s) => WordRef::Owned(split_text_into_words(s, font, font_size_no_line_height)), + } +} + +fn scale_words(words: &mut Words, scale_factor: f32) { + // Scale the horizontal width of the words to match the new font size + // Since each word has a local origin (i.e. the first character of each word + // is at (0, 0)), we can simply scale the X position of each glyph by a + // certain factor. + // + // So if we previously had a 12pt font and now a 13pt font, + // we simply scale each glyph position by 13 / 12. This is faster than + // re-calculating the font metrics (from Rusttype) each time we scale a + // large amount of text. + for word in words.iter_mut() { + if let SemanticWordItem::Word(ref mut w) = word { + w.glyphs.iter_mut().for_each(|g| g.point.x *= scale_factor); + w.total_width *= scale_factor; + } + } +} + /// This function is also used in the `text_cache` module for caching large strings. /// /// It is one of the most expensive functions, use with care. @@ -318,7 +347,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: let mut last_glyph = None; fn end_word(words: &mut Vec, - chars_in_this_word: &mut Vec, glyphs_in_this_word: &mut Vec, cur_word_length: &mut f32, word_caret: &mut f32, @@ -326,7 +354,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: { // End of word words.push(SemanticWordItem::Word(Word { - text: chars_in_this_word.drain(..).collect(), glyphs: glyphs_in_this_word.drain(..).collect(), total_width: *cur_word_length, })); @@ -344,7 +371,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: if !chars_in_this_word.is_empty() { end_word( &mut words, - &mut chars_in_this_word, &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, @@ -357,7 +383,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: if !chars_in_this_word.is_empty() { end_word( &mut words, - &mut chars_in_this_word, &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, @@ -369,7 +394,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: if !chars_in_this_word.is_empty() { end_word( &mut words, - &mut chars_in_this_word, &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, @@ -414,7 +438,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: if !chars_in_this_word.is_empty() { end_word( &mut words, - &mut chars_in_this_word, &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, @@ -610,7 +633,7 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) /// the rectangle horizontally #[inline(always)] fn words_to_left_aligned_glyphs<'a>( - words: Vec, + words: &[SemanticWordItem], font: &Font<'a>, max_horizontal_width: Option, font_metrics: &FontMetrics) From 3f78a624e18c2677dd319b3d9547c6360cb11384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 17 Jul 2018 10:11:02 +0200 Subject: [PATCH 149/868] Fixed borrowing issues and text caching system Currently there are still a few minor annoyances, but it's much better than what we had before: - The font ID is currently cloned inside of the get_text_cached since Rusts .entry() function does not allow lookup by reference - If you use NodeType::Label, the key is cloned shortly during layout. This can and should be avoided, but right now cloning solves the borrowing issue. Besides, using Label is not guaranteed to be fast, so it's likely that the cloned string is only very small. --- src/app.rs | 4 +- src/display_list.rs | 25 +++++---- src/text_layout.rs | 125 ++++++++++++++++++++------------------------ 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3c388cc81..161cf3501 100644 --- a/src/app.rs +++ b/src/app.rs @@ -640,11 +640,11 @@ fn do_hit_test_and_call_callbacks( } } -fn render<'a, T: Layout>( +fn render( window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, - app_resources: &'a mut AppResources<'a>, + app_resources: &mut AppResources, has_window_size_changed: bool) { use webrender::api::*; diff --git a/src/display_list.rs b/src/display_list.rs index 13df9dfe5..76edfdf2c 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -67,12 +67,15 @@ pub(crate) struct SolvedLayout { /// In the cached version, you can lookup the text as well as the dimensions of /// the words in the `AppResources`. For the `Uncached` version, you'll have to re- /// calculate it on every frame. -pub(crate) enum TextInfo<'a> { +/// +/// TODO: It should be possible to switch this over to a `&'a str`, but currently +/// this leads to unsolvable borrowing issues. +pub(crate) enum TextInfo { Cached(TextId), - Uncached(&'a str), + Uncached(String), } -impl<'a> TextInfo<'a> { +impl TextInfo { /// Returns if the inner text is empty. /// /// Returns true if the TextInfo::Cached TextId does not exist @@ -244,12 +247,12 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } - pub fn into_display_list_builder<'b>( + pub fn into_display_list_builder( &self, pipeline_id: PipelineId, ui_solver: &mut UiSolver, css: &mut Css, - app_resources: &'b mut AppResources<'b>, + app_resources: &mut AppResources, render_api: &RenderApi, mut has_window_size_changed: bool, window_size: &WindowSize) @@ -336,14 +339,14 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } -fn displaylist_handle_rect<'a, 'b>( +fn displaylist_handle_rect<'a>( builder: &mut DisplayListBuilder, rect_idx: NodeId, arena: &Arena>, html_node: &NodeType, bounds: TypedRect, full_screen_rect: TypedRect, - app_resources: &'b mut AppResources<'b>, + app_resources: &mut AppResources, render_api: &RenderApi, resource_updates: &mut Vec) { @@ -410,7 +413,7 @@ fn displaylist_handle_rect<'a, 'b>( Label(text) => { push_text( &info, - &TextInfo::Uncached(text), + &TextInfo::Uncached(text.clone()), builder, &rect.style, app_resources, @@ -524,12 +527,12 @@ fn push_rect( } #[inline] -fn push_text<'a, 'b>( +fn push_text( info: &PrimitiveInfo, - text: &TextInfo<'a>, + text: &TextInfo, builder: &mut DisplayListBuilder, style: &RectStyle, - app_resources: &'b mut AppResources<'b>, + app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, resource_updates: &mut Vec, diff --git a/src/text_layout.rs b/src/text_layout.rs index 3437d2ecc..a769282f2 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -10,6 +10,7 @@ use { TextAlignmentHorz, FontSize, BackgroundColor, FontId, TextAlignmentVert, LineHeight, LayoutOverflow }, + text_cache::{TextId, TextCache}, }; /// Rusttype has a certain sizing hack, I have no idea where this number comes from @@ -128,20 +129,6 @@ struct FontMetrics { offset_top: f32, } -enum WordRef<'a> { - Owned(Words), - Borrowed(&'a Words), -} - -impl<'a> WordRef<'a> { - fn as_ref<'b: 'a>(&'b self) -> &'b Words { - match self { - WordRef::Owned(ref w) => w, - WordRef::Borrowed(w) => w, - } - } -} - /// ## Inputs /// /// - `app_resources`: This is only used for caching - if you already have a `LargeString`, which @@ -165,15 +152,15 @@ impl<'a> WordRef<'a> { /// - `TextOverflowPass2`: This is internally used for aligning text (horizontally / vertically), but /// it is necessary for drawing the scrollbars later on, to determine the height of the bar. Contains /// info about if the text has overflown the rectangle, and if yes, by how many pixels -pub(crate) fn get_glyphs<'a, 'b>( - app_resources: &'b mut AppResources<'b>, +pub(crate) fn get_glyphs( + app_resources: &mut AppResources, bounds: &TypedRect, horiz_alignment: TextAlignmentHorz, vert_alignment: TextAlignmentVert, target_font_id: &FontId, target_font_size: &FontSize, line_height: Option, - text: &TextInfo<'a>, + text: &TextInfo, overflow: &LayoutOverflow, scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) @@ -207,8 +194,16 @@ pub(crate) fn get_glyphs<'a, 'b>( // This function also normalizes the unicode characters and calculates kerning. // // NOTE: This should be revisited, the caching does unnecessary cloning. - let words = get_words_cached(text, &target_font.0, target_font_id, target_font_size, font_size_no_line_height, app_resources); - let words = words.as_ref(); + let words_owned; + let words = match text { + TextInfo::Cached(text_id) => { + get_words_cached(text_id, &target_font.0, target_font_id, target_font_size, font_size_no_line_height, &mut app_resources.text_cache) + }, + TextInfo::Uncached(s) => { + words_owned = split_text_into_words(s, &target_font.0, font_size_no_line_height); + &words_owned + }, + }; // (2) Calculate the additions / subtractions that have to be take into account // let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, &target_font.0); @@ -248,68 +243,59 @@ pub(crate) fn get_glyphs<'a, 'b>( (positioned_glyphs, overflow_pass_2) } -fn get_words_cached<'a, 'b>( - text: &TextInfo<'a>, +fn get_words_cached<'a>( + text_id: &TextId, font: &Font<'a>, font_id: &FontId, font_size: &FontSize, font_size_no_line_height: Scale, - app_resources: &'b mut AppResources<'b>) --> WordRef<'b> + text_cache: &'a mut TextCache) +-> &'a Words { use std::collections::hash_map::Entry::*; use FastHashMap; - match text { - TextInfo::Cached(text_id) => { - - let mut should_words_be_scaled = false; + let mut should_words_be_scaled = false; - match app_resources.text_cache.cached_strings.entry(*text_id) { - Occupied(font_hash_map) => { + match text_cache.cached_strings.entry(*text_id) { + Occupied(mut font_hash_map) => { - let font_size_map = font_hash_map.get_mut().entry(*font_id).or_insert_with(|| FastHashMap::default()); - let is_new_font = font_size_map.is_empty(); + let font_size_map = font_hash_map.get_mut().entry(font_id.clone()).or_insert_with(|| FastHashMap::default()); + let is_new_font = font_size_map.is_empty(); - match font_size_map.entry(*font_size) { - Occupied(existing_font_size_words) => { - // Text ID, Font ID and font size already exist - return the cache - return WordRef::Borrowed(existing_font_size_words.get()); - } - Vacant(v) => { - if is_new_font { - v.insert(split_text_into_words(&app_resources.text_cache.string_cache[text_id], font, font_size_no_line_height)); - } else { - // If we can get the words from any other size, we can just scale them here - // ex. if an existing font size gets scaled. - should_words_be_scaled = true; - } - } + match font_size_map.entry(*font_size) { + Occupied(existing_font_size_words) => { } + Vacant(v) => { + if is_new_font { + v.insert(split_text_into_words(&text_cache.string_cache[text_id], font, font_size_no_line_height)); + } else { + // If we can get the words from any other size, we can just scale them here + // ex. if an existing font size gets scaled. + should_words_be_scaled = true; } - }, - Vacant(_) => panic!("Invalid TextId \"{:?}\" encountered in text_layout::get_words_cached", text_id), + } } + }, + Vacant(_) => { }, + } - // We have an entry in the font size -> words cache already, but it's not the right font size - // instead of recalculating the words, we simply scale them up. - if should_words_be_scaled { - let words_cloned = { - let font_size_map = &app_resources.text_cache.cached_strings[&text_id][&font_id]; - let (ref old_font_size, ref next_words_for_font) = font_size_map.iter().next().unwrap(); - let mut words_cloned: Words = *next_words_for_font.clone(); - let scale_factor = font_size.0.to_pixels() / old_font_size.0.to_pixels(); - - scale_words(&mut words_cloned, scale_factor); - words_cloned - }; - - app_resources.text_cache.cached_strings[&text_id][&font_id].insert(*font_size, words_cloned); - } + // We have an entry in the font size -> words cache already, but it's not the right font size + // instead of recalculating the words, we simply scale them up. + if should_words_be_scaled { + let words_cloned = { + let font_size_map = &text_cache.cached_strings[&text_id][&font_id]; + let (old_font_size, next_words_for_font) = font_size_map.iter().next().unwrap(); + let mut words_cloned: Words = next_words_for_font.clone(); + let scale_factor = font_size.0.to_pixels() / old_font_size.0.to_pixels(); + + scale_words(&mut words_cloned, scale_factor); + words_cloned + }; - WordRef::Borrowed(&app_resources.text_cache.cached_strings[&text_id][&font_id][&font_size]) - }, - TextInfo::Uncached(s) => WordRef::Owned(split_text_into_words(s, font, font_size_no_line_height)), + text_cache.cached_strings.get_mut(&text_id).unwrap().get_mut(&font_id).unwrap().insert(*font_size, words_cloned); } + + text_cache.cached_strings.get(&text_id).unwrap().get(&font_id).unwrap().get(&font_size).unwrap() } fn scale_words(words: &mut Words, scale_factor: f32) { @@ -685,12 +671,13 @@ fn words_to_left_aligned_glyphs<'a>( current_line_num += 1; } - for mut glyph in word.glyphs { + for glyph in &word.glyphs { + let mut new_glyph = *glyph; let push_x = word_caret; let push_y = (current_line_num as f32 * vertical_advance) + offset_top; - glyph.point.x += push_x; - glyph.point.y += push_y; - left_aligned_glyphs.push(glyph); + new_glyph.point.x += push_x; + new_glyph.point.y += push_y; + left_aligned_glyphs.push(new_glyph); } // Add the word width to the current word_caret From fce6ed68c37c808085c549beb0771b73388f88a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 17 Jul 2018 10:47:13 +0200 Subject: [PATCH 150/868] Fixed CSS tests --- src/css_parser.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index 0eaa5046d..c31e9c71d 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -2076,12 +2076,12 @@ mod css_tests { #[test] fn test_parse_pixel_value_1() { - assert_eq!(parse_pixel_value("15px"), Ok(PixelValue { metric: CssMetric::Px, number: 15.0 })); + assert_eq!(parse_pixel_value("15px"), Ok(PixelValue { metric: CssMetric::Px, number: 15000 })); } #[test] fn test_parse_pixel_value_2() { - assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue { metric: CssMetric::Em, number: 1.2 })); + assert_eq!(parse_pixel_value("1.2em"), Ok(PixelValue { metric: CssMetric::Em, number: 1200 })); } #[test] From db892e3f52157e78b0c3a0bc6bc1bbdd1d341a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 18 Jul 2018 01:30:08 +0200 Subject: [PATCH 151/868] Added initial SVG text rendering --- examples/debug.rs | 11 +++++++--- src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 52 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index b5b1c2551..d98be5501 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -71,10 +71,15 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let mut svg_cache = SvgCache::empty(); let (font, _) = app_state.get_font(&FontId::BuiltinFont("sans-serif"))?; - let ch = SvgLayerType::from_character('a', font); - println!("ch: {:?}", ch); - let svg_layers = svg_cache.add_svg(&contents).ok()?; + let text_layer = LayerType::from_single_layer(SvgLayerType::from_character('a', font).1); + let svg_layer = SvgLayer::default_from_layer(text_layer, SvgStyle::filled(ColorU { r: 0, b: 0, g: 0, a: 200 })); + + let mut svg_layers = svg_cache.add_svg(&contents).ok()?; + + let text_layer_id = svg_cache.add_layer(svg_layer); + svg_layers.push(text_layer_id); + app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, layers: svg_layers, diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 596d142f2..360fcaf50 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,6 +3,6 @@ pub mod button; pub mod label; // Re-export widgets -pub use self::svg::{Svg, SvgLayerId, SvgLayer, SvgLayerType, SvgWorldPixel, SvgCache}; +pub use self::svg::{Svg, SvgLayerId, SvgLayer, LayerType, SvgStyle, SvgLayerType, SvgWorldPixel, SvgCache}; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index dc9c07d65..8112d4789 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -516,9 +516,23 @@ pub struct SvgLayer { pub callbacks: SvgCallbacks, pub style: SvgStyle, pub transform_id: Option, + // TODO: This is currently not used pub view_box_id: SvgViewBoxId, } +impl SvgLayer { + /// Shorthand for creating a SvgLayer from some data and style + pub fn default_from_layer(data: LayerType, style: SvgStyle) -> Self { + SvgLayer { + data, + callbacks: SvgCallbacks::None, + style, + transform_id: None, + view_box_id: new_view_box_id(), + } + } +} + #[derive(Debug, Clone)] pub enum LayerType { KnownSize([SvgLayerType; 1]), @@ -533,6 +547,14 @@ impl LayerType { UnknownSize(b) => &b[..], } } + + pub fn from_polygons(data: Vec) -> Self { + LayerType::UnknownSize(data) + } + + pub fn from_single_layer(data: SvgLayerType) -> Self { + LayerType::KnownSize([data]) + } } impl Clone for SvgLayer { @@ -602,6 +624,21 @@ pub struct SvgStyle { // TODO: stroke-dasharray } +impl SvgStyle { + pub fn stroked(color: ColorU, stroke_opts: SvgStrokeOptions) -> Self { + Self { + stroke: Some((color, stroke_opts)), + .. Default::default() + } + } + + pub fn filled(color: ColorU) -> Self { + Self { + fill: Some(color), + .. Default::default() + } + } +} // similar to lyon::SvgStrokeOptions, except the // thickness is a usize (f32 * 1000 as usize), in order // to implement Hash @@ -774,7 +811,7 @@ impl SvgLayerType { .as_ref() .unwrap() .iter() - .map(svg_to_lyon::glyph_to_path_events) + .map(svg_to_lyon::rusttype_glyph_to_path_events) .collect(); (glyph.id(), SvgLayerType::Text(path_events)) @@ -1040,13 +1077,18 @@ mod svg_to_lyon { // Convert a Rusttype glyph to a Vec of PathEvents, // in order to turn a glyph into a polygon - pub fn glyph_to_path_events(vertex: &Vertex) + pub fn rusttype_glyph_to_path_events(vertex: &Vertex) -> PathEvent { use rusttype::VertexType; + // Rusttypes vertex type needs to be inverted in the Y axis + // in order to work with lyon correctly match vertex.vertex_type() { - VertexType::CurveTo => PathEvent::QuadraticTo(Point::new(vertex.cx as f32, vertex.cy as f32), Point::new(vertex.x as f32, vertex.y as f32)), - VertexType::MoveTo => PathEvent::MoveTo(Point::new(vertex.x as f32, vertex.y as f32)), - VertexType::LineTo => PathEvent::LineTo(Point::new(vertex.x as f32, vertex.y as f32)), + VertexType::CurveTo => PathEvent::QuadraticTo( + Point::new(vertex.cx as f32, -(vertex.cy as f32)), + Point::new(vertex.x as f32, -(vertex.y as f32)) + ), + VertexType::MoveTo => PathEvent::MoveTo(Point::new(vertex.x as f32, -(vertex.y as f32))), + VertexType::LineTo => PathEvent::LineTo(Point::new(vertex.x as f32, -(vertex.y as f32))), } } } From 8a1010b6bc13a34bdadede8fd9e67eb72d395797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 18 Jul 2018 06:54:11 +0200 Subject: [PATCH 152/868] Exposed text_layout functionality in the public API --- examples/debug.rs | 8 ++++ src/css_parser.rs | 34 +++++++++++++++-- src/lib.rs | 16 ++++---- src/text_layout.rs | 95 +++++++++++++++++++++++++++++++++------------- 4 files changed, 115 insertions(+), 38 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index d98be5501..2c690bccb 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -68,6 +68,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv open_file_dialog(None, None) .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { + let mut svg_cache = SvgCache::empty(); let (font, _) = app_state.get_font(&FontId::BuiltinFont("sans-serif"))?; @@ -80,6 +81,13 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let text_layer_id = svg_cache.add_layer(svg_layer); svg_layers.push(text_layer_id); + { + use azul::text_layout::*; + let font_metrics = FontMetrics::new(font, &FontSize::px(10.0), None); + let layout = layout_text("Hello World", font, &font_metrics); + println!("text layout glyphs: {:?}", layout.0); + } + app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, layers: svg_layers, diff --git a/src/css_parser.rs b/src/css_parser.rs index c31e9c71d..ee23f0a53 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -277,12 +277,21 @@ pub struct InvalidValueErr<'a>(pub &'a str); #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct PixelValue { - pub metric: CssMetric, - /// Has to be divided by 1000.0 - pub number: isize, + metric: CssMetric, + /// Has to be divided by 1000.0 - PixelValue needs to implement Hash, + /// but Hash is not possible for floating-point values + number: isize, } impl PixelValue { + + pub fn from_metric(metric: CssMetric, value: f32) -> Self { + Self { + metric: metric, + number: (value * 1000.0) as isize, + } + } + pub fn to_pixels(&self) -> f32 { match self.metric { CssMetric::Px => { self.number as f32 / 1000.0 }, @@ -1654,10 +1663,27 @@ fn parse_line_height(input: &str) } #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] -pub struct FontSize(pub PixelValue); +pub struct FontSize(pub(crate) PixelValue); typed_pixel_value_parser!(parse_css_font_size, FontSize); +impl FontSize { + /// Creates the font size in pixel + pub fn px(value: f32) -> Self { + FontSize(PixelValue::from_metric(CssMetric::Px, value)) + } + + /// Creates the font size in em + pub fn em(value: f32) -> Self { + FontSize(PixelValue::from_metric(CssMetric::Em, value)) + } + + /// Creates the font size in point (pt) + pub fn pt(value: f32) -> Self { + FontSize(PixelValue::from_metric(CssMetric::Pt, value)) + } +} + #[derive(Debug, PartialEq, Clone)] pub struct FontFamily { // parsed fonts, in order, i.e. "Webly Sleeky UI", "monospace", etc. diff --git a/src/lib.rs b/src/lib.rs index f615adba5..050aa0352 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,24 +65,24 @@ extern crate nfd; /// DOM / HTML node handling pub mod dom; -/// The layout traits for creating a layout-able application -pub mod traits; -/// Window handling -pub mod window; +/// Bindings to the native file-chooser, color picker, etc. dialogs +pub mod dialogs; /// Async IO / task system pub mod task; +/// Text layout helper functions - useful for text layout outside of standard containers +pub mod text_layout; +/// The layout traits for creating a layout-able application +pub mod traits; /// Built-in widgets pub mod widgets; -/// Bindings to the native file-chooser, color picker, etc. dialogs -pub mod dialogs; +/// Window handling +pub mod window; /// Global application (Initialization starts here) mod app; /// Wrapper for the application data & application state mod app_state; /// Styling & CSS parsing mod css; -/// Text layout -mod text_layout; /// Font & image resource handling, lookup and caching mod resources; /// UI Description & display list handling (webrender) diff --git a/src/text_layout.rs b/src/text_layout.rs index a769282f2..f967f393c 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -116,9 +116,10 @@ pub(crate) struct ScrollbarInfo { pub(crate) background_color: BackgroundColor, } -/// Temporary struct so I don't have to pass the three parameters around seperately all the time +/// Temporary struct that contains various metrics related to a font - +/// useful so we don't have to access the font to look up certain widths #[derive(Debug, Copy, Clone)] -struct FontMetrics { +pub struct FontMetrics { /// Width of the space character space_width: f32, /// Usually 4 * space_width @@ -127,6 +128,12 @@ struct FontMetrics { vertical_advance: f32, /// Offset of the font from the top of the bounding rectangle offset_top: f32, + /// Font size (for rusttype, includes the `RUSTTYPE_SIZE_HACK`) in **pt** (not px) + /// Used for vertical layouting (since it includes the line height) + font_size_with_line_height: Scale, + /// Same as `font_size_with_line_height` but without the line height incorporated. + /// Used for horizontal layouting + font_size_no_line_height: Scale, } /// ## Inputs @@ -167,26 +174,9 @@ pub(crate) fn get_glyphs( { use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; - let target_font = app_resources.font_data.get(target_font_id) - .expect("Drawing with invalid font!"); - - let target_font_size_f32 = target_font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT; - let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; - let font_size_with_line_height = Scale::uniform(target_font_size_f32 * line_height); - let font_size_no_line_height = Scale::uniform(target_font_size_f32); - let space_width = target_font.0.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; - let tab_width = 4.0 * space_width; // TODO: make this configurable + let target_font = app_resources.font_data.get(target_font_id).expect("Drawing with invalid font!"); - let v_metrics_scaled = target_font.0.v_metrics(font_size_with_line_height); - let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - let offset_top = v_metrics_scaled.ascent / 2.0; - - let font_metrics = FontMetrics { - vertical_advance: v_advance_scaled, - space_width: space_width, - tab_width: tab_width, - offset_top: offset_top, - }; + let font_metrics = calculate_font_metrics(&target_font.0, target_font_size, line_height); // (1) Split the text into semantic items (word, tab or newline) OR get the cached // text and scale it accordingly. @@ -197,10 +187,10 @@ pub(crate) fn get_glyphs( let words_owned; let words = match text { TextInfo::Cached(text_id) => { - get_words_cached(text_id, &target_font.0, target_font_id, target_font_size, font_size_no_line_height, &mut app_resources.text_cache) + get_words_cached(text_id, &target_font.0, target_font_id, target_font_size, font_metrics.font_size_no_line_height, &mut app_resources.text_cache) }, TextInfo::Uncached(s) => { - words_owned = split_text_into_words(s, &target_font.0, font_size_no_line_height); + words_owned = split_text_into_words(s, &target_font.0, font_metrics.font_size_no_line_height); &words_owned }, }; @@ -243,6 +233,38 @@ pub(crate) fn get_glyphs( (positioned_glyphs, overflow_pass_2) } +impl FontMetrics { + /// Given a font, font size and line height, calculates the `FontMetrics` necessary + /// which are later used to layout a block of text + pub fn new<'a>(font: &Font<'a>, font_size: &FontSize, line_height: Option) -> Self { + calculate_font_metrics(font, font_size, line_height) + } +} + +fn calculate_font_metrics<'a>(font: &Font<'a>, font_size: &FontSize, line_height: Option) -> FontMetrics { + + let font_size_f32 = font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT; + let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; + let font_size_with_line_height = Scale::uniform(font_size_f32 * line_height); + let font_size_no_line_height = Scale::uniform(font_size_f32); + + let space_width = font.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; + let tab_width = 4.0 * space_width; // TODO: make this configurable + + let v_metrics_scaled = font.v_metrics(font_size_with_line_height); + let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; + let offset_top = v_metrics_scaled.ascent / 2.0; + + FontMetrics { + vertical_advance: v_advance_scaled, + space_width, + tab_width, + offset_top, + font_size_with_line_height, + font_size_no_line_height, + } +} + fn get_words_cached<'a>( text_id: &TextId, font: &Font<'a>, @@ -444,7 +466,7 @@ fn estimate_overflow_pass_1( { use self::SemanticWordItem::*; - let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; let max_text_line_len_horizontal = 0.0; @@ -556,7 +578,7 @@ fn estimate_overflow_pass_2( pass1: TextOverflowPass1) -> (TypedSize2D, TextOverflowPass2) { - let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; let mut new_size = *rect_dimensions; @@ -625,7 +647,7 @@ fn words_to_left_aligned_glyphs<'a>( font_metrics: &FontMetrics) -> (Vec, Vec<(usize, f32)>) { - let FontMetrics { space_width, tab_width, vertical_advance, offset_top } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, // left-aligned @@ -833,6 +855,27 @@ fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) } } +// -------------------------- PUBLIC API -------------------------- // + +/// Use `calculate_font_metrics` to calculate the `font_metrics` value. +/// +/// This is useful if you need to layout many small texts in a loop, so we don't need to +/// re-calculate the metrics over and over again. +pub fn layout_text<'a>( + text: &str, + font: &Font<'a>, + font_metrics: &FontMetrics) +-> (Vec, Vec<(usize, f32)>) +{ + // NOTE: This function is different from the get_glyphs function that is + // used internally to azul. + // + // This function simply lays out a text, without trying to fit it into a rectangle. + // This function does not calculate any overflow. + let words = split_text_into_words(text, font, font_metrics.font_size_no_line_height); + words_to_left_aligned_glyphs(&words, font, None, font_metrics) +} + #[test] fn test_it_should_add_origin() { let mut instances = vec![ From 7f3c8b0116c604cf5a3139187826f22771be8e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 18 Jul 2018 08:07:14 +0200 Subject: [PATCH 153/868] Refactored text_layout, exposed TextCache in WindowInfo --- examples/debug.rs | 6 ++-- src/app.rs | 25 +++++++------- src/lib.rs | 5 +-- src/text_cache.rs | 14 ++++---- src/text_layout.rs | 81 +++++++++++++++++++++++++++++++++++----------- src/ui_state.rs | 10 ++++-- src/window.rs | 4 ++- 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 2c690bccb..9ccf27ea1 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -34,7 +34,7 @@ impl Layout for MyAppData { } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file - Button::with_label("SVG Datei öffnen...").dom() + Button::with_label("Open SVG file...").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } @@ -85,7 +85,8 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv use azul::text_layout::*; let font_metrics = FontMetrics::new(font, &FontSize::px(10.0), None); let layout = layout_text("Hello World", font, &font_metrics); - println!("text layout glyphs: {:?}", layout.0); + println!("text layout glyphs: {:?}", layout.layouted_glyphs); + println!("text min w: {} min h: {}", layout.min_width, layout.min_height); } app_state.data.modify(|data| data.map = Some(Map { @@ -95,6 +96,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv pan_horz: 0.0, pan_vert: 0.0, })); + Some(UpdateScreen::Redraw) }) .unwrap_or_else(|| { diff --git a/src/app.rs b/src/app.rs index 161cf3501..cdb2c7e1d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -261,12 +261,12 @@ impl<'a, T: Layout> App<'a, T> { if frame_event_info.should_redraw_window || force_redraw_cache[idx] > 0 { // Call the Layout::layout() fn, get the DOM - ui_state_cache[idx] = UiState::from_app_state(&self.app_state, WindowInfo { - window_id: WindowId { id: idx }, - window: ReadOnlyWindow { - inner: window.display.clone(), - } - }); + let window_id = WindowId { id: idx }; + let read_only_window = ReadOnlyWindow { inner: window.display.clone() }; + ui_state_cache[idx] = UiState::from_app_state( + &self.app_state, window_id, read_only_window + ); + // Style the DOM ui_description_cache[idx] = UiDescription::from_ui_state(&ui_state_cache[idx], &mut window.css); // send webrender the size and buffer of the display @@ -320,14 +320,11 @@ impl<'a, T: Layout> App<'a, T> { { use window::{ReadOnlyWindow, WindowInfo}; - windows.iter().enumerate().map(|(idx, w)| - UiState::from_app_state(app_state, WindowInfo { - window_id: WindowId { id: idx }, - window: ReadOnlyWindow { - inner: w.display.clone(), - } - }) - ).collect() + windows.iter().enumerate().map(|(idx, w)| { + let window_id = WindowId { id: idx }; + let read_only_window = ReadOnlyWindow { inner: w.display.clone() }; + UiState::from_app_state(app_state, window_id, read_only_window) + }).collect() } /// Add an image to the internal resources diff --git a/src/lib.rs b/src/lib.rs index 050aa0352..3d38713de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,8 @@ pub mod dom; pub mod dialogs; /// Async IO / task system pub mod task; +/// Module for caching long texts (including their layout / character positions) across multiple frames +pub mod text_cache; /// Text layout helper functions - useful for text layout outside of standard containers pub mod text_layout; /// The layout traits for creating a layout-able application @@ -113,8 +115,6 @@ mod menu; mod compositor; // /// Platform extensions (non-portable window extensions for Win32, Wayland, X11, Cocoa) // mod platform_ext; -/// Module for caching long texts (including their layout / character positions) across multiple frames -mod text_cache; /// Default logger, can be turned off with `feature = "logging"` #[cfg(feature = "logging")] mod logging; @@ -135,6 +135,7 @@ pub mod prelude { WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; pub use window_state::WindowState; pub use images::ImageType; + pub use text_cache::{TextCache, TextId}; pub use css_parser::{ ParsedCssProperty, BorderRadius, BackgroundColor, TextColor, BorderWidths, BorderDetails, Background, FontSize, diff --git a/src/text_cache.rs b/src/text_cache.rs index bb70c003f..5ea7ab090 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -5,7 +5,7 @@ use std::{ use { FastHashMap, css_parser::{FontId, FontSize}, - text_layout::SemanticWordItem, + text_layout::Words, }; static TEXT_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -25,34 +25,34 @@ pub struct TextId { /// Cache for accessing large amounts of text #[derive(Debug, Default, Clone)] -pub(crate) struct TextCache { +pub struct TextCache { /// Caches the layout of the strings / words. /// /// TextId -> FontId (to look up by font) /// FontId -> FontSize (to categorize by size within a font) /// FontSize -> layouted words (to cache the glyph widths on a per-font-size basis) - pub(crate) cached_strings: FastHashMap>>>, + pub cached_strings: FastHashMap>>, /// Mapping from the TextID to the actual, UTF-8 String /// /// This is stored outside of the actual glyph calculation, because usually you don't /// need the string, except for rebuilding a cached string (for example, when the font is changed) - pub(crate) string_cache: FastHashMap, + pub string_cache: FastHashMap, } impl TextCache { /// Add a new, large text to the resources - pub(crate) fn add_text>(&mut self, text: S) -> TextId { + pub fn add_text>(&mut self, text: S) -> TextId { let id = new_text_id(); self.string_cache.insert(id, text.into()); id } - pub(crate) fn delete_text(&mut self, id: TextId) { + pub fn delete_text(&mut self, id: TextId) { self.string_cache.remove(&id); self.cached_strings.remove(&id); } - pub(crate) fn clear_all_texts(&mut self) { + pub fn clear_all_texts(&mut self) { self.string_cache.clear(); self.cached_strings.clear(); } diff --git a/src/text_layout.rs b/src/text_layout.rs index f967f393c..294a00340 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -19,18 +19,31 @@ pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; -type Words = Vec; +/// Words are a collection of glyph information, i.e. how much +/// horizontal space each of the words in a text block and how much +/// space each individual glyph take up. +/// +/// This is important for calculating metrics such as the minimal +/// bounding box of a block of text, for example - without actually +/// acessing the font at all. +/// +/// Be careful when caching this - the `Words` are independent of the +/// original font, so be sure to note the font ID if you cache this struct. +#[derive(Debug, Clone)] +pub struct Words(Vec); +/// A `Word` contains information about the layout of a single word #[derive(Debug, Clone)] -pub(crate) struct Word { +pub struct Word { /// Glyphs, positions are relative to the first character of the word pub glyphs: Vec, /// The sum of the width of all the characters pub total_width: f32, } +/// Either a white-space delimited word, tab or return character #[derive(Debug, Clone)] -pub(crate) enum SemanticWordItem { +pub enum SemanticWordItem { /// Encountered a word (delimited by spaces) Word(Word), // `\t` or `x09` @@ -209,7 +222,7 @@ pub(crate) fn get_glyphs( let max_horizontal_text_width = if overflow.allows_horizontal_overflow() { None } else { Some(new_size.width) }; // (5) Align text to the left, initial layout of glyphs - let (mut positioned_glyphs, line_break_offsets) = + let (mut positioned_glyphs, line_break_offsets, _, _) = words_to_left_aligned_glyphs(words, &target_font.0, max_horizontal_text_width, &font_metrics); // (6) Add the harfbuzz adjustments to the positioned glyphs @@ -330,7 +343,7 @@ fn scale_words(words: &mut Words, scale_factor: f32) { // we simply scale each glyph position by 13 / 12. This is faster than // re-calculating the font metrics (from Rusttype) each time we scale a // large amount of text. - for word in words.iter_mut() { + for word in words.0.iter_mut() { if let SemanticWordItem::Word(ref mut w) = word { w.glyphs.iter_mut().for_each(|g| g.point.x *= scale_factor); w.total_width *= scale_factor; @@ -342,7 +355,7 @@ fn scale_words(words: &mut Words, scale_factor: f32) { /// /// It is one of the most expensive functions, use with care. pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: Scale) --> Vec +-> Words { use unicode_normalization::UnicodeNormalization; @@ -452,19 +465,21 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: &mut last_glyph); } - words + Words(words) } // First pass: calculate if the words will overflow (using the tabs) #[inline(always)] fn estimate_overflow_pass_1( - words: &[SemanticWordItem], + words: &Words, rect_dimensions: &TypedSize2D, font_metrics: &FontMetrics, overflow: &LayoutOverflow) -> TextOverflowPass1 { use self::SemanticWordItem::*; + + let words = &words.0; let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; @@ -570,14 +585,14 @@ fn estimate_overflow_pass_1( #[inline(always)] fn estimate_overflow_pass_2( - words: &[SemanticWordItem], + words: &Words, rect_dimensions: &TypedSize2D, font_metrics: &FontMetrics, overflow: &LayoutOverflow, scrollbar_info: &ScrollbarInfo, pass1: TextOverflowPass1) -> (TypedSize2D, TextOverflowPass2) -{ +{ let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; let mut new_size = *rect_dimensions; @@ -641,12 +656,14 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) /// the rectangle horizontally #[inline(always)] fn words_to_left_aligned_glyphs<'a>( - words: &[SemanticWordItem], + words: &Words, font: &Font<'a>, max_horizontal_width: Option, font_metrics: &FontMetrics) --> (Vec, Vec<(usize, f32)>) +-> (Vec, Vec<(usize, f32)>, f32, f32) { + let words = &words.0; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, @@ -736,6 +753,9 @@ fn words_to_left_aligned_glyphs<'a>( } } + let min_enclosing_width = max_word_caret; + let min_enclosing_height = (current_line_num as f32 * vertical_advance) + offset_top; + let line_break_offsets = line_break_offsets.into_iter().map(|(line, space_r)| { let space_r = match space_r { WordCaretMax::SomeMaxWidth(s) => s, @@ -744,7 +764,7 @@ fn words_to_left_aligned_glyphs<'a>( (line, space_r) }).collect(); - (left_aligned_glyphs, line_break_offsets) + (left_aligned_glyphs, line_break_offsets, min_enclosing_width, min_enclosing_height) } #[inline(always)] @@ -857,15 +877,33 @@ fn add_origin(positioned_glyphs: &mut [GlyphInstance], x: f32, y: f32) // -------------------------- PUBLIC API -------------------------- // -/// Use `calculate_font_metrics` to calculate the `font_metrics` value. -/// -/// This is useful if you need to layout many small texts in a loop, so we don't need to -/// re-calculate the metrics over and over again. +pub type IndexOfLineBreak = usize; +pub type RemainingSpaceToRight = f32; + +/// Returned result from the `layout_text` function +#[derive(Debug, Clone)] +pub struct LayoutTextResult { + /// The words, broken into + pub words: Words, + /// Left-aligned glyphs + pub layouted_glyphs: Vec, + /// The line_breaks contain: + /// + /// - The index of the glyph at which the line breaks (index into the `self.layouted_glyphs`) + /// - How much space each line has (to the right edge of the containing rectangle) + pub line_breaks: Vec<(IndexOfLineBreak, RemainingSpaceToRight)>, + /// Minimal width of the layouted text + pub min_width: f32, + /// Minimal height of the layouted text + pub min_height: f32, +} + +/// Layout a string of text horizontally, given a font with its metrics. pub fn layout_text<'a>( text: &str, font: &Font<'a>, font_metrics: &FontMetrics) --> (Vec, Vec<(usize, f32)>) +-> LayoutTextResult { // NOTE: This function is different from the get_glyphs function that is // used internally to azul. @@ -873,7 +911,12 @@ pub fn layout_text<'a>( // This function simply lays out a text, without trying to fit it into a rectangle. // This function does not calculate any overflow. let words = split_text_into_words(text, font, font_metrics.font_size_no_line_height); - words_to_left_aligned_glyphs(&words, font, None, font_metrics) + let (layouted_glyphs, line_breaks, min_width, min_height) = + words_to_left_aligned_glyphs(&words, font, None, font_metrics); + + LayoutTextResult { + words, layouted_glyphs, line_breaks, min_width, min_height + } } #[test] diff --git a/src/ui_state.rs b/src/ui_state.rs index 5215d1371..bd2267b07 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -3,7 +3,7 @@ use std::{ collections::BTreeMap, }; use { - window::WindowInfo, + window::{WindowInfo, ReadOnlyWindow, WindowId}, traits::Layout, dom::{NODE_ID, CALLBACK_ID, Callback, Dom, On}, app_state::AppState, @@ -31,11 +31,17 @@ impl fmt::Debug for UiState { impl UiState { #[allow(unused_imports, unused_variables)] - pub(crate) fn from_app_state(app_state: &AppState, window_info: WindowInfo) -> Self + pub(crate) fn from_app_state(app_state: &AppState, window_id: WindowId, read_only_window: ReadOnlyWindow) -> Self { use dom::{Dom, On, NodeType}; use std::sync::atomic::Ordering; + let window_info = WindowInfo { + window_id, + window: read_only_window, + texts: &app_state.resources.text_cache, + }; + // Only shortly lock the data to get the dom out let dom: Dom = { let dom_lock = app_state.data.lock().unwrap(); diff --git a/src/window.rs b/src/window.rs index 8c5609a0a..064f93752 100644 --- a/src/window.rs +++ b/src/window.rs @@ -33,6 +33,7 @@ use { cache::{EditVariableCache, DomTreeCache}, id_tree::NodeId, compositor::Compositor, + text_cache::TextCache, app::FrameEventInfo, }; @@ -159,9 +160,10 @@ impl Drop for ReadOnlyWindow { } } -pub struct WindowInfo { +pub struct WindowInfo<'a> { pub window_id: WindowId, pub window: ReadOnlyWindow, + pub texts: &'a TextCache, } impl fmt::Debug for FakeWindow { From ab51fc1c4b7eef82e5db088d5a3e8cf2b4453421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 18 Jul 2018 08:30:21 +0200 Subject: [PATCH 154/868] Added VectorizedFont in the SVG component This enables the user to extract the fonts glyph data, build and triangulate the polygons once (since the glyphs inside of a font never change) and then layout text with polygon-like characters, which is quick for zooming / path-based rendering. --- src/widgets/mod.rs | 5 +++- src/widgets/svg.rs | 59 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 360fcaf50..adec511a9 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,6 +3,9 @@ pub mod button; pub mod label; // Re-export widgets -pub use self::svg::{Svg, SvgLayerId, SvgLayer, LayerType, SvgStyle, SvgLayerType, SvgWorldPixel, SvgCache}; +pub use self::svg::{ + Svg, SvgLayerId, SvgLayer, LayerType, + SvgStyle, SvgLayerType, SvgWorldPixel, + SvgCache, VectorizedFont, VectorizedFontCache}; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 8112d4789..5cb737439 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -28,12 +28,14 @@ use lyon::{ }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF}; +use rusttype::{Font, Glyph, GlyphId}; use { FastHashMap, dom::{Dom, NodeType, Callback}, traits::Layout, id_tree::NonZeroUsizeHack, window::ReadOnlyWindow, + css_parser::FontId, }; static SVG_LAYER_ID: AtomicUsize = AtomicUsize::new(0); @@ -798,23 +800,56 @@ implement_vertex!(SvgVert, xy, normal); #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; -use rusttype::{Font, GlyphId}; +/// A vectorized font holds the glyphs for a given font, but in a vector format +pub struct VectorizedFont { + /// Glyph -> Polygon map + pub(crate) glyph_map: FastHashMap, +} + +impl VectorizedFont { + pub fn from_font(font: &Font) -> Self { + let mut glyph_map = (0x0000..0xffff) + .filter_map(|i| { + let g = font.glyph(GlyphId(i)); + if g.id() == GlyphId(0) { + None + } else { + Some(g) + } + }) + .map(|g| (g.id(), glyph_to_svg_layer_type(g))) + .collect::>(); + + glyph_map.insert(GlyphId(0), glyph_to_svg_layer_type(font.glyph(GlyphId(0)))); + + Self { glyph_map } + } +} + +fn glyph_to_svg_layer_type<'a>(glyph: Glyph<'a>) -> SvgLayerType { + SvgLayerType::Text(glyph + .standalone() + .get_data() + .unwrap().shape + .as_ref() + .unwrap() + .iter() + .map(svg_to_lyon::rusttype_glyph_to_path_events) + .collect()) +} + +pub struct VectorizedFontCache { + /// Font -> Vectorized glyph map + vectorized_fonts: FastHashMap, +} impl SvgLayerType { pub fn from_character(ch: char, font: &Font) -> (GlyphId, Self) { let glyph = font.glyph(ch); - let path_events = glyph - .standalone() - .get_data() - .unwrap().shape - .as_ref() - .unwrap() - .iter() - .map(svg_to_lyon::rusttype_glyph_to_path_events) - .collect(); - - (glyph.id(), SvgLayerType::Text(path_events)) + let glyph_id = glyph.id(); + let text_layer = glyph_to_svg_layer_type(glyph); + (glyph_id, text_layer) } pub fn tesselate(&self, tolerance: f32, stroke: Option) From 1ce2c95347bed0dfb350ce05b1f815b443c21232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 20 Jul 2018 06:54:53 +0200 Subject: [PATCH 155/868] Upgraded webrender, tweaked alignment & windows styles --- .travis.yml | 3 +++ Cargo.toml | 6 +++--- examples/debug.rs | 6 +++--- src/compositor.rs | 1 - src/styles/native_windows.css | 17 +++++++---------- src/text_layout.rs | 26 +++++++++++++------------- src/window.rs | 2 +- 7 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2e0e41aac..735883d6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,9 @@ os: matrix: fast_finish: true +notifications: + email: false + # We can't test OpenGL 3.2 on Travis, the shader compilation fails # because glium does a check first if it has a OGL 3.2 context script: diff --git a/Cargo.toml b/Cargo.toml index f646b8adc..0f5a53e01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ cassowary = "0.3.0" simplecss = "0.1.0" twox-hash = "1.1.0" glium = "0.22.0" -gleam = "0.5" -euclid = "0.17" +gleam = "0.6" +euclid = "0.18" image = "0.19.0" rusttype = { git = "https://github.com/fschutt/rusttype" } app_units = "0.6" @@ -46,7 +46,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "915ec3a047d5991284bf5655df3b043e6b9756fa" +rev = "2cb682553816200bb74ce75d3851753bc122f488" [features] default = ["logging"] diff --git a/examples/debug.rs b/examples/debug.rs index 9ccf27ea1..815451568 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -68,7 +68,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv open_file_dialog(None, None) .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { - + let mut svg_cache = SvgCache::empty(); let (font, _) = app_state.get_font(&FontId::BuiltinFont("sans-serif"))?; @@ -81,7 +81,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let text_layer_id = svg_cache.add_layer(svg_layer); svg_layers.push(text_layer_id); - { + { use azul::text_layout::*; let font_metrics = FontMetrics::new(font, &FontSize::px(10.0), None); let layout = layout_text("Hello World", font, &font_metrics); @@ -96,7 +96,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv pan_horz: 0.0, pan_vert: 0.0, })); - + Some(UpdateScreen::Redraw) }) .unwrap_or_else(|| { diff --git a/src/compositor.rs b/src/compositor.rs index 6413397bd..fcd6d5802 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -72,7 +72,6 @@ impl ExternalImageHandler for Compositor { fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) { TO_DELETE_TEXTURES.lock().unwrap().insert(key); - // ACTIVE_GL_TEXTURES.lock().unwrap().remove(&key); } } diff --git a/src/styles/native_windows.css b/src/styles/native_windows.css index 72140ee2f..ab40fb82a 100644 --- a/src/styles/native_windows.css +++ b/src/styles/native_windows.css @@ -1,19 +1,16 @@ .__azul-native-button { - border: 1px solid #b7b7b7; - border-radius: 5.5px; - box-shadow: 0px 0px 3px #c5c5c5ad; - background: linear-gradient(#fcfcfc, #efefef); - width: [[ my_id | 200px ]]; - height: 200px; - min-height: 400px; + border: 1px solid #3399ff; /* inactive: #acacac */ + background: linear-gradient(#f0f0f0, #e5e5e5); + width: 85px; + height: 21px; text-align: center; flex-direction: column; justify-content: center; } * { - font-size: 14px; + font-size: 12px; font-family: sans-serif; - color: #4c4c4c; - background-color: #e7e7e7; + color: #000; + background-color: #f0f0f0; /* Windows Background color, rgb(240, 240, 240) */ } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index 294a00340..30df8ae24 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -19,11 +19,11 @@ pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; -/// Words are a collection of glyph information, i.e. how much -/// horizontal space each of the words in a text block and how much +/// Words are a collection of glyph information, i.e. how much +/// horizontal space each of the words in a text block and how much /// space each individual glyph take up. /// -/// This is important for calculating metrics such as the minimal +/// This is important for calculating metrics such as the minimal /// bounding box of a block of text, for example - without actually /// acessing the font at all. /// @@ -129,7 +129,7 @@ pub(crate) struct ScrollbarInfo { pub(crate) background_color: BackgroundColor, } -/// Temporary struct that contains various metrics related to a font - +/// Temporary struct that contains various metrics related to a font - /// useful so we don't have to access the font to look up certain widths #[derive(Debug, Copy, Clone)] pub struct FontMetrics { @@ -266,7 +266,7 @@ fn calculate_font_metrics<'a>(font: &Font<'a>, font_size: &FontSize, line_height let v_metrics_scaled = font.v_metrics(font_size_with_line_height); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - let offset_top = v_metrics_scaled.ascent / 2.0; + let offset_top = v_advance_scaled / 2.0; FontMetrics { vertical_advance: v_advance_scaled, @@ -478,7 +478,7 @@ fn estimate_overflow_pass_1( -> TextOverflowPass1 { use self::SemanticWordItem::*; - + let words = &words.0; let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; @@ -592,7 +592,7 @@ fn estimate_overflow_pass_2( scrollbar_info: &ScrollbarInfo, pass1: TextOverflowPass1) -> (TypedSize2D, TextOverflowPass2) -{ +{ let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; let mut new_size = *rect_dimensions; @@ -883,7 +883,7 @@ pub type RemainingSpaceToRight = f32; /// Returned result from the `layout_text` function #[derive(Debug, Clone)] pub struct LayoutTextResult { - /// The words, broken into + /// The words, broken into pub words: Words, /// Left-aligned glyphs pub layouted_glyphs: Vec, @@ -900,9 +900,9 @@ pub struct LayoutTextResult { /// Layout a string of text horizontally, given a font with its metrics. pub fn layout_text<'a>( - text: &str, - font: &Font<'a>, - font_metrics: &FontMetrics) + text: &str, + font: &Font<'a>, + font_metrics: &FontMetrics) -> LayoutTextResult { // NOTE: This function is different from the get_glyphs function that is @@ -911,9 +911,9 @@ pub fn layout_text<'a>( // This function simply lays out a text, without trying to fit it into a rectangle. // This function does not calculate any overflow. let words = split_text_into_words(text, font, font_metrics.font_size_no_line_height); - let (layouted_glyphs, line_breaks, min_width, min_height) = + let (layouted_glyphs, line_breaks, min_width, min_height) = words_to_left_aligned_glyphs(&words, font, None, font_metrics); - + LayoutTextResult { words, layouted_glyphs, line_breaks, min_width, min_height } diff --git a/src/window.rs b/src/window.rs index 064f93752..2dfb7ea43 100644 --- a/src/window.rs +++ b/src/window.rs @@ -703,7 +703,7 @@ impl Window { solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); renderer.set_external_image_handler(Box::new(Compositor::default())); - + let window = Window { events_loop: events_loop, state: options.state, From 177df307e5625f7d4ed44b8afe22964b0147adba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 20 Jul 2018 11:03:20 +0200 Subject: [PATCH 156/868] Added font drawing for multiple characters in SVG, fixed various crashes Later on we need to develop some kind of nicer and safer API for building texts. Right now there is no text layout being done, but drawing the text itself takes only one draw call. --- examples/debug.rs | 81 +++++++++--- src/lib.rs | 1 + src/logging.rs | 1 - src/resources.rs | 2 +- src/ui_state.rs | 4 +- src/widgets/mod.rs | 8 +- src/widgets/svg.rs | 312 +++++++++++++++++++++++++++++++-------------- src/window.rs | 3 +- 8 files changed, 292 insertions(+), 120 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 815451568..6aa6a1327 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -12,10 +12,14 @@ pub struct MyAppData { pub map: Option, } +// need: (Style, VertexBuffer, IndexBuffer) +// TODO: This will be slow at first if we don't cache this + #[derive(Debug)] pub struct Map { pub cache: SvgCache, pub layers: Vec, + pub font_cache: VectorizedFontCache, pub zoom: f64, pub pan_horz: f64, pub pan_vert: f64, @@ -26,7 +30,7 @@ impl Layout for MyAppData { -> Dom { if let Some(map) = &self.map { - Svg::with_layers(map.layers.clone()) + Svg::with_layers(build_layers(&map.layers, &map.font_cache, &info.resources)) .with_pan(map.pan_horz as f32, map.pan_vert as f32) .with_zoom(map.zoom as f32) .dom(&info.window, &map.cache) @@ -40,6 +44,58 @@ impl Layout for MyAppData { } } +const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); + +fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFontCache, resources: &AppResources) +-> Vec +{ + let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); + + // layout the texts + use azul::text_layout::*; + + let cur_string = "HelloWorld"; + + let font = resources.get_font(&FONT_ID).unwrap(); + let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); + + // let font_metrics = FontMetrics::new(&font.0, &FontSize::px(10.0), None); + // let layout = layout_text(&cur_string, &font.0, &font_metrics); + + let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); + let (fill, stroke) = { + + let mut fill = None; + let mut stroke = None; + + // TODO: calculate the offset of each glyph vertex and apply it here + // NOTE: cant use spaces here, this will panic! + + if style.fill.is_some() { + let mut fill_buf = Vec::new(); + for gid in cur_string.chars().map(|g| font.0.glyph(g).id()) { + if let Some(s) = vectorized_font.get_fill_vertices(&gid){ fill_buf.push(s); } + } + fill = Some(join_vertex_buffers(&fill_buf)); + } + + if style.stroke.is_some() { + let mut stroke_buf = Vec::new(); + for gid in cur_string.chars().map(|g| font.0.glyph(g).id()) { + if let Some(s) = vectorized_font.get_stroke_vertices(&gid){ stroke_buf.push(s); } + } + stroke = Some(join_vertex_buffers(&stroke_buf)); + } + + (fill.and_then(|s| Some(VerticesIndicesBuffer { vertices: s.0, indices: s.1 })), + stroke.and_then(|s| Some(VerticesIndicesBuffer { vertices: s.0, indices: s.1 }))) + }; + + layers.push(SvgLayerResource::Direct { style, fill, stroke }); + + layers +} + fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { @@ -70,27 +126,16 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv .and_then(|contents| { let mut svg_cache = SvgCache::empty(); + let svg_layers = svg_cache.add_svg(&contents).ok()?; - let (font, _) = app_state.get_font(&FontId::BuiltinFont("sans-serif"))?; - - let text_layer = LayerType::from_single_layer(SvgLayerType::from_character('a', font).1); - let svg_layer = SvgLayer::default_from_layer(text_layer, SvgStyle::filled(ColorU { r: 0, b: 0, g: 0, a: 200 })); - - let mut svg_layers = svg_cache.add_svg(&contents).ok()?; - - let text_layer_id = svg_cache.add_layer(svg_layer); - svg_layers.push(text_layer_id); - - { - use azul::text_layout::*; - let font_metrics = FontMetrics::new(font, &FontSize::px(10.0), None); - let layout = layout_text("Hello World", font, &font_metrics); - println!("text layout glyphs: {:?}", layout.layouted_glyphs); - println!("text min w: {} min h: {}", layout.min_width, layout.min_height); - } + // Pre-vectorize the glyphs of the font into vertex buffers + let (font, _) = app_state.get_font(&FONT_ID)?; + let mut vectorized_font_cache = VectorizedFontCache::new(); + vectorized_font_cache.insert_if_not_exist(FONT_ID, font); app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, + font_cache: vectorized_font_cache, layers: svg_layers, zoom: 1.0, pan_horz: 0.0, diff --git a/src/lib.rs b/src/lib.rs index 3d38713de..0852f2a04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,6 +152,7 @@ pub mod prelude { }; pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; pub use rusttype::Font; + pub use resources::AppResources; #[cfg(feature = "logging")] pub use log::LevelFilter; diff --git a/src/logging.rs b/src/logging.rs index 8ed307333..d19292b87 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -86,7 +86,6 @@ pub(crate) fn set_up_panic_hooks() { let backtrace_str = backtrace_str_old .lines() .filter(|l| !l.is_empty()) - .skip(11) .collect::>() .join("\r\n"); diff --git a/src/resources.rs b/src/resources.rs index bb79e412f..4ed0100c2 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -33,7 +33,7 @@ use rusttype::Font; /// /// Images and fonts can be references across window contexts /// (not yet tested, but should work). -pub(crate) struct AppResources<'a> { +pub struct AppResources<'a> { /// When looking up images, there are two sources: Either the indirect way via using a /// CssId (which is a String) or a direct ImageId. The indirect way requires one extra /// lookup (to map from the stringified ID to the actual image ID). This is what this diff --git a/src/ui_state.rs b/src/ui_state.rs index bd2267b07..57c08a05c 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -39,7 +39,7 @@ impl UiState { let window_info = WindowInfo { window_id, window: read_only_window, - texts: &app_state.resources.text_cache, + resources: &app_state.resources, }; // Only shortly lock the data to get the dom out @@ -48,7 +48,7 @@ impl UiState { #[cfg(test)]{ Dom::::new(NodeType::Div) } - + #[cfg(not(test))]{ dom_lock.layout(window_info) } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index adec511a9..c79ce5e38 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -4,8 +4,10 @@ pub mod label; // Re-export widgets pub use self::svg::{ - Svg, SvgLayerId, SvgLayer, LayerType, - SvgStyle, SvgLayerType, SvgWorldPixel, - SvgCache, VectorizedFont, VectorizedFontCache}; + Svg, SvgLayerId, SvgLayer, LayerType, + SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, + SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, + join_vertex_buffers +}; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 5cb737439..0f92fb054 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -65,6 +65,7 @@ pub fn new_svg_layer_id() -> SvgLayerId { const SHADER_VERSION_GL: &str = "#version 150"; const SHADER_VERSION_GLES: &str = "#version 300 es"; +const DEFAULT_GLYPH_TOLERANCE: f32 = 0.01; const SVG_VERTEX_SHADER: &str = " @@ -312,6 +313,32 @@ impl Default for SvgCache { } } +fn fill_vertex_buffer_cache<'a, F: Facade>( + id: &SvgLayerId, + rmut: &'a mut FastHashMap, IndexBuffer)>, + rnotmut: &FastHashMap, Vec)>, + window: &F) + -> Option<&'a (VertexBuffer, IndexBuffer)> +{ + use std::collections::hash_map::Entry::*; + + match rmut.entry(*id) { + Occupied(_) => { }, + Vacant(v) => { + let (vbuf, ibuf) = match rnotmut.get(id).as_ref() { + Some(s) => s, + None => return None, + }; + let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); + ; + v.insert((vertex_buffer, index_buffer)); + } + } + + rmut.get(id) +} + impl SvgCache { /// Creates an empty SVG cache @@ -329,7 +356,7 @@ impl SvgCache { } fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) - -> &'a (VertexBuffer, IndexBuffer) + -> Option<&'a (VertexBuffer, IndexBuffer)> { use std::collections::hash_map::Entry::*; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; @@ -337,12 +364,7 @@ impl SvgCache { let rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; let rnotmut = &self.stroke_gpu_ready_to_upload_cache; - rmut.entry(*id).or_insert_with(|| { - let (vbuf, ibuf) = rnotmut.get(id).as_ref().unwrap(); - let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); - let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); - (vertex_buffer, index_buffer) - }) + Some(fill_vertex_buffer_cache(id, rmut, rnotmut, window)?) } /// Note: panics if the ID isn't found. @@ -350,7 +372,7 @@ impl SvgCache { /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` /// in sync, a panic should never happen fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) - -> &'a (VertexBuffer, IndexBuffer) + -> Option<&'a (VertexBuffer, IndexBuffer)> { use std::collections::hash_map::Entry::*; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; @@ -368,12 +390,7 @@ impl SvgCache { let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; let rnotmut = &self.gpu_ready_to_upload_cache; - rmut.entry(*id).or_insert_with(|| { - let (vbuf, ibuf) = rnotmut.get(id).as_ref().unwrap(); - let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); - let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); - (vertex_buffer, index_buffer) - }) + Some(fill_vertex_buffer_cache(id, rmut, rnotmut, window)?) } fn get_style(&self, id: &SvgLayerId) @@ -382,25 +399,12 @@ impl SvgCache { self.layers.get(id).as_ref().unwrap().style } -} - -impl fmt::Debug for SvgCache { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for layer in self.layers.keys() { - write!(f, "{:?}", layer)?; - } - Ok(()) - } -} - -impl SvgCache { - pub fn add_layer(&mut self, layer: SvgLayer) -> SvgLayerId { // TODO: set tolerance based on zoom let new_svg_id = new_svg_layer_id(); let ((vertex_buf, index_buf), opt_stroke) = - tesselate_layer_data(&layer.data, 0.01, layer.style.stroke.and_then(|s| Some(s.1.clone()))); + tesselate_layer_data(&layer.data, DEFAULT_GLYPH_TOLERANCE, layer.style.stroke.and_then(|s| Some(s.1.clone()))); self.gpu_ready_to_upload_cache.insert(new_svg_id, (vertex_buf, index_buf)); @@ -454,10 +458,20 @@ impl SvgCache { } } +impl fmt::Debug for SvgCache { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for layer in self.layers.keys() { + write!(f, "{:?}", layer)?; + } + Ok(()) + } +} + +const GL_RESTART_INDEX: u32 = ::std::u32::MAX; + fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: Option) -> ((Vec, Vec), Option<(Vec, Vec)>) { - const GL_RESTART_INDEX: u32 = ::std::u32::MAX; let mut last_index = 0; let mut vertex_buf = Vec::::new(); @@ -493,6 +507,26 @@ fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: } } +/// Joins multiple SvgVert buffers to one and calculates the indices +/// +/// TODO: Wrap this in a nicer API +pub fn join_vertex_buffers(input: &[&VertexBuffers]) -> (Vec, Vec) { + + let mut last_index = 0; + let mut vertex_buf = Vec::::new(); + let mut index_buf = Vec::::new(); + + for VertexBuffers { vertices, indices } in input { + let vertices_len = vertices.len(); + vertex_buf.extend(vertices.into_iter()); + index_buf.extend(indices.into_iter().map(|i| *i as u32 + last_index as u32)); + index_buf.push(GL_RESTART_INDEX); + last_index += vertices_len; + } + + (vertex_buf, index_buf) +} + #[derive(Debug)] pub enum SvgParseError { /// Syntax error in the Svg @@ -786,7 +820,6 @@ pub enum SvgLayerType { Polygon(Vec), Circle(SvgCircle), Rect(SvgRect), - Text(Vec), } #[derive(Debug, Copy, Clone)] @@ -801,57 +834,107 @@ implement_vertex!(SvgVert, xy, normal); pub struct SvgWorldPixel; /// A vectorized font holds the glyphs for a given font, but in a vector format +#[derive(Debug, Clone)] pub struct VectorizedFont { /// Glyph -> Polygon map - pub(crate) glyph_map: FastHashMap, + glyph_polygon_map: FastHashMap>, + /// Glyph -> Stroke map + glyph_stroke_map: FastHashMap>, } impl VectorizedFont { pub fn from_font(font: &Font) -> Self { - let mut glyph_map = (0x0000..0xffff) - .filter_map(|i| { + + let mut glyph_polygon_map = FastHashMap::default(); + let mut glyph_stroke_map = FastHashMap::default(); + + let stroke_options = SvgStrokeOptions::default(); + + // TODO: In a regular font (4000 characters), this is pretty slow! + + for g in (0..font.glyph_count() as u32).filter_map(|i| { let g = font.glyph(GlyphId(i)); if g.id() == GlyphId(0) { None } else { Some(g) } - }) - .map(|g| (g.id(), glyph_to_svg_layer_type(g))) - .collect::>(); + }) { + // Tesselate all the font vertices and store them in the glyph map + let glyph_id = g.id(); + if let Some((polygon_verts, stroke_verts)) = + glyph_to_svg_layer_type(g) + .and_then(|poly| Some(poly.tesselate(DEFAULT_GLYPH_TOLERANCE, Some(stroke_options)))) + { + // safe unwrap, since we set the stroke_options to Some() + glyph_polygon_map.insert(glyph_id, polygon_verts); + glyph_stroke_map.insert(glyph_id, stroke_verts.unwrap()); + } + } - glyph_map.insert(GlyphId(0), glyph_to_svg_layer_type(font.glyph(GlyphId(0)))); + if let Some((polygon_verts_zero, stroke_verts_zero)) = + glyph_to_svg_layer_type(font.glyph(GlyphId(0))) + .and_then(|poly| Some(poly.tesselate(DEFAULT_GLYPH_TOLERANCE, Some(stroke_options)))) + { + glyph_polygon_map.insert(GlyphId(0), polygon_verts_zero); + glyph_stroke_map.insert(GlyphId(0), stroke_verts_zero.unwrap()); + } + + Self { glyph_polygon_map, glyph_stroke_map } + } - Self { glyph_map } + pub fn get_fill_vertices(&self, id: &GlyphId) -> Option<&VertexBuffers> { + let result = self.glyph_polygon_map.get(id); + result + } + + pub fn get_stroke_vertices(&self, id: &GlyphId) -> Option<&VertexBuffers> { + self.glyph_stroke_map.get(id) } } -fn glyph_to_svg_layer_type<'a>(glyph: Glyph<'a>) -> SvgLayerType { - SvgLayerType::Text(glyph +/// Converts a glyph to a `SvgLayerType::Polygon` +fn glyph_to_svg_layer_type<'a>(glyph: Glyph<'a>) -> Option { + Some(SvgLayerType::Polygon(glyph .standalone() - .get_data() - .unwrap().shape - .as_ref() - .unwrap() + .get_data()?.shape + .as_ref()? .iter() .map(svg_to_lyon::rusttype_glyph_to_path_events) - .collect()) + .collect())) } +#[derive(Debug, Default)] pub struct VectorizedFontCache { /// Font -> Vectorized glyph map vectorized_fonts: FastHashMap, } -impl SvgLayerType { +impl VectorizedFontCache { + + pub fn new() -> Self { + Self::default() + } + + pub fn insert_if_not_exist(&mut self, id: FontId, font: &Font) { + self.vectorized_fonts.entry(id).or_insert_with(|| VectorizedFont::from_font(font)); + } - pub fn from_character(ch: char, font: &Font) -> (GlyphId, Self) { - let glyph = font.glyph(ch); - let glyph_id = glyph.id(); - let text_layer = glyph_to_svg_layer_type(glyph); - (glyph_id, text_layer) + pub fn insert(&mut self, id: FontId, font: VectorizedFont) { + self.vectorized_fonts.insert(id, font); } + pub fn get_font(&self, id: &FontId) -> Option<&VectorizedFont> { + self.vectorized_fonts.get(id) + } + + pub fn remove_font(&mut self, id: &FontId) { + self.vectorized_fonts.remove(id); + } +} + +impl SvgLayerType { + pub fn tesselate(&self, tolerance: f32, stroke: Option) -> (VertexBuffers, Option>) { @@ -863,7 +946,7 @@ impl SvgLayerType { }); match self { - SvgLayerType::Polygon(p) | SvgLayerType::Text(p) => { + SvgLayerType::Polygon(p) => { let mut builder = Builder::with_capacity(p.len()).flattened(tolerance); for event in p { builder.path_event(*event); @@ -1128,10 +1211,10 @@ mod svg_to_lyon { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct Svg { /// Currently active layers - pub layers: Vec, + pub layers: Vec, /// Pan (horizontal, vertical) in pixels pub pan: (f32, f32), /// 1.0 = default zoom @@ -1151,10 +1234,26 @@ impl Default for Svg { } } +#[derive(Debug, Clone)] +pub enum SvgLayerResource { + Reference(SvgLayerId), + Direct { + style: SvgStyle, + fill: Option, + stroke: Option, + }, +} + +#[derive(Debug, Clone)] +pub struct VerticesIndicesBuffer { + pub vertices: Vec, + pub indices: Vec, +} + impl Svg { #[inline] - pub fn with_layers(layers: Vec) + pub fn with_layers(layers: Vec) -> Self { Self { layers: layers, .. Default::default() } @@ -1209,53 +1308,78 @@ impl Svg { { let mut surface = tex.as_surface(); - for layer_id in &self.layers { + for layer in &self.layers { use palette::Srgba; - let style = svg_cache.get_style(layer_id); - if let Some(color) = style.fill { - let color: ColorF = color.into(); - let (vertex_buffer, index_buffer) = svg_cache.get_vertices_and_indices(window, layer_id); - let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, - color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; + let style = match layer { + SvgLayerResource::Reference(layer_id) => { svg_cache.get_style(layer_id) }, + SvgLayerResource::Direct { style, .. } => *style, + }; - surface.draw(vertex_buffer, index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + if let Some(color) = style.fill { + let mut direct_fill = None; + if let Some((fill_vertices, fill_indices)) = match &layer { + SvgLayerResource::Reference(layer_id) => svg_cache.get_vertices_and_indices(window, layer_id), + SvgLayerResource::Direct { fill, .. } => fill.as_ref().and_then(|f| { + let vertex_buffer = VertexBuffer::new(window, &f.vertices).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, &f.indices).unwrap(); + direct_fill = Some((vertex_buffer, index_buffer)); + Some(direct_fill.as_ref().unwrap()) + })} + { + let color: ColorF = color.into(); + let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, + color.alpha as f32 + ), + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, + }; + + surface.draw(fill_vertices, fill_indices, &shader.program, &uniforms, &draw_options).unwrap(); + } } if let Some((stroke_color, _)) = style.stroke { - let stroke_color: ColorF = stroke_color.into(); - let (stroke_vertex_buffer, stroke_index_buffer) = svg_cache.get_stroke_vertices_and_indices(window, layer_id); - let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - stroke_color.color.red as f32, - stroke_color.color.green as f32, - stroke_color.color.blue as f32, - stroke_color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; - surface.draw(stroke_vertex_buffer, stroke_index_buffer, &shader.program, &uniforms, &draw_options).unwrap(); + let mut direct_stroke = None; + if let Some((stroke_vertices, stroke_indices)) = match &layer { + SvgLayerResource::Reference(layer_id) => svg_cache.get_stroke_vertices_and_indices(window, layer_id), + SvgLayerResource::Direct { stroke, .. } => stroke.as_ref().and_then(|f| { + let vertex_buffer = VertexBuffer::new(window, &f.vertices).unwrap(); + let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, &f.indices).unwrap(); + direct_stroke = Some((vertex_buffer, index_buffer)); + Some(direct_stroke.as_ref().unwrap()) + })} + { + let stroke_color: ColorF = stroke_color.into(); + let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + stroke_color.color.red as f32, + stroke_color.color.green as f32, + stroke_color.color.blue as f32, + stroke_color.alpha as f32 + ), + offset: (self.pan.0, self.pan.1), + zoom: self.zoom, + }; + + surface.draw(stroke_vertices, stroke_indices, &shader.program, &uniforms, &draw_options).unwrap(); + } } } } diff --git a/src/window.rs b/src/window.rs index 2dfb7ea43..1d6ea9aba 100644 --- a/src/window.rs +++ b/src/window.rs @@ -35,6 +35,7 @@ use { compositor::Compositor, text_cache::TextCache, app::FrameEventInfo, + resources::AppResources, }; /// azul-internal ID for a window @@ -163,7 +164,7 @@ impl Drop for ReadOnlyWindow { pub struct WindowInfo<'a> { pub window_id: WindowId, pub window: ReadOnlyWindow, - pub texts: &'a TextCache, + pub resources: &'a AppResources<'a>, } impl fmt::Debug for FakeWindow { From b88df969e54f9577fd45ae0b03a0c2eb9aa38273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 22 Jul 2018 08:03:38 +0200 Subject: [PATCH 157/868] Refactored and de-duplicated SVG text drawing code --- examples/debug.rs | 48 +++++++---------- src/text_layout.rs | 4 +- src/widgets/mod.rs | 3 +- src/widgets/svg.rs | 126 ++++++++++++++++++++++++++------------------- 4 files changed, 96 insertions(+), 85 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 6aa6a1327..931877daa 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -59,39 +59,29 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let font = resources.get_font(&FONT_ID).unwrap(); let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); - // let font_metrics = FontMetrics::new(&font.0, &FontSize::px(10.0), None); - // let layout = layout_text(&cur_string, &font.0, &font_metrics); + let font_metrics = FontMetrics::new(&font.0, &FontSize::px(10.0), None); + let layout = layout_text(&cur_string, &font.0, &font_metrics); let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - let (fill, stroke) = { - let mut fill = None; - let mut stroke = None; - - // TODO: calculate the offset of each glyph vertex and apply it here - // NOTE: cant use spaces here, this will panic! - - if style.fill.is_some() { - let mut fill_buf = Vec::new(); - for gid in cur_string.chars().map(|g| font.0.glyph(g).id()) { - if let Some(s) = vectorized_font.get_fill_vertices(&gid){ fill_buf.push(s); } - } - fill = Some(join_vertex_buffers(&fill_buf)); - } - - if style.stroke.is_some() { - let mut stroke_buf = Vec::new(); - for gid in cur_string.chars().map(|g| font.0.glyph(g).id()) { - if let Some(s) = vectorized_font.get_stroke_vertices(&gid){ stroke_buf.push(s); } - } - stroke = Some(join_vertex_buffers(&stroke_buf)); - } - - (fill.and_then(|s| Some(VerticesIndicesBuffer { vertices: s.0, indices: s.1 })), - stroke.and_then(|s| Some(VerticesIndicesBuffer { vertices: s.0, indices: s.1 }))) - }; + fn get_vertices<'a>( + glyph_ids: &[GlyphInstance], + vectorized_font: &'a VectorizedFont, + transform_func: fn(&'a VectorizedFont, &GlyphId) -> Option<&'a VertexBuffers> + ) -> VerticesIndicesBuffer + { + let fill_buf = glyph_ids.iter().filter_map(|gid| { + transform_func(vectorized_font, &GlyphId(gid.index)) + }).collect::>(); + let s = join_vertex_buffers(&fill_buf); + VerticesIndicesBuffer { vertices: s.0, indices: s.1 } + } - layers.push(SvgLayerResource::Direct { style, fill, stroke }); + layers.push(SvgLayerResource::Direct { + style, + fill: style.fill.and_then(|_| Some(get_vertices(&layout.layouted_glyphs, vectorized_font, get_fill_vertices))), + stroke: style.stroke.and_then(|_| Some(get_vertices(&layout.layouted_glyphs, vectorized_font, get_stroke_vertices))), + }); layers } diff --git a/src/text_layout.rs b/src/text_layout.rs index 30df8ae24..ad00cbe19 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,6 +1,6 @@ #![allow(unused_variables, dead_code)] -use webrender::api::{LayoutPixel, GlyphInstance}; +use webrender::api::LayoutPixel; use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; use { @@ -13,6 +13,8 @@ use { text_cache::{TextId, TextCache}, }; +pub use webrender::api::GlyphInstance; + /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c79ce5e38..f062eb5c7 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -7,7 +7,8 @@ pub use self::svg::{ Svg, SvgLayerId, SvgLayer, LayerType, SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, - join_vertex_buffers + VertexBuffers, SvgVert, GlyphId, + join_vertex_buffers, get_fill_vertices, get_stroke_vertices, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 0f92fb054..2db1606bc 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -13,7 +13,7 @@ use glium::{ }; use lyon::{ tessellation::{ - VertexBuffers, FillOptions, BuffersBuilder, FillVertex, FillTessellator, + FillOptions, BuffersBuilder, FillVertex, FillTessellator, LineCap, LineJoin, StrokeTessellator, StrokeOptions, StrokeVertex, basic_shapes::{ fill_circle, stroke_circle, fill_rounded_rectangle, @@ -28,7 +28,7 @@ use lyon::{ }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF}; -use rusttype::{Font, Glyph, GlyphId}; +use rusttype::{Font, Glyph}; use { FastHashMap, dom::{Dom, NodeType, Callback}, @@ -38,6 +38,9 @@ use { css_parser::FontId, }; +pub use lyon::tessellation::VertexBuffers; +pub use rusttype::GlyphId; + static SVG_LAYER_ID: AtomicUsize = AtomicUsize::new(0); static SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); static SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); @@ -882,15 +885,14 @@ impl VectorizedFont { Self { glyph_polygon_map, glyph_stroke_map } } +} - pub fn get_fill_vertices(&self, id: &GlyphId) -> Option<&VertexBuffers> { - let result = self.glyph_polygon_map.get(id); - result - } +pub fn get_fill_vertices<'a>(vectorized_font: &'a VectorizedFont, id: &GlyphId) -> Option<&'a VertexBuffers> { + vectorized_font.glyph_polygon_map.get(id) +} - pub fn get_stroke_vertices(&self, id: &GlyphId) -> Option<&VertexBuffers> { - self.glyph_stroke_map.get(id) - } +pub fn get_stroke_vertices<'a>(vectorized_font: &'a VectorizedFont, id: &GlyphId) -> Option<&'a VertexBuffers> { + vectorized_font.glyph_stroke_map.get(id) } /// Converts a glyph to a `SvgLayerType::Polygon` @@ -1293,11 +1295,6 @@ impl Svg { let tex = window.create_texture(window_width as u32, window_height as u32); tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); - let draw_options = DrawParameters { - primitive_restart_index: true, - .. Default::default() - }; - let z_index: f32 = 0.5; let bbox: TypedRect = TypedRect { origin: TypedPoint2D::new(0.0, 0.0), @@ -1305,13 +1302,16 @@ impl Svg { }; let shader = svg_cache.init_shader(window); + let draw_options = DrawParameters { + primitive_restart_index: true, + .. Default::default() + }; + { let mut surface = tex.as_surface(); for layer in &self.layers { - use palette::Srgba; - let style = match layer { SvgLayerResource::Reference(layer_id) => { svg_cache.get_style(layer_id) }, SvgLayerResource::Direct { style, .. } => *style, @@ -1326,26 +1326,18 @@ impl Svg { let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, &f.indices).unwrap(); direct_fill = Some((vertex_buffer, index_buffer)); Some(direct_fill.as_ref().unwrap()) - })} - { - let color: ColorF = color.into(); - let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, - color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; - - surface.draw(fill_vertices, fill_indices, &shader.program, &uniforms, &draw_options).unwrap(); + })} { + draw_vertex_buffer_to_surface( + &mut surface, + &shader.program, + &fill_vertices, + &fill_indices, + &draw_options, + &bbox, + color.into(), + z_index, + self.pan, + self.zoom); } } @@ -1361,24 +1353,17 @@ impl Svg { Some(direct_stroke.as_ref().unwrap()) })} { - let stroke_color: ColorF = stroke_color.into(); - let stroke_color = Srgba::new(stroke_color.r, stroke_color.g, stroke_color.b, stroke_color.a).into_linear(); - - let uniforms = uniform! { - bbox_origin: (bbox.origin.x, bbox.origin.y), - bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), - z_index: z_index, - color: ( - stroke_color.color.red as f32, - stroke_color.color.green as f32, - stroke_color.color.blue as f32, - stroke_color.alpha as f32 - ), - offset: (self.pan.0, self.pan.1), - zoom: self.zoom, - }; - - surface.draw(stroke_vertices, stroke_indices, &shader.program, &uniforms, &draw_options).unwrap(); + draw_vertex_buffer_to_surface( + &mut surface, + &shader.program, + &stroke_vertices, + &stroke_indices, + &draw_options, + &bbox, + stroke_color.into(), + z_index, + self.pan, + self.zoom); } } } @@ -1392,6 +1377,39 @@ impl Svg { } } +fn draw_vertex_buffer_to_surface( + surface: &mut S, + shader: &Program, + vertices: &VertexBuffer, + indices: &IndexBuffer, + draw_options: &DrawParameters, + bbox: &TypedRect, + color: ColorF, + z_index: f32, + pan: (f32, f32), + zoom: f32) +{ + use palette::Srgba; + + let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); + + let uniforms = uniform! { + bbox_origin: (bbox.origin.x, bbox.origin.y), + bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), + z_index: z_index, + color: ( + color.color.red as f32, + color.color.green as f32, + color.color.blue as f32, + color.alpha as f32 + ), + offset: (pan.0, pan.1), + zoom: zoom, + }; + + surface.draw(vertices, indices, shader, &uniforms, draw_options).unwrap(); +} + #[test] fn __codecov_test_widget_svg_file() { From f004f51b2ca69ff2b7e5b77b1d14e7dbf98c3e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 22 Jul 2018 08:44:45 +0200 Subject: [PATCH 158/868] Got SVG text layout somewhat working --- examples/debug.rs | 40 +++++++++++++++++++++++++++++++--------- src/css_parser.rs | 4 ++++ src/widgets/mod.rs | 1 + src/widgets/svg.rs | 34 +++++++++++++++++++++++++++------- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 931877daa..5beb8ad1a 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -59,28 +59,50 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let font = resources.get_font(&FONT_ID).unwrap(); let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); - let font_metrics = FontMetrics::new(&font.0, &FontSize::px(10.0), None); + let font_size = FontSize::px(10.0); + let font_metrics = FontMetrics::new(&font.0, &font_size, None); let layout = layout_text(&cur_string, &font.0, &font_metrics); let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - fn get_vertices<'a>( + fn get_vertices( + font_size: &FontSize, glyph_ids: &[GlyphInstance], - vectorized_font: &'a VectorizedFont, - transform_func: fn(&'a VectorizedFont, &GlyphId) -> Option<&'a VertexBuffers> + vectorized_font: &VectorizedFont, + transform_func: fn(&VectorizedFont, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { - let fill_buf = glyph_ids.iter().filter_map(|gid| { - transform_func(vectorized_font, &GlyphId(gid.index)) - }).collect::>(); + let fill_buf = glyph_ids.iter() + .filter_map(|gid| { + transform_func(vectorized_font, &GlyphId(gid.index)) + .and_then(|vertex_buf| Some((gid, vertex_buf))) + }) + .map(|(gid, mut vertex_buf)| { + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + (gid, vertex_buf) + }) + .map(|(gid, mut vertex_buf)| { + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x, gid.point.y); + vertex_buf + }) + /*.map(|vertex_buf| rotate_buf(vertex_buf, 5.0))*/ + .collect::>(); let s = join_vertex_buffers(&fill_buf); VerticesIndicesBuffer { vertices: s.0, indices: s.1 } } + let fill_vertices = style.fill.and_then(|_| { + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, get_fill_vertices)) + }); + + let stroke_vertices = style.stroke.and_then(|_| { + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, get_stroke_vertices)) + }); + layers.push(SvgLayerResource::Direct { style, - fill: style.fill.and_then(|_| Some(get_vertices(&layout.layouted_glyphs, vectorized_font, get_fill_vertices))), - stroke: style.stroke.and_then(|_| Some(get_vertices(&layout.layouted_glyphs, vectorized_font, get_stroke_vertices))), + fill: fill_vertices, + stroke: stroke_vertices, }); layers diff --git a/src/css_parser.rs b/src/css_parser.rs index ee23f0a53..eefe0ed9b 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1682,6 +1682,10 @@ impl FontSize { pub fn pt(value: f32) -> Self { FontSize(PixelValue::from_metric(CssMetric::Pt, value)) } + + pub fn to_pixels(&self) -> f32 { + self.0.to_pixels() + } } #[derive(Debug, PartialEq, Clone)] diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index f062eb5c7..5e8f4a89c 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,6 +9,7 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, VertexBuffers, SvgVert, GlyphId, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, + scale_vertex_buffer, transform_vertex_buffer, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 2db1606bc..f640c5aec 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -27,7 +27,7 @@ use lyon::{ geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; -use webrender::api::{ColorU, ColorF}; +use webrender::api::{ColorU, ColorF, LayoutPixel}; use rusttype::{Font, Glyph}; use { FastHashMap, @@ -35,7 +35,7 @@ use { traits::Layout, id_tree::NonZeroUsizeHack, window::ReadOnlyWindow, - css_parser::FontId, + css_parser::{FontId, FontSize}, }; pub use lyon::tessellation::VertexBuffers; @@ -513,7 +513,7 @@ fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: /// Joins multiple SvgVert buffers to one and calculates the indices /// /// TODO: Wrap this in a nicer API -pub fn join_vertex_buffers(input: &[&VertexBuffers]) -> (Vec, Vec) { +pub fn join_vertex_buffers(input: &[VertexBuffers]) -> (Vec, Vec) { let mut last_index = 0; let mut vertex_buf = Vec::::new(); @@ -530,6 +530,22 @@ pub fn join_vertex_buffers(input: &[&VertexBuffers]) -> (Vec, (vertex_buf, index_buf) } +pub fn scale_vertex_buffer(input: &mut [SvgVert], scale: &FontSize) { + let real_size = scale.to_pixels(); + let scale_factor = real_size / 1024.0; + for vert in input { + vert.xy.0 *= scale_factor; + vert.xy.1 *= scale_factor; + } +} + +pub fn transform_vertex_buffer(input: &mut [SvgVert], x: f32, y: f32) { + for vert in input { + vert.xy.0 += x; + vert.xy.1 += y; + } +} + #[derive(Debug)] pub enum SvgParseError { /// Syntax error in the Svg @@ -887,12 +903,16 @@ impl VectorizedFont { } } -pub fn get_fill_vertices<'a>(vectorized_font: &'a VectorizedFont, id: &GlyphId) -> Option<&'a VertexBuffers> { - vectorized_font.glyph_polygon_map.get(id) +pub fn get_fill_vertices(vectorized_font: &VectorizedFont, id: &GlyphId) +-> Option> +{ + vectorized_font.glyph_polygon_map.get(id).cloned() } -pub fn get_stroke_vertices<'a>(vectorized_font: &'a VectorizedFont, id: &GlyphId) -> Option<&'a VertexBuffers> { - vectorized_font.glyph_stroke_map.get(id) +pub fn get_stroke_vertices(vectorized_font: &VectorizedFont, id: &GlyphId) +-> Option> +{ + vectorized_font.glyph_stroke_map.get(id).cloned() } /// Converts a glyph to a `SvgLayerType::Polygon` From 82781792c3e44a8d193595da7695806706d2aa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 25 Jul 2018 10:07:17 +0200 Subject: [PATCH 159/868] Worked on bezier / curved text rendering --- examples/debug.rs | 125 ++++++++++++++++++++++++++++++++++++++++----- src/widgets/mod.rs | 3 +- src/widgets/svg.rs | 37 +++++++++++--- 3 files changed, 145 insertions(+), 20 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 5beb8ad1a..675723891 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -46,16 +46,14 @@ impl Layout for MyAppData { const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); +use azul::text_layout::*; + fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFontCache, resources: &AppResources) -> Vec { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); - // layout the texts - use azul::text_layout::*; - - let cur_string = "HelloWorld"; - + let cur_string = "Helloldakjfalfkjadlkfjdsalfkjdsalfkjdsf World"; let font = resources.get_font(&FONT_ID).unwrap(); let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); @@ -65,6 +63,7 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); + // Calculates the layout for one word block fn get_vertices( font_size: &FontSize, glyph_ids: &[GlyphInstance], @@ -72,23 +71,42 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo transform_func: fn(&VectorizedFont, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { + let character_rotations = vec![30.0_f32; glyph_ids.len()]; + let char_offsets = test_bezier_points_offsets(glyph_ids, 0.0); + + println!("char offsets: {:?}", char_offsets); + + assert!(char_offsets.len() == glyph_ids.len()); + let fill_buf = glyph_ids.iter() .filter_map(|gid| { + // 1. Transform glyph to vertex buffer && filter out all glyphs + // that don't have a vertex buffer transform_func(vectorized_font, &GlyphId(gid.index)) .and_then(|vertex_buf| Some((gid, vertex_buf))) }) - .map(|(gid, mut vertex_buf)| { + .zip(character_rotations.into_iter()) + .zip(char_offsets.into_iter()) + .map(|(((gid, mut vertex_buf), char_rot), (char_offset_x, char_offset_y))| { + + // 2. Scale characters to the final size scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - (gid, vertex_buf) - }) - .map(|(gid, mut vertex_buf)| { - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x, gid.point.y); + + // 3. Rotate individual characters inside of the word + let char_angle = char_rot.to_radians(); + let (char_sin, char_cos) = (char_angle.sin(), char_angle.cos()); + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + + // 4. Transform characters to their respective positions + transform_vertex_buffer(&mut vertex_buf.vertices, + (gid.point.x * 2.0) + char_offset_x, + (gid.point.y * 2.0) + char_offset_y); + vertex_buf }) - /*.map(|vertex_buf| rotate_buf(vertex_buf, 5.0))*/ .collect::>(); - let s = join_vertex_buffers(&fill_buf); - VerticesIndicesBuffer { vertices: s.0, indices: s.1 } + + join_vertex_buffers(&fill_buf) } let fill_vertices = style.fill.and_then(|_| { @@ -105,9 +123,90 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo stroke: stroke_vertices, }); + // layers.append(&mut test_bezier_points()); layers } +#[derive(Debug, Copy, Clone)] +struct BezierControlPoint { + x: f32, + y: f32, +} + +impl BezierControlPoint { + /// Distance of two points + pub fn distance(&self, other: &Self) -> f32 { + ((other.x - self.x).powi(2) + (other.y - self.y).powi(2)).sqrt() + } +} + +/// Roughly estimate the length of a bezier curve arc using 10 samples +fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> f32 { + + let mut origin = curve[0]; + let mut total_distance = 0.0; + + for i in 1..10 { + let new_point = get_bezier_point_at(curve, i as f32 / 10.0); + total_distance += origin.distance(&new_point); + origin = new_point; + } + + total_distance += origin.distance(&curve[3]); + total_distance +} + +/// t is between 0.0 and 1.0 on the four points +fn get_bezier_point_at(curve: &[BezierControlPoint;4], t: f32) -> BezierControlPoint { + let one_minus = 1.0 - t; + let one_minus_square = one_minus.powi(2); + let one_minus_cubic = one_minus.powi(3); + + // Bezier curve formula for 4 control points + // (1 - t) + let x = one_minus_cubic * curve[0].x + + 3.0 * one_minus_square * t * curve[1].x + + 3.0 * one_minus * t.powi(2) * curve[2].x + + t.powi(3) * curve[3].x; + + let y = one_minus_cubic * curve[0].y + + 3.0 * one_minus_square * t * curve[1].y + + 3.0 * one_minus * t.powi(2) * curve[2].y + + t.powi(3) * curve[3].y; + + BezierControlPoint { x, y } +} + +fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> Vec<(f32, f32)> { + let test_curve = [ + BezierControlPoint { x: 0.0, y: 0.0 }, + BezierControlPoint { x: 40.0, y: 120.0 }, + BezierControlPoint { x: 80.0, y: 120.0 }, + BezierControlPoint { x: 120.0, y: 0.0 }, + ]; + + let curve_length = estimate_arc_length(&test_curve); + + let mut current_offset = start_offset; + let mut offsets = vec![]; + + for glyph in glyphs { + let char_bezier_pt = get_bezier_point_at(&test_curve, current_offset); + offsets.push((char_bezier_pt.x, char_bezier_pt.y)); + + let x_advance_px = glyph.point.x * 2.0; + let x_advance_percent = if x_advance_px > 0.00001 { + x_advance_px / curve_length + } else { + 0.0 + }; + + current_offset = start_offset + x_advance_percent; + } + + offsets +} + fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 5e8f4a89c..ce5058ee9 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,7 +9,8 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, VertexBuffers, SvgVert, GlyphId, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, - scale_vertex_buffer, transform_vertex_buffer, + scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, + quick_circle, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index f640c5aec..5839271c6 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -475,7 +475,6 @@ const GL_RESTART_INDEX: u32 = ::std::u32::MAX; fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: Option) -> ((Vec, Vec), Option<(Vec, Vec)>) { - let mut last_index = 0; let mut vertex_buf = Vec::::new(); let mut index_buf = Vec::::new(); @@ -510,10 +509,25 @@ fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: } } +/// Quick helper function to generate the vertices for a black circle at runtime +pub fn quick_circle(x: f32, y: f32, radius: f32) -> SvgLayerResource { + let (fill, _) = tesselate_layer_data(&LayerType::from_single_layer(SvgLayerType::Circle(SvgCircle { + center_x: x, + center_y: y, + radius + })), 0.01, None); + let style = SvgStyle::filled(ColorU { r: 0, b: 0, g: 0, a: 255 }); + SvgLayerResource::Direct { + style: style, + fill: Some(VerticesIndicesBuffer { vertices: fill.0, indices: fill.1 }), + stroke: None, + } +} + /// Joins multiple SvgVert buffers to one and calculates the indices /// /// TODO: Wrap this in a nicer API -pub fn join_vertex_buffers(input: &[VertexBuffers]) -> (Vec, Vec) { +pub fn join_vertex_buffers(input: &[VertexBuffers]) -> VerticesIndicesBuffer { let mut last_index = 0; let mut vertex_buf = Vec::::new(); @@ -526,8 +540,8 @@ pub fn join_vertex_buffers(input: &[VertexBuffers]) -> (Vec, V index_buf.push(GL_RESTART_INDEX); last_index += vertices_len; } - - (vertex_buf, index_buf) + + VerticesIndicesBuffer { vertices: vertex_buf, indices: index_buf } } pub fn scale_vertex_buffer(input: &mut [SvgVert], scale: &FontSize) { @@ -546,6 +560,16 @@ pub fn transform_vertex_buffer(input: &mut [SvgVert], x: f32, y: f32) { } } +/// sin and cos are the sinus and cosinus of the rotation +pub fn rotate_vertex_buffer(input: &mut [SvgVert], sin: f32, cos: f32) { + for vert in input { + let (x, y) = vert.xy; + let new_x = (x * cos) - (y * sin); + let new_y = (x * sin) + (y * cos); + vert.xy = (new_x, new_y); + } +} + #[derive(Debug)] pub enum SvgParseError { /// Syntax error in the Svg @@ -870,8 +894,9 @@ impl VectorizedFont { let stroke_options = SvgStrokeOptions::default(); // TODO: In a regular font (4000 characters), this is pretty slow! - - for g in (0..font.glyph_count() as u32).filter_map(|i| { + // font.glyph_count() as u32 + // Pre-load the first 128 characters + for g in (0..128).filter_map(|i| { let g = font.glyph(GlyphId(i)); if g.id() == GlyphId(0) { None From 858bc0c81c58d64d5e4573c6956fb9434e43fb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 25 Jul 2018 10:54:34 +0200 Subject: [PATCH 160/868] Improved and stabilized SVG font loading --- Cargo.toml | 1 + examples/debug.rs | 19 +++++++------ src/widgets/mod.rs | 4 +-- src/widgets/svg.rs | 71 +++++++++++++++++++++++++++++++++++----------- 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f5a53e01..18b7a58bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ categories = ["gui"] license = "MIT" repository = "https://github.com/maps4print/azul" readme = "README.md" +exclude = ["assets", "doc", "examples"] [dependencies] cassowary = "0.3.0" diff --git a/examples/debug.rs b/examples/debug.rs index 675723891..4674addb5 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -68,7 +68,8 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo font_size: &FontSize, glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, - transform_func: fn(&VectorizedFont, &GlyphId) -> Option> + original_font: &Font, + transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { let character_rotations = vec![30.0_f32; glyph_ids.len()]; @@ -80,15 +81,15 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let fill_buf = glyph_ids.iter() .filter_map(|gid| { - // 1. Transform glyph to vertex buffer && filter out all glyphs + // 1. Transform glyph to vertex buffer && filter out all glyphs // that don't have a vertex buffer - transform_func(vectorized_font, &GlyphId(gid.index)) + transform_func(vectorized_font, original_font, &GlyphId(gid.index)) .and_then(|vertex_buf| Some((gid, vertex_buf))) }) .zip(character_rotations.into_iter()) .zip(char_offsets.into_iter()) .map(|(((gid, mut vertex_buf), char_rot), (char_offset_x, char_offset_y))| { - + // 2. Scale characters to the final size scale_vertex_buffer(&mut vertex_buf.vertices, font_size); @@ -98,10 +99,10 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, - (gid.point.x * 2.0) + char_offset_x, + transform_vertex_buffer(&mut vertex_buf.vertices, + (gid.point.x * 2.0) + char_offset_x, (gid.point.y * 2.0) + char_offset_y); - + vertex_buf }) .collect::>(); @@ -110,11 +111,11 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo } let fill_vertices = style.fill.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, get_fill_vertices)) + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, get_fill_vertices)) }); let stroke_vertices = style.stroke.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, get_stroke_vertices)) + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, get_stroke_vertices)) }); layers.push(SvgLayerResource::Direct { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index ce5058ee9..48f57bb8a 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -7,10 +7,10 @@ pub use self::svg::{ Svg, SvgLayerId, SvgLayer, LayerType, SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, - VertexBuffers, SvgVert, GlyphId, + SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, - quick_circle, + quick_circle, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 5839271c6..02674f8fc 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -510,13 +510,21 @@ fn tesselate_layer_data(layer_data: &LayerType, tolerance: f32, stroke_options: } /// Quick helper function to generate the vertices for a black circle at runtime -pub fn quick_circle(x: f32, y: f32, radius: f32) -> SvgLayerResource { - let (fill, _) = tesselate_layer_data(&LayerType::from_single_layer(SvgLayerType::Circle(SvgCircle { - center_x: x, - center_y: y, - radius - })), 0.01, None); - let style = SvgStyle::filled(ColorU { r: 0, b: 0, g: 0, a: 255 }); +pub fn quick_circle(circle: SvgCircle, fill_color: ColorU) -> SvgLayerResource { + let (fill, _) = tesselate_layer_data(&LayerType::from_single_layer(SvgLayerType::Circle(circle)), 0.01, None); + let style = SvgStyle::filled(fill_color); + SvgLayerResource::Direct { + style: style, + fill: Some(VerticesIndicesBuffer { vertices: fill.0, indices: fill.1 }), + stroke: None, + } +} + +/// Quick helper function to generate the layer for **multiple** circles (in one draw call) +pub fn quick_circles(circles: &[SvgCircle], fill_color: ColorU) -> SvgLayerResource { + let circles = circles.iter().map(|c| SvgLayerType::Circle(*c)).collect(); + let (fill, _) = tesselate_layer_data(&LayerType::from_polygons(circles), 0.01, None); + let style = SvgStyle::filled(fill_color); SvgLayerResource::Direct { style: style, fill: Some(VerticesIndicesBuffer { vertices: fill.0, indices: fill.1 }), @@ -540,7 +548,7 @@ pub fn join_vertex_buffers(input: &[VertexBuffers]) -> VerticesIndicesB index_buf.push(GL_RESTART_INDEX); last_index += vertices_len; } - + VerticesIndicesBuffer { vertices: vertex_buf, indices: index_buf } } @@ -876,13 +884,15 @@ implement_vertex!(SvgVert, xy, normal); #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; +use std::cell::RefCell; + /// A vectorized font holds the glyphs for a given font, but in a vector format #[derive(Debug, Clone)] pub struct VectorizedFont { /// Glyph -> Polygon map - glyph_polygon_map: FastHashMap>, + glyph_polygon_map: Rc>>>, /// Glyph -> Stroke map - glyph_stroke_map: FastHashMap>, + glyph_stroke_map: Rc>>>, } impl VectorizedFont { @@ -896,7 +906,7 @@ impl VectorizedFont { // TODO: In a regular font (4000 characters), this is pretty slow! // font.glyph_count() as u32 // Pre-load the first 128 characters - for g in (0..128).filter_map(|i| { + for g in (0..1).filter_map(|i| { let g = font.glyph(GlyphId(i)); if g.id() == GlyphId(0) { None @@ -924,20 +934,49 @@ impl VectorizedFont { glyph_stroke_map.insert(GlyphId(0), stroke_verts_zero.unwrap()); } - Self { glyph_polygon_map, glyph_stroke_map } + Self { + glyph_polygon_map: Rc::new(RefCell::new(glyph_polygon_map)), + glyph_stroke_map: Rc::new(RefCell::new(glyph_stroke_map)), + } } } +use std::collections::hash_map::Entry::*; -pub fn get_fill_vertices(vectorized_font: &VectorizedFont, id: &GlyphId) +pub fn get_fill_vertices(vectorized_font: &VectorizedFont, original_font: &Font, id: &GlyphId) -> Option> { - vectorized_font.glyph_polygon_map.get(id).cloned() + let svg_stroke_opts = Some(SvgStrokeOptions::default()); + + match vectorized_font.glyph_polygon_map.borrow_mut().entry(*id) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let g = original_font.glyph(*id); + let poly = glyph_to_svg_layer_type(g)?; + let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); + v.insert(polygon_verts.clone()); + vectorized_font.glyph_stroke_map.borrow_mut().insert(*id, stroke_verts.unwrap()); + Some(polygon_verts) + } + } } -pub fn get_stroke_vertices(vectorized_font: &VectorizedFont, id: &GlyphId) +pub fn get_stroke_vertices(vectorized_font: &VectorizedFont, original_font: &Font, id: &GlyphId) -> Option> { - vectorized_font.glyph_stroke_map.get(id).cloned() + let svg_stroke_opts = Some(SvgStrokeOptions::default()); + + match vectorized_font.glyph_stroke_map.borrow_mut().entry(*id) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let g = original_font.glyph(*id); + let poly = glyph_to_svg_layer_type(g)?; + let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); + let stroke_verts = stroke_verts.unwrap(); + v.insert(stroke_verts.clone()); + vectorized_font.glyph_polygon_map.borrow_mut().insert(*id, polygon_verts); + Some(stroke_verts) + } + } } /// Converts a glyph to a `SvgLayerType::Polygon` From eb15cb1189a57783c03218f68295648c6bf35e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 25 Jul 2018 11:38:26 +0200 Subject: [PATCH 161/868] Fixed SVG text offset calculation slightly Note: For some reason, the program now takes 100MB RAM (instead of previously just 40MB)??? Even without the SVG file loaded (only the webrender stuff). Probably a bug, although not sure where. --- Cargo.toml | 2 +- examples/debug.rs | 42 +++++++++++++++++++++--------------------- src/widgets/mod.rs | 3 ++- src/widgets/svg.rs | 8 +++----- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 18b7a58bf..b17bbf348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "2cb682553816200bb74ce75d3851753bc122f488" +rev = "cf9b780325f67c32637deac1256375492e81b4d2" [features] default = ["logging"] diff --git a/examples/debug.rs b/examples/debug.rs index 4674addb5..716ef78cc 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -69,26 +69,21 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, + char_offsets: Vec<(f32, f32)>, transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { let character_rotations = vec![30.0_f32; glyph_ids.len()]; - let char_offsets = test_bezier_points_offsets(glyph_ids, 0.0); - - println!("char offsets: {:?}", char_offsets); - - assert!(char_offsets.len() == glyph_ids.len()); let fill_buf = glyph_ids.iter() .filter_map(|gid| { // 1. Transform glyph to vertex buffer && filter out all glyphs // that don't have a vertex buffer transform_func(vectorized_font, original_font, &GlyphId(gid.index)) - .and_then(|vertex_buf| Some((gid, vertex_buf))) }) .zip(character_rotations.into_iter()) .zip(char_offsets.into_iter()) - .map(|(((gid, mut vertex_buf), char_rot), (char_offset_x, char_offset_y))| { + .map(|((mut vertex_buf, char_rot), (char_offset_x, char_offset_y))| { // 2. Scale characters to the final size scale_vertex_buffer(&mut vertex_buf.vertices, font_size); @@ -99,9 +94,7 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, - (gid.point.x * 2.0) + char_offset_x, - (gid.point.y * 2.0) + char_offset_y); + transform_vertex_buffer(&mut vertex_buf.vertices, char_offset_x, char_offset_y); vertex_buf }) @@ -110,12 +103,14 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo join_vertex_buffers(&fill_buf) } + let (circle_layer, char_offsets) = test_bezier_points_offsets(&layout.layouted_glyphs, 0.0); + let fill_vertices = style.fill.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, get_fill_vertices)) + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, char_offsets.clone(), get_fill_vertices)) }); let stroke_vertices = style.stroke.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, get_stroke_vertices)) + Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, char_offsets, get_stroke_vertices)) }); layers.push(SvgLayerResource::Direct { @@ -124,6 +119,8 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo stroke: stroke_vertices, }); + layers.push(circle_layer); + // layers.append(&mut test_bezier_points()); layers } @@ -142,19 +139,22 @@ impl BezierControlPoint { } /// Roughly estimate the length of a bezier curve arc using 10 samples -fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> f32 { +fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> (Vec, f32) { let mut origin = curve[0]; let mut total_distance = 0.0; + let mut circles = vec![curve[0]]; for i in 1..10 { let new_point = get_bezier_point_at(curve, i as f32 / 10.0); total_distance += origin.distance(&new_point); + circles.push(new_point); origin = new_point; } total_distance += origin.distance(&curve[3]); - total_distance + circles.push(curve[3]); + (circles, total_distance) } /// t is between 0.0 and 1.0 on the four points @@ -178,7 +178,7 @@ fn get_bezier_point_at(curve: &[BezierControlPoint;4], t: f32) -> BezierControlP BezierControlPoint { x, y } } -fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> Vec<(f32, f32)> { +fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) -> (SvgLayerResource, Vec<(f32, f32)>) { let test_curve = [ BezierControlPoint { x: 0.0, y: 0.0 }, BezierControlPoint { x: 40.0, y: 120.0 }, @@ -186,13 +186,13 @@ fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> Ve BezierControlPoint { x: 120.0, y: 0.0 }, ]; - let curve_length = estimate_arc_length(&test_curve); + let (circles, curve_length) = estimate_arc_length(&test_curve); - let mut current_offset = start_offset; let mut offsets = vec![]; for glyph in glyphs { - let char_bezier_pt = get_bezier_point_at(&test_curve, current_offset); + println!("start offset is: {:?}", start_offset); + let char_bezier_pt = get_bezier_point_at(&test_curve, start_offset); offsets.push((char_bezier_pt.x, char_bezier_pt.y)); let x_advance_px = glyph.point.x * 2.0; @@ -201,11 +201,11 @@ fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> Ve } else { 0.0 }; - - current_offset = start_offset + x_advance_percent; + start_offset += x_advance_percent; } - offsets + let circles = circles.into_iter().map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }).collect::>(); + (quick_circles(&circles, ColorU { r: 0, b: 0, g: 0, a: 255 }), offsets) } fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 48f57bb8a..c2a44ae1e 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -8,9 +8,10 @@ pub use self::svg::{ SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, + SvgCircle, SvgRect, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, - quick_circle, + quick_circle, quick_circles, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 02674f8fc..679802926 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -3,8 +3,9 @@ use std::{ rc::Rc, io::{Error as IoError, Read}, sync::{Mutex, atomic::{Ordering, AtomicUsize}}, - cell::UnsafeCell, + cell::{UnsafeCell, RefCell}, hash::{Hash, Hasher}, + collections::hash_map::Entry::*, }; use glium::{ backend::Facade, index::PrimitiveType, @@ -884,8 +885,6 @@ implement_vertex!(SvgVert, xy, normal); #[derive(Debug, Copy, Clone)] pub struct SvgWorldPixel; -use std::cell::RefCell; - /// A vectorized font holds the glyphs for a given font, but in a vector format #[derive(Debug, Clone)] pub struct VectorizedFont { @@ -906,7 +905,7 @@ impl VectorizedFont { // TODO: In a regular font (4000 characters), this is pretty slow! // font.glyph_count() as u32 // Pre-load the first 128 characters - for g in (0..1).filter_map(|i| { + for g in (0..128).filter_map(|i| { let g = font.glyph(GlyphId(i)); if g.id() == GlyphId(0) { None @@ -940,7 +939,6 @@ impl VectorizedFont { } } } -use std::collections::hash_map::Entry::*; pub fn get_fill_vertices(vectorized_font: &VectorizedFont, original_font: &Font, id: &GlyphId) -> Option> From c22da4ab290bfa4be64ad66aed0c14900e555dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 27 Jul 2018 13:43:58 +0200 Subject: [PATCH 162/868] Added quick_lines function to generate lines at runtime This is useful for debugging, as well as thing such as drawing dynamic overlays on top of an SVG, or graphs, etc. --- src/widgets/svg.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 679802926..81f4f03ad 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -533,6 +533,46 @@ pub fn quick_circles(circles: &[SvgCircle], fill_color: ColorU) -> SvgLayerResou } } +/// Helper function to easily draw some lines at runtime +/// +/// ## Inputs +/// +/// - `lines`: Each item in `lines` is a line (represented by a `Vec<(x, y)>`). +/// Lines that are shorter than 2 points are ignored / not rendered. +/// - `stroke_color`: The color of the line +/// - `stroke_options`: If the line should be round, square, etc. +pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_options: Option) +-> SvgLayerResource +{ + let stroke_options = stroke_options.unwrap_or_default(); + let style = SvgStyle::stroked(stroke_color, stroke_options); + + let polygons = lines.iter() + .filter(|line| line.len() < 2) + .map(|line| { + + let first_point = &line[0]; + let mut poly_events = vec![PathEvent::MoveTo(TypedPoint2D::new(first_point.0, first_point.1))]; + + for (x, y) in line.iter().skip(1) { + poly_events.push(PathEvent::LineTo(TypedPoint2D::new(*x, *y))); + } + + SvgLayerType::Polygon(poly_events) + }).collect(); + + let (_, stroke) = tesselate_layer_data(&LayerType::from_polygons(polygons), 0.01, Some(stroke_options)); + + // Safe unwrap, since we passed Some(stroke_options) into tesselate_layer_data + let stroke = stroke.unwrap(); + + SvgLayerResource::Direct { + style: style, + fill: None, + stroke: Some(VerticesIndicesBuffer { vertices: stroke.0, indices: stroke.1 }), + } +} + /// Joins multiple SvgVert buffers to one and calculates the indices /// /// TODO: Wrap this in a nicer API @@ -1328,7 +1368,7 @@ pub enum SvgLayerResource { }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct VerticesIndicesBuffer { pub vertices: Vec, pub indices: Vec, From d3bc1dece2409eb0e157c75aacfe774cb5a8eb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 27 Jul 2018 13:57:04 +0200 Subject: [PATCH 163/868] Moved BezierControlPoint into the svg.rs file --- examples/debug.rs | 51 +++++++--------------------------------------- src/widgets/mod.rs | 5 +++-- src/widgets/svg.rs | 45 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 716ef78cc..783b979c5 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -5,16 +5,17 @@ extern crate azul; use azul::prelude::*; use azul::widgets::*; use azul::dialogs::*; +use azul::text_layout::*; + use std::fs; +const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); + #[derive(Debug)] pub struct MyAppData { pub map: Option, } -// need: (Style, VertexBuffer, IndexBuffer) -// TODO: This will be slow at first if we don't cache this - #[derive(Debug)] pub struct Map { pub cache: SvgCache, @@ -44,10 +45,6 @@ impl Layout for MyAppData { } } -const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); - -use azul::text_layout::*; - fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFontCache, resources: &AppResources) -> Vec { @@ -125,19 +122,6 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo layers } -#[derive(Debug, Copy, Clone)] -struct BezierControlPoint { - x: f32, - y: f32, -} - -impl BezierControlPoint { - /// Distance of two points - pub fn distance(&self, other: &Self) -> f32 { - ((other.x - self.x).powi(2) + (other.y - self.y).powi(2)).sqrt() - } -} - /// Roughly estimate the length of a bezier curve arc using 10 samples fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> (Vec, f32) { @@ -146,7 +130,7 @@ fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> (Vec (Vec BezierControlPoint { - let one_minus = 1.0 - t; - let one_minus_square = one_minus.powi(2); - let one_minus_cubic = one_minus.powi(3); - - // Bezier curve formula for 4 control points - // (1 - t) - let x = one_minus_cubic * curve[0].x - + 3.0 * one_minus_square * t * curve[1].x - + 3.0 * one_minus * t.powi(2) * curve[2].x - + t.powi(3) * curve[3].x; - - let y = one_minus_cubic * curve[0].y - + 3.0 * one_minus_square * t * curve[1].y - + 3.0 * one_minus * t.powi(2) * curve[2].y - + t.powi(3) * curve[3].y; - - BezierControlPoint { x, y } -} - fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) -> (SvgLayerResource, Vec<(f32, f32)>) { let test_curve = [ BezierControlPoint { x: 0.0, y: 0.0 }, @@ -191,8 +154,7 @@ fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) - let mut offsets = vec![]; for glyph in glyphs { - println!("start offset is: {:?}", start_offset); - let char_bezier_pt = get_bezier_point_at(&test_curve, start_offset); + let char_bezier_pt = cubic_interpolate_bezier(&test_curve, start_offset); offsets.push((char_bezier_pt.x, char_bezier_pt.y)); let x_advance_px = glyph.point.x * 2.0; @@ -205,6 +167,7 @@ fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) - } let circles = circles.into_iter().map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }).collect::>(); + (quick_circles(&circles, ColorU { r: 0, b: 0, g: 0, a: 255 }), offsets) } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c2a44ae1e..099adc5c3 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -8,10 +8,11 @@ pub use self::svg::{ SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, - SvgCircle, SvgRect, + SvgCircle, SvgRect, BezierControlPoint, + join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, - quick_circle, quick_circles, + quick_circle, quick_circles, cubic_interpolate_bezier }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 81f4f03ad..2fd9665c7 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -538,7 +538,7 @@ pub fn quick_circles(circles: &[SvgCircle], fill_color: ColorU) -> SvgLayerResou /// ## Inputs /// /// - `lines`: Each item in `lines` is a line (represented by a `Vec<(x, y)>`). -/// Lines that are shorter than 2 points are ignored / not rendered. +/// Lines that are shorter than 2 points are ignored / not rendered. /// - `stroke_color`: The color of the line /// - `stroke_options`: If the line should be round, square, etc. pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_options: Option) @@ -1374,6 +1374,49 @@ pub struct VerticesIndicesBuffer { pub indices: Vec, } +#[derive(Debug, Copy, Clone)] +pub struct BezierControlPoint { + pub x: f32, + pub y: f32, +} + +impl BezierControlPoint { + /// Distance of two points + pub fn distance(&self, other: &Self) -> f32 { + ((other.x - self.x).powi(2) + (other.y - self.y).powi(2)).sqrt() + } +} + +/// Bezier formula for cubic curves (start, handle 1, handle 2, end). +/// +/// ## Inputs +/// +/// - `curve`: The 4 handles of the curve +/// - `t`: The interpolation amount - usually between 0.0 and 1.0 if the point +/// should be between the start and end +/// +/// ## Returns +/// +/// - `BezierControlPoint`: The calculated point which lies on the curve, +/// according the the bezier formula +pub fn cubic_interpolate_bezier(curve: &[BezierControlPoint;4], t: f32) -> BezierControlPoint { + let one_minus = 1.0 - t; + let one_minus_square = one_minus.powi(2); + let one_minus_cubic = one_minus.powi(3); + + let x = one_minus_cubic * curve[0].x + + 3.0 * one_minus_square * t * curve[1].x + + 3.0 * one_minus * t.powi(2) * curve[2].x + + t.powi(3) * curve[3].x; + + let y = one_minus_cubic * curve[0].y + + 3.0 * one_minus_square * t * curve[1].y + + 3.0 * one_minus * t.powi(2) * curve[2].y + + t.powi(3) * curve[3].y; + + BezierControlPoint { x, y } +} + impl Svg { #[inline] From e36f7fac880ee41c3b7982a0a6388221ca2de114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 30 Jul 2018 16:26:19 +0200 Subject: [PATCH 164/868] Implemented arc length parametrization for SVG text-on-curve TODO: Does not work yet - there seem to be some scaling bugs, otherwise the implementation is solid. This should later be moved into the svg.rs file. --- examples/debug.rs | 225 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 158 insertions(+), 67 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 783b979c5..8cd97c254 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -60,54 +60,14 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - // Calculates the layout for one word block - fn get_vertices( - font_size: &FontSize, - glyph_ids: &[GlyphInstance], - vectorized_font: &VectorizedFont, - original_font: &Font, - char_offsets: Vec<(f32, f32)>, - transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> - ) -> VerticesIndicesBuffer - { - let character_rotations = vec![30.0_f32; glyph_ids.len()]; - - let fill_buf = glyph_ids.iter() - .filter_map(|gid| { - // 1. Transform glyph to vertex buffer && filter out all glyphs - // that don't have a vertex buffer - transform_func(vectorized_font, original_font, &GlyphId(gid.index)) - }) - .zip(character_rotations.into_iter()) - .zip(char_offsets.into_iter()) - .map(|((mut vertex_buf, char_rot), (char_offset_x, char_offset_y))| { - - // 2. Scale characters to the final size - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - - // 3. Rotate individual characters inside of the word - let char_angle = char_rot.to_radians(); - let (char_sin, char_cos) = (char_angle.sin(), char_angle.cos()); - rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); - - // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, char_offset_x, char_offset_y); - - vertex_buf - }) - .collect::>(); - - join_vertex_buffers(&fill_buf) - } - let (circle_layer, char_offsets) = test_bezier_points_offsets(&layout.layouted_glyphs, 0.0); let fill_vertices = style.fill.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, char_offsets.clone(), get_fill_vertices)) + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, get_fill_vertices)) }); let stroke_vertices = style.stroke.and_then(|_| { - Some(get_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, char_offsets, get_stroke_vertices)) + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, get_stroke_vertices)) }); layers.push(SvgLayerResource::Direct { @@ -122,26 +82,158 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo layers } -/// Roughly estimate the length of a bezier curve arc using 10 samples -fn estimate_arc_length(curve: &[BezierControlPoint;4]) -> (Vec, f32) { +// Calculates the layout for one word block +fn vector_text_to_vertices( + font_size: &FontSize, + glyph_ids: &[GlyphInstance], + vectorized_font: &VectorizedFont, + original_font: &Font, + char_offsets: &[(f32, f32)], + transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> +) -> VerticesIndicesBuffer +{ + let character_rotations = vec![30.0_f32; glyph_ids.len()]; + + let fill_buf = glyph_ids.iter() + .filter_map(|gid| { + // 1. Transform glyph to vertex buffer && filter out all glyphs + // that don't have a vertex buffer + transform_func(vectorized_font, original_font, &GlyphId(gid.index)) + }) + .zip(character_rotations.into_iter()) + .zip(char_offsets.iter()) + .map(|((mut vertex_buf, char_rot), char_offset)| { + + let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue + + // 2. Scale characters to the final size + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + + // 3. Rotate individual characters inside of the word + let char_angle = char_rot.to_radians(); + let (char_sin, char_cos) = (char_angle.sin(), char_angle.cos()); + + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + + // 4. Transform characters to their respective positions + transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x, *char_offset_y); + + vertex_buf + }) + .collect::>(); + + join_vertex_buffers(&fill_buf) +} + +const BEZIER_SAMPLE_RATE: usize = 10; + +type ArcLength = f32; + +/// The sampled bezier curve stores information about 10 points that lie along the +/// bezier curve. +/// +/// For example: To place a text on a curve, we only have the layout +/// of the text in pixels. In order to calculate the position and rotation of +/// the individual characters (to place the text on the curve) we need to know +/// what the percentage offset (from 0.0 to 1.0) of the current character is +/// (which we can then give to the bezier formula, which will calculate the position +/// and rotation of the character) +/// +/// Calculating the position accurately is an unsolvable problem, but we can +/// "estimate" where the character would be, by solving 10 bezier points +/// for the offsets 0.0, 0.1, 0.2, and so on and storing the arc length from the +/// start for each position, ex. the position 0.1 is at 20 pixels, the position +/// 0.5 at 500 pixels, etc. Since a bezier curve is, well, curved, this offset is +/// not constantly increasing, it can vary from point to point. +/// +/// Lastly, to get the percentage of the string on the curve, we simply interpolate +/// linearly between the two nearest values. I.e. if we need to place a character +/// at 300 pixels from the start, we interpolate linearly between 0.1 +/// (which we know is at 20 pixels) and 0.5 (which we know is at 500 pixels). +/// +/// This process is called "arc length parametrization". More info: +#[derive(Debug, Copy, Clone)] +struct SampledBezierCurve { + /// Total length of the arc of the curve (from 0.0 to 1.0) + arc_length: f32, + /// Stores the x and y position of the sampled bezier points + sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE], + /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, + /// index 2 = 0.2 and so on. + /// + /// Stores the length of the BezierControlPoint at i from the + /// start of the curve + arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE], +} + +impl SampledBezierCurve { - let mut origin = curve[0]; - let mut total_distance = 0.0; - let mut circles = vec![curve[0]]; + /// Roughly estimate the length of a bezier curve arc using 10 samples + pub fn from_curve(curve: &[BezierControlPoint;4]) -> Self { - for i in 1..10 { - let new_point = cubic_interpolate_bezier(curve, i as f32 / 10.0); - total_distance += origin.distance(&new_point); - circles.push(new_point); - origin = new_point; + let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE]; + let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE]; + + for i in 1..BEZIER_SAMPLE_RATE { + sampled_bezier_points[i] = cubic_interpolate_bezier(curve, i as f32 / BEZIER_SAMPLE_RATE as f32); + } + + sampled_bezier_points[BEZIER_SAMPLE_RATE - 1] = curve[3]; + + // arc_length represents the sum of all sampled arcs up until the + // current sampled iteration point + let mut arc_length = 0.0; + + for (i, w) in sampled_bezier_points.windows(2).enumerate() { + let dist_current = w[0].distance(&w[1]); + arc_length_parametrization[i] = arc_length; + arc_length += dist_current; + } + + arc_length_parametrization[BEZIER_SAMPLE_RATE - 1] = arc_length; + + SampledBezierCurve { + arc_length, + sampled_bezier_points, + arc_length_parametrization, + } } - total_distance += origin.distance(&curve[3]); - circles.push(curve[3]); - (circles, total_distance) + /// Offset should be the point you seek from the start, i.e. 500 pixels for example. + /// + /// NOTE: Currently this function assumes a value that will be on the curve, + /// not past the 1.0 mark. + pub fn get_bezier_percentage_from_offset(&self, offset: f32) -> f32 { + + let mut lower_bound = 0; + let mut upper_bound = BEZIER_SAMPLE_RATE - 1; + + // If the offset is too high (past 1.0) we simply interpolate between the 0.9 + // and 1.0 point. Because of this we don't want to include the last point when iterating + for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE - 1).enumerate() { + if *param < offset { + lower_bound = i; + } else if *param > offset { + upper_bound = i; + break; + } + } + + // Now we know that the offset lies between the lower and upper bound, we need to + // find out how much we should (linearly) interpolate + let lower_bound_value = self.arc_length_parametrization[lower_bound]; + let upper_bound_value = self.arc_length_parametrization[upper_bound]; + let interpolate_percent = (offset - lower_bound_value) / (upper_bound_value - lower_bound_value); + + let lower_bound_percent = lower_bound as f32 / BEZIER_SAMPLE_RATE as f32; + let upper_bound_percent = upper_bound as f32 / BEZIER_SAMPLE_RATE as f32; + + lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) + } } -fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) -> (SvgLayerResource, Vec<(f32, f32)>) { +/// `start_offset` is in pixels - the offset of the text froma the start of the curve +fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> (SvgLayerResource, Vec<(f32, f32)>) { let test_curve = [ BezierControlPoint { x: 0.0, y: 0.0 }, BezierControlPoint { x: 40.0, y: 120.0 }, @@ -149,24 +241,23 @@ fn test_bezier_points_offsets(glyphs: &[GlyphInstance], mut start_offset: f32) - BezierControlPoint { x: 120.0, y: 0.0 }, ]; - let (circles, curve_length) = estimate_arc_length(&test_curve); + let sampled_bezier_curve = SampledBezierCurve::from_curve(&test_curve); let mut offsets = vec![]; + let mut current_offset = start_offset; for glyph in glyphs { - let char_bezier_pt = cubic_interpolate_bezier(&test_curve, start_offset); + let char_bezier_percentage = sampled_bezier_curve.get_bezier_percentage_from_offset(current_offset); + println!("current offset is: {}", current_offset); + let char_bezier_pt = cubic_interpolate_bezier(&test_curve, char_bezier_percentage); offsets.push((char_bezier_pt.x, char_bezier_pt.y)); - - let x_advance_px = glyph.point.x * 2.0; - let x_advance_percent = if x_advance_px > 0.00001 { - x_advance_px / curve_length - } else { - 0.0 - }; - start_offset += x_advance_percent; + current_offset += glyph.point.x * 2.0; } - let circles = circles.into_iter().map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }).collect::>(); + let circles = sampled_bezier_curve.sampled_bezier_points + .into_iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) + .collect::>(); (quick_circles(&circles, ColorU { r: 0, b: 0, g: 0, a: 255 }), offsets) } From 5fd584297601086916950c5e734a8d2d02be3bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 30 Jul 2018 16:26:44 +0200 Subject: [PATCH 165/868] Upgraded tested Rust version from 1.27.1 to 1.27.2 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 735883d6e..10c45b96c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ cache: cargo rust: - 1.26.0 - - 1.27.1 + - 1.27.2 os: - linux From 8eeb476423cacbb4a43ae2214fd7ccc8052d8f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 31 Jul 2018 00:55:20 +0200 Subject: [PATCH 166/868] Fixed quick_lines, fixed off-by-one error in SVG text position buffer --- Cargo.toml | 2 +- examples/debug.rs | 104 +++++++++++++++++++++++++++++---------------- src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 2 +- 4 files changed, 70 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b17bbf348..98d5ec95a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "cf9b780325f67c32637deac1256375492e81b4d2" +rev = "691063334823467a71af27d39cc3b6dcb68763fa" [features] default = ["logging"] diff --git a/examples/debug.rs b/examples/debug.rs index 8cd97c254..5c0211065 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -50,7 +50,8 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); - let cur_string = "Helloldakjfalfkjadlkfjdsalfkjdsalfkjdsf World"; + + let cur_string = "Hello World"; let font = resources.get_font(&FONT_ID).unwrap(); let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); @@ -60,14 +61,23 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - let (circle_layer, char_offsets) = test_bezier_points_offsets(&layout.layouted_glyphs, 0.0); + let test_curve = [ + BezierControlPoint { x: 0.0, y: 0.0 }, + BezierControlPoint { x: 40.0, y: 120.0 }, + BezierControlPoint { x: 80.0, y: 120.0 }, + BezierControlPoint { x: 120.0, y: 0.0 }, + ]; + + let interpolated_curve = SampledBezierCurve::from_curve(&test_curve); + let char_offsets = get_text_on_curve_offsets(&test_curve, &interpolated_curve, &layout.layouted_glyphs, 0.0); + let char_rotations = get_text_on_curve_rotations(&test_curve, &interpolated_curve, &layout.layouted_glyphs, 0.0); let fill_vertices = style.fill.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, get_fill_vertices)) + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, &char_rotations, get_fill_vertices)) }); let stroke_vertices = style.stroke.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, get_stroke_vertices)) + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, &char_rotations, get_stroke_vertices)) }); layers.push(SvgLayerResource::Direct { @@ -76,7 +86,8 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo stroke: stroke_vertices, }); - layers.push(circle_layer); + layers.push(interpolated_curve.draw_circles()); + layers.push(interpolated_curve.draw_lines()); // layers.append(&mut test_bezier_points()); layers @@ -89,18 +100,17 @@ fn vector_text_to_vertices( vectorized_font: &VectorizedFont, original_font: &Font, char_offsets: &[(f32, f32)], + char_rotations: &[f32], transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { - let character_rotations = vec![30.0_f32; glyph_ids.len()]; - let fill_buf = glyph_ids.iter() .filter_map(|gid| { // 1. Transform glyph to vertex buffer && filter out all glyphs // that don't have a vertex buffer transform_func(vectorized_font, original_font, &GlyphId(gid.index)) }) - .zip(character_rotations.into_iter()) + .zip(char_rotations.into_iter()) .zip(char_offsets.iter()) .map(|((mut vertex_buf, char_rot), char_offset)| { @@ -157,13 +167,13 @@ struct SampledBezierCurve { /// Total length of the arc of the curve (from 0.0 to 1.0) arc_length: f32, /// Stores the x and y position of the sampled bezier points - sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE], + sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, /// index 2 = 0.2 and so on. /// /// Stores the length of the BezierControlPoint at i from the /// start of the curve - arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE], + arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], } impl SampledBezierCurve { @@ -171,14 +181,14 @@ impl SampledBezierCurve { /// Roughly estimate the length of a bezier curve arc using 10 samples pub fn from_curve(curve: &[BezierControlPoint;4]) -> Self { - let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE]; - let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE]; + let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE + 1]; + let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE + 1]; - for i in 1..BEZIER_SAMPLE_RATE { + for i in 1..(BEZIER_SAMPLE_RATE + 1) { sampled_bezier_points[i] = cubic_interpolate_bezier(curve, i as f32 / BEZIER_SAMPLE_RATE as f32); } - sampled_bezier_points[BEZIER_SAMPLE_RATE - 1] = curve[3]; + sampled_bezier_points[BEZIER_SAMPLE_RATE] = curve[3]; // arc_length represents the sum of all sampled arcs up until the // current sampled iteration point @@ -190,7 +200,7 @@ impl SampledBezierCurve { arc_length += dist_current; } - arc_length_parametrization[BEZIER_SAMPLE_RATE - 1] = arc_length; + arc_length_parametrization[BEZIER_SAMPLE_RATE] = arc_length; SampledBezierCurve { arc_length, @@ -206,11 +216,11 @@ impl SampledBezierCurve { pub fn get_bezier_percentage_from_offset(&self, offset: f32) -> f32 { let mut lower_bound = 0; - let mut upper_bound = BEZIER_SAMPLE_RATE - 1; + let mut upper_bound = BEZIER_SAMPLE_RATE; // If the offset is too high (past 1.0) we simply interpolate between the 0.9 // and 1.0 point. Because of this we don't want to include the last point when iterating - for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE - 1).enumerate() { + for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE).enumerate() { if *param < offset { lower_bound = i; } else if *param > offset { @@ -230,36 +240,56 @@ impl SampledBezierCurve { lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) } -} -/// `start_offset` is in pixels - the offset of the text froma the start of the curve -fn test_bezier_points_offsets(glyphs: &[GlyphInstance], start_offset: f32) -> (SvgLayerResource, Vec<(f32, f32)>) { - let test_curve = [ - BezierControlPoint { x: 0.0, y: 0.0 }, - BezierControlPoint { x: 40.0, y: 120.0 }, - BezierControlPoint { x: 80.0, y: 120.0 }, - BezierControlPoint { x: 120.0, y: 0.0 }, - ]; + /// Returns the geometry necessary for drawing `self.sampled_bezier_points` + pub fn draw_circles(&self) -> SvgLayerResource { + quick_circles( + &self.sampled_bezier_points + .iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) + .collect::>(), + ColorU { r: 0, b: 0, g: 0, a: 255 }) + } - let sampled_bezier_curve = SampledBezierCurve::from_curve(&test_curve); + pub fn draw_lines(&self) -> SvgLayerResource { + let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; + quick_lines(&line, ColorU { r: 0, b: 0, g: 0, a: 255 }, None) + } +} +/// `start_offset` is in pixels - the offset of the text froma the start of the curve +/// +/// Returns the x and y offsets of the glyph characters +fn get_text_on_curve_offsets( + curve: &[BezierControlPoint;4], + sampled_bezier_curve: &SampledBezierCurve, + glyphs: &[GlyphInstance], + start_offset: f32) +-> Vec<(f32, f32)> +{ let mut offsets = vec![]; - let mut current_offset = start_offset; + let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); - for glyph in glyphs { + for glyph_idx in 0..glyphs.len() { let char_bezier_percentage = sampled_bezier_curve.get_bezier_percentage_from_offset(current_offset); - println!("current offset is: {}", current_offset); - let char_bezier_pt = cubic_interpolate_bezier(&test_curve, char_bezier_percentage); + let char_bezier_pt = cubic_interpolate_bezier(curve, char_bezier_percentage); offsets.push((char_bezier_pt.x, char_bezier_pt.y)); - current_offset += glyph.point.x * 2.0; + current_offset += glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x)).unwrap_or(0.0); } - let circles = sampled_bezier_curve.sampled_bezier_points - .into_iter() - .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) - .collect::>(); + offsets +} - (quick_circles(&circles, ColorU { r: 0, b: 0, g: 0, a: 255 }), offsets) +/// Calculate the rotations +fn get_text_on_curve_rotations( + curve: &[BezierControlPoint;4], + sampled_bezier_curve: &SampledBezierCurve, + glyphs: &[GlyphInstance], + start_offset: f32) +-> Vec +{ + // TODO !!! + vec![30.0; glyphs.len()] } fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 099adc5c3..4a092e856 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -12,7 +12,7 @@ pub use self::svg::{ join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, - quick_circle, quick_circles, cubic_interpolate_bezier + quick_circle, quick_circles, quick_lines, cubic_interpolate_bezier }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 2fd9665c7..6976e974c 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -548,7 +548,7 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio let style = SvgStyle::stroked(stroke_color, stroke_options); let polygons = lines.iter() - .filter(|line| line.len() < 2) + .filter(|line| line.len() > 2) .map(|line| { let first_point = &line[0]; From 7b6669b0b41490720689e8c95205dcaec8a68e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 31 Jul 2018 00:58:57 +0200 Subject: [PATCH 167/868] Moved SampledBezierCurve to svg.rs --- examples/debug.rs | 122 --------------------------------------------- src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 123 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 5c0211065..b9df2f835 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -135,128 +135,6 @@ fn vector_text_to_vertices( join_vertex_buffers(&fill_buf) } -const BEZIER_SAMPLE_RATE: usize = 10; - -type ArcLength = f32; - -/// The sampled bezier curve stores information about 10 points that lie along the -/// bezier curve. -/// -/// For example: To place a text on a curve, we only have the layout -/// of the text in pixels. In order to calculate the position and rotation of -/// the individual characters (to place the text on the curve) we need to know -/// what the percentage offset (from 0.0 to 1.0) of the current character is -/// (which we can then give to the bezier formula, which will calculate the position -/// and rotation of the character) -/// -/// Calculating the position accurately is an unsolvable problem, but we can -/// "estimate" where the character would be, by solving 10 bezier points -/// for the offsets 0.0, 0.1, 0.2, and so on and storing the arc length from the -/// start for each position, ex. the position 0.1 is at 20 pixels, the position -/// 0.5 at 500 pixels, etc. Since a bezier curve is, well, curved, this offset is -/// not constantly increasing, it can vary from point to point. -/// -/// Lastly, to get the percentage of the string on the curve, we simply interpolate -/// linearly between the two nearest values. I.e. if we need to place a character -/// at 300 pixels from the start, we interpolate linearly between 0.1 -/// (which we know is at 20 pixels) and 0.5 (which we know is at 500 pixels). -/// -/// This process is called "arc length parametrization". More info: -#[derive(Debug, Copy, Clone)] -struct SampledBezierCurve { - /// Total length of the arc of the curve (from 0.0 to 1.0) - arc_length: f32, - /// Stores the x and y position of the sampled bezier points - sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], - /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, - /// index 2 = 0.2 and so on. - /// - /// Stores the length of the BezierControlPoint at i from the - /// start of the curve - arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], -} - -impl SampledBezierCurve { - - /// Roughly estimate the length of a bezier curve arc using 10 samples - pub fn from_curve(curve: &[BezierControlPoint;4]) -> Self { - - let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE + 1]; - let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE + 1]; - - for i in 1..(BEZIER_SAMPLE_RATE + 1) { - sampled_bezier_points[i] = cubic_interpolate_bezier(curve, i as f32 / BEZIER_SAMPLE_RATE as f32); - } - - sampled_bezier_points[BEZIER_SAMPLE_RATE] = curve[3]; - - // arc_length represents the sum of all sampled arcs up until the - // current sampled iteration point - let mut arc_length = 0.0; - - for (i, w) in sampled_bezier_points.windows(2).enumerate() { - let dist_current = w[0].distance(&w[1]); - arc_length_parametrization[i] = arc_length; - arc_length += dist_current; - } - - arc_length_parametrization[BEZIER_SAMPLE_RATE] = arc_length; - - SampledBezierCurve { - arc_length, - sampled_bezier_points, - arc_length_parametrization, - } - } - - /// Offset should be the point you seek from the start, i.e. 500 pixels for example. - /// - /// NOTE: Currently this function assumes a value that will be on the curve, - /// not past the 1.0 mark. - pub fn get_bezier_percentage_from_offset(&self, offset: f32) -> f32 { - - let mut lower_bound = 0; - let mut upper_bound = BEZIER_SAMPLE_RATE; - - // If the offset is too high (past 1.0) we simply interpolate between the 0.9 - // and 1.0 point. Because of this we don't want to include the last point when iterating - for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE).enumerate() { - if *param < offset { - lower_bound = i; - } else if *param > offset { - upper_bound = i; - break; - } - } - - // Now we know that the offset lies between the lower and upper bound, we need to - // find out how much we should (linearly) interpolate - let lower_bound_value = self.arc_length_parametrization[lower_bound]; - let upper_bound_value = self.arc_length_parametrization[upper_bound]; - let interpolate_percent = (offset - lower_bound_value) / (upper_bound_value - lower_bound_value); - - let lower_bound_percent = lower_bound as f32 / BEZIER_SAMPLE_RATE as f32; - let upper_bound_percent = upper_bound as f32 / BEZIER_SAMPLE_RATE as f32; - - lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) - } - - /// Returns the geometry necessary for drawing `self.sampled_bezier_points` - pub fn draw_circles(&self) -> SvgLayerResource { - quick_circles( - &self.sampled_bezier_points - .iter() - .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) - .collect::>(), - ColorU { r: 0, b: 0, g: 0, a: 255 }) - } - - pub fn draw_lines(&self) -> SvgLayerResource { - let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; - quick_lines(&line, ColorU { r: 0, b: 0, g: 0, a: 255 }, None) - } -} - /// `start_offset` is in pixels - the offset of the text froma the start of the curve /// /// Returns the x and y offsets of the glyph characters diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4a092e856..c1cb94a53 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -8,7 +8,7 @@ pub use self::svg::{ SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, - SvgCircle, SvgRect, BezierControlPoint, + SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 6976e974c..7f4225d62 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -573,6 +573,128 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio } } +const BEZIER_SAMPLE_RATE: usize = 10; + +type ArcLength = f32; + +/// The sampled bezier curve stores information about 10 points that lie along the +/// bezier curve. +/// +/// For example: To place a text on a curve, we only have the layout +/// of the text in pixels. In order to calculate the position and rotation of +/// the individual characters (to place the text on the curve) we need to know +/// what the percentage offset (from 0.0 to 1.0) of the current character is +/// (which we can then give to the bezier formula, which will calculate the position +/// and rotation of the character) +/// +/// Calculating the position accurately is an unsolvable problem, but we can +/// "estimate" where the character would be, by solving 10 bezier points +/// for the offsets 0.0, 0.1, 0.2, and so on and storing the arc length from the +/// start for each position, ex. the position 0.1 is at 20 pixels, the position +/// 0.5 at 500 pixels, etc. Since a bezier curve is, well, curved, this offset is +/// not constantly increasing, it can vary from point to point. +/// +/// Lastly, to get the percentage of the string on the curve, we simply interpolate +/// linearly between the two nearest values. I.e. if we need to place a character +/// at 300 pixels from the start, we interpolate linearly between 0.1 +/// (which we know is at 20 pixels) and 0.5 (which we know is at 500 pixels). +/// +/// This process is called "arc length parametrization". More info: +#[derive(Debug, Copy, Clone)] +pub struct SampledBezierCurve { + /// Total length of the arc of the curve (from 0.0 to 1.0) + pub arc_length: f32, + /// Stores the x and y position of the sampled bezier points + pub sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], + /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, + /// index 2 = 0.2 and so on. + /// + /// Stores the length of the BezierControlPoint at i from the + /// start of the curve + pub arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], +} + +impl SampledBezierCurve { + + /// Roughly estimate the length of a bezier curve arc using 10 samples + pub fn from_curve(curve: &[BezierControlPoint;4]) -> Self { + + let mut sampled_bezier_points = [curve[0]; BEZIER_SAMPLE_RATE + 1]; + let mut arc_length_parametrization = [0.0; BEZIER_SAMPLE_RATE + 1]; + + for i in 1..(BEZIER_SAMPLE_RATE + 1) { + sampled_bezier_points[i] = cubic_interpolate_bezier(curve, i as f32 / BEZIER_SAMPLE_RATE as f32); + } + + sampled_bezier_points[BEZIER_SAMPLE_RATE] = curve[3]; + + // arc_length represents the sum of all sampled arcs up until the + // current sampled iteration point + let mut arc_length = 0.0; + + for (i, w) in sampled_bezier_points.windows(2).enumerate() { + let dist_current = w[0].distance(&w[1]); + arc_length_parametrization[i] = arc_length; + arc_length += dist_current; + } + + arc_length_parametrization[BEZIER_SAMPLE_RATE] = arc_length; + + SampledBezierCurve { + arc_length, + sampled_bezier_points, + arc_length_parametrization, + } + } + + /// Offset should be the point you seek from the start, i.e. 500 pixels for example. + /// + /// NOTE: Currently this function assumes a value that will be on the curve, + /// not past the 1.0 mark. + pub fn get_bezier_percentage_from_offset(&self, offset: f32) -> f32 { + + let mut lower_bound = 0; + let mut upper_bound = BEZIER_SAMPLE_RATE; + + // If the offset is too high (past 1.0) we simply interpolate between the 0.9 + // and 1.0 point. Because of this we don't want to include the last point when iterating + for (i, param) in self.arc_length_parametrization.iter().take(BEZIER_SAMPLE_RATE).enumerate() { + if *param < offset { + lower_bound = i; + } else if *param > offset { + upper_bound = i; + break; + } + } + + // Now we know that the offset lies between the lower and upper bound, we need to + // find out how much we should (linearly) interpolate + let lower_bound_value = self.arc_length_parametrization[lower_bound]; + let upper_bound_value = self.arc_length_parametrization[upper_bound]; + let interpolate_percent = (offset - lower_bound_value) / (upper_bound_value - lower_bound_value); + + let lower_bound_percent = lower_bound as f32 / BEZIER_SAMPLE_RATE as f32; + let upper_bound_percent = upper_bound as f32 / BEZIER_SAMPLE_RATE as f32; + + lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) + } + + /// Returns the geometry necessary for drawing `self.sampled_bezier_points` + pub fn draw_circles(&self) -> SvgLayerResource { + quick_circles( + &self.sampled_bezier_points + .iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) + .collect::>(), + ColorU { r: 0, b: 0, g: 0, a: 255 }) + } + + pub fn draw_lines(&self) -> SvgLayerResource { + let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; + quick_lines(&line, ColorU { r: 0, b: 0, g: 0, a: 255 }, None) + } +} + /// Joins multiple SvgVert buffers to one and calculates the indices /// /// TODO: Wrap this in a nicer API From 69e7ee7df095c01e50aad942d7b72854905a5380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 31 Jul 2018 01:35:15 +0200 Subject: [PATCH 168/868] Refactored text-on-curve layout into seperate function --- examples/debug.rs | 92 ++++++++++++++++------------------------------ src/widgets/svg.rs | 57 +++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index b9df2f835..6f545f295 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -50,47 +50,52 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); - - let cur_string = "Hello World"; + let text_style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); let font = resources.get_font(&FONT_ID).unwrap(); let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); - let font_size = FontSize::px(10.0); - let font_metrics = FontMetrics::new(&font.0, &font_size, None); - let layout = layout_text(&cur_string, &font.0, &font_metrics); - - let style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - - let test_curve = [ + let test_curve = SampledBezierCurve::from_curve(&[ BezierControlPoint { x: 0.0, y: 0.0 }, BezierControlPoint { x: 40.0, y: 120.0 }, BezierControlPoint { x: 80.0, y: 120.0 }, BezierControlPoint { x: 120.0, y: 0.0 }, - ]; + ]); + + layers.push(text_on_curve("Hello World", &test_curve, text_style, &font.0, vectorized_font, font_size)); + layers.push(test_curve.draw_circles()); + layers.push(test_curve.draw_lines()); + layers.push(test_curve.draw_control_handles()); + + layers +} + +fn text_on_curve( + text: &str, + curve: &SampledBezierCurve, + text_style: SvgStyle, + font: &Font, + vector_font: &VectorizedFont, + font_size: FontSize) +-> SvgLayerResource +{ + let font_metrics = FontMetrics::new(font, &font_size, None); + let layout = layout_text(text, font, &font_metrics); - let interpolated_curve = SampledBezierCurve::from_curve(&test_curve); - let char_offsets = get_text_on_curve_offsets(&test_curve, &interpolated_curve, &layout.layouted_glyphs, 0.0); - let char_rotations = get_text_on_curve_rotations(&test_curve, &interpolated_curve, &layout.layouted_glyphs, 0.0); + let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs, 0.0); - let fill_vertices = style.fill.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, &char_rotations, get_fill_vertices)) + let fill_vertices = text_style.fill.and_then(|_| { + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_fill_vertices)) }); - let stroke_vertices = style.stroke.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vectorized_font, &font.0, &char_offsets, &char_rotations, get_stroke_vertices)) + let stroke_vertices = text_style.stroke.and_then(|_| { + Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) }); - layers.push(SvgLayerResource::Direct { - style, + SvgLayerResource::Direct { + style: text_style, fill: fill_vertices, stroke: stroke_vertices, - }); - - layers.push(interpolated_curve.draw_circles()); - layers.push(interpolated_curve.draw_lines()); - - // layers.append(&mut test_bezier_points()); - layers + } } // Calculates the layout for one word block @@ -135,41 +140,6 @@ fn vector_text_to_vertices( join_vertex_buffers(&fill_buf) } -/// `start_offset` is in pixels - the offset of the text froma the start of the curve -/// -/// Returns the x and y offsets of the glyph characters -fn get_text_on_curve_offsets( - curve: &[BezierControlPoint;4], - sampled_bezier_curve: &SampledBezierCurve, - glyphs: &[GlyphInstance], - start_offset: f32) --> Vec<(f32, f32)> -{ - let mut offsets = vec![]; - let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); - - for glyph_idx in 0..glyphs.len() { - let char_bezier_percentage = sampled_bezier_curve.get_bezier_percentage_from_offset(current_offset); - let char_bezier_pt = cubic_interpolate_bezier(curve, char_bezier_percentage); - offsets.push((char_bezier_pt.x, char_bezier_pt.y)); - current_offset += glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x)).unwrap_or(0.0); - } - - offsets -} - -/// Calculate the rotations -fn get_text_on_curve_rotations( - curve: &[BezierControlPoint;4], - sampled_bezier_curve: &SampledBezierCurve, - glyphs: &[GlyphInstance], - start_offset: f32) --> Vec -{ - // TODO !!! - vec![30.0; glyphs.len()] -} - fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 7f4225d62..0e1b49ff4 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -28,7 +28,7 @@ use lyon::{ geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; -use webrender::api::{ColorU, ColorF, LayoutPixel}; +use webrender::api::{ColorU, ColorF, LayoutPixel, GlyphInstance}; use rusttype::{Font, Glyph}; use { FastHashMap, @@ -602,16 +602,18 @@ type ArcLength = f32; /// This process is called "arc length parametrization". More info: #[derive(Debug, Copy, Clone)] pub struct SampledBezierCurve { + /// Copy of the original curve which the SampledBezierCurve was created from + original_curve: [BezierControlPoint;4], /// Total length of the arc of the curve (from 0.0 to 1.0) - pub arc_length: f32, + arc_length: f32, /// Stores the x and y position of the sampled bezier points - pub sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], + sampled_bezier_points: [BezierControlPoint;BEZIER_SAMPLE_RATE + 1], /// Each index is the bezier value * 0.1, i.e. index 1 = 0.1, /// index 2 = 0.2 and so on. /// /// Stores the length of the BezierControlPoint at i from the /// start of the curve - pub arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], + arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], } impl SampledBezierCurve { @@ -641,6 +643,7 @@ impl SampledBezierCurve { arc_length_parametrization[BEZIER_SAMPLE_RATE] = arc_length; SampledBezierCurve { + original_curve: *curve, arc_length, sampled_bezier_points, arc_length_parametrization, @@ -679,7 +682,40 @@ impl SampledBezierCurve { lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) } - /// Returns the geometry necessary for drawing `self.sampled_bezier_points` + /// Place some glyphs on a curve and calculate the respective offsets and rotations + /// for the glyphs + /// + /// ## Inputs + /// + /// - `glyphs`: The glyph positions of the text you want to place on the curve + /// - `start_offset` The offset of the first character from the start of the curve: + /// **Note**: `start_offset` is measured in pixels, not percent! + /// + /// ## Returns + /// + /// - `Vec<(f32, f32)>`: the x and y offsets of the glyph characters + /// - `Vec`: The rotations in degrees of the glyph characters + pub fn get_text_offsets_and_rotations(&self, glyphs: &[GlyphInstance], start_offset: f32) + -> (Vec<(f32, f32)>, Vec) + { + let mut glyph_offsets = vec![]; + let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); + + for glyph_idx in 0..glyphs.len() { + let char_bezier_percentage = self.get_bezier_percentage_from_offset(current_offset); + let char_bezier_pt = cubic_interpolate_bezier(&self.original_curve, char_bezier_percentage); + glyph_offsets.push((char_bezier_pt.x, char_bezier_pt.y)); + current_offset += glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x)).unwrap_or(0.0); + } + + // TODO !!! + let glyph_rotations = vec![30.0; glyphs.len()]; + + (glyph_offsets, glyph_rotations) + } + + /// Returns the geometry necessary for drawing the points from `self.sampled_bezier_points`. + /// Usually only good for debugging pub fn draw_circles(&self) -> SvgLayerResource { quick_circles( &self.sampled_bezier_points @@ -689,6 +725,17 @@ impl SampledBezierCurve { ColorU { r: 0, b: 0, g: 0, a: 255 }) } + /// Returns the geometry necessary to draw the control handles of this curve + pub fn draw_control_handles(&self) -> SvgLayerResource { + quick_circles( + &self.original_curve + .iter() + .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 3.0 }) + .collect::>(), + ColorU { r: 255, b: 0, g: 0, a: 255 }) + } + + /// Returns the geometry necessary to draw the bezier curve (the actual line) pub fn draw_lines(&self) -> SvgLayerResource { let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; quick_lines(&line, ColorU { r: 0, b: 0, g: 0, a: 255 }, None) From 611a75b1e56486adf9ff6d60df2c598b63aac2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 1 Aug 2018 00:13:27 +0200 Subject: [PATCH 169/868] Fixed offset bug for curved text The offsets are laid out from the start of the curve, they are not "advances to the next character". Also fixed a mathematical mistake in the get_bezier_percentage_from_offset function. --- src/widgets/svg.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 0e1b49ff4..f4075de94 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -674,12 +674,14 @@ impl SampledBezierCurve { // find out how much we should (linearly) interpolate let lower_bound_value = self.arc_length_parametrization[lower_bound]; let upper_bound_value = self.arc_length_parametrization[upper_bound]; - let interpolate_percent = (offset - lower_bound_value) / (upper_bound_value - lower_bound_value); + let lower_upper_diff = upper_bound_value - lower_bound_value; + let interpolate_percent = (offset - lower_bound_value) / lower_upper_diff; let lower_bound_percent = lower_bound as f32 / BEZIER_SAMPLE_RATE as f32; let upper_bound_percent = upper_bound as f32 / BEZIER_SAMPLE_RATE as f32; - lower_bound_percent + ((upper_bound_percent - lower_bound_percent) * interpolate_percent) + let lower_upper_diff_percent = upper_bound_percent - lower_bound_percent; + lower_bound_percent + (lower_upper_diff_percent * interpolate_percent) } /// Place some glyphs on a curve and calculate the respective offsets and rotations @@ -699,13 +701,14 @@ impl SampledBezierCurve { -> (Vec<(f32, f32)>, Vec) { let mut glyph_offsets = vec![]; - let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); + // note: g.point.x is the offset from the start, not the advance! + let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); for glyph_idx in 0..glyphs.len() { let char_bezier_percentage = self.get_bezier_percentage_from_offset(current_offset); let char_bezier_pt = cubic_interpolate_bezier(&self.original_curve, char_bezier_percentage); glyph_offsets.push((char_bezier_pt.x, char_bezier_pt.y)); - current_offset += glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x)).unwrap_or(0.0); + current_offset = start_offset + glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); } // TODO !!! From 3e9bbaef0a9d481bb26ea5f7c1eccb547d99c797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 1 Aug 2018 20:36:56 +0200 Subject: [PATCH 170/868] Added rotation / bezier normal calculation for text rotations The formulas work, but the end result is rotated by 90 degrees for some reason. --- examples/debug.rs | 5 +-- src/widgets/mod.rs | 4 +- src/widgets/svg.rs | 107 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 6f545f295..da392a802 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -105,7 +105,7 @@ fn vector_text_to_vertices( vectorized_font: &VectorizedFont, original_font: &Font, char_offsets: &[(f32, f32)], - char_rotations: &[f32], + char_rotations: &[BezierCharacterRotation], transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> ) -> VerticesIndicesBuffer { @@ -125,8 +125,7 @@ fn vector_text_to_vertices( scale_vertex_buffer(&mut vertex_buf.vertices, font_size); // 3. Rotate individual characters inside of the word - let char_angle = char_rot.to_radians(); - let (char_sin, char_cos) = (char_angle.sin(), char_angle.cos()); + let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c1cb94a53..22d7fef18 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,10 +9,12 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, + BezierNormalVector, BezierCharacterRotation, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, - quick_circle, quick_circles, quick_lines, cubic_interpolate_bezier + quick_circle, quick_circles, quick_lines, cubic_interpolate_bezier, + quadratic_interpolate_bezier, }; pub use self::button::{Button, ButtonContent}; pub use self::label::Label; \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index f4075de94..ed805ad91 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -599,7 +599,8 @@ type ArcLength = f32; /// at 300 pixels from the start, we interpolate linearly between 0.1 /// (which we know is at 20 pixels) and 0.5 (which we know is at 500 pixels). /// -/// This process is called "arc length parametrization". More info: +/// This process is called "arc length parametrization". For more info + diagrams, see: +/// http://www.planetclegg.com/projects/WarpingTextToSplines.html #[derive(Debug, Copy, Clone)] pub struct SampledBezierCurve { /// Copy of the original curve which the SampledBezierCurve was created from @@ -616,6 +617,10 @@ pub struct SampledBezierCurve { arc_length_parametrization: [ArcLength; BEZIER_SAMPLE_RATE + 1], } +/// NOTE: The inner value is in **radians**, not degrees! +#[derive(Debug, Copy, Clone)] +pub struct BezierCharacterRotation(pub f32); + impl SampledBezierCurve { /// Roughly estimate the length of a bezier curve arc using 10 samples @@ -698,22 +703,23 @@ impl SampledBezierCurve { /// - `Vec<(f32, f32)>`: the x and y offsets of the glyph characters /// - `Vec`: The rotations in degrees of the glyph characters pub fn get_text_offsets_and_rotations(&self, glyphs: &[GlyphInstance], start_offset: f32) - -> (Vec<(f32, f32)>, Vec) + -> (Vec<(f32, f32)>, Vec) { let mut glyph_offsets = vec![]; - // note: g.point.x is the offset from the start, not the advance! + let mut glyph_rotations = vec![]; + + // NOTE: g.point.x is the offset from the start, not the advance! let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); for glyph_idx in 0..glyphs.len() { let char_bezier_percentage = self.get_bezier_percentage_from_offset(current_offset); let char_bezier_pt = cubic_interpolate_bezier(&self.original_curve, char_bezier_percentage); + let rotation = cubic_bezier_normal(&self.original_curve, char_bezier_percentage).to_rotation(); glyph_offsets.push((char_bezier_pt.x, char_bezier_pt.y)); + glyph_rotations.push(rotation); current_offset = start_offset + glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); } - // TODO !!! - let glyph_rotations = vec![30.0; glyphs.len()]; - (glyph_offsets, glyph_rotations) } @@ -1576,19 +1582,100 @@ pub fn cubic_interpolate_bezier(curve: &[BezierControlPoint;4], t: f32) -> Bezie let one_minus_square = one_minus.powi(2); let one_minus_cubic = one_minus.powi(3); + let t_pow2 = t.powi(2); + let t_pow3 = t.powi(3); + let x = one_minus_cubic * curve[0].x + 3.0 * one_minus_square * t * curve[1].x - + 3.0 * one_minus * t.powi(2) * curve[2].x - + t.powi(3) * curve[3].x; + + 3.0 * one_minus * t_pow2 * curve[2].x + + t_pow3 * curve[3].x; let y = one_minus_cubic * curve[0].y + 3.0 * one_minus_square * t * curve[1].y - + 3.0 * one_minus * t.powi(2) * curve[2].y - + t.powi(3) * curve[3].y; + + 3.0 * one_minus * t_pow2 * curve[2].y + + t_pow3 * curve[3].y; BezierControlPoint { x, y } } +pub fn quadratic_interpolate_bezier(curve: &[BezierControlPoint;3], t: f32) -> BezierControlPoint { + let one_minus = 1.0 - t; + let one_minus_square = one_minus.powi(2); + + let t_pow2 = t.powi(2); + + // TODO: Why 3.0 and not 2.0? + + let x = one_minus_square * curve[0].x + + 2.0 * one_minus * t * curve[1].x + + 3.0 * t_pow2 * curve[2].x; + + let y = one_minus_square * curve[0].y + + 2.0 * one_minus * t * curve[1].y + + 3.0 * t_pow2 * curve[2].y; + + BezierControlPoint { x, y } +} + +#[derive(Debug, Copy, Clone)] +pub struct BezierNormalVector { + pub x: f32, + pub y: f32, +} + +impl BezierNormalVector { + pub fn to_rotation(&self) -> BezierCharacterRotation { + BezierCharacterRotation((self.y / self.x).atan()) + } +} + +/// Calculates the normal vector at a certain point (perpendicular to the curve) +pub fn cubic_bezier_normal(curve: &[BezierControlPoint;4], t: f32) -> BezierNormalVector { + + // 1. Calculate the derivative of the bezier curve + // + // This means, we go from 4 control points to 3 control points and redistribute + // the weights of the control points according to the formula: + // + // w'0 = 3(w1-w0) + // w'1 = 3(w2-w1) + // w'2 = 3(w3-w2) + + let weight_1_x = 3.0 * (curve[1].x - curve[0].x); + let weight_1_y = 3.0 * (curve[1].y - curve[0].y); + + let weight_2_x = 3.0 * (curve[2].x - curve[1].x); + let weight_2_y = 3.0 * (curve[2].y - curve[1].y); + + let weight_3_x = 3.0 * (curve[3].x - curve[2].x); + let weight_3_y = 3.0 * (curve[3].y - curve[2].y); + + // The first derivative of a cubic bezier curve is a quadratic bezier curve + // Luckily, the first derivative is also the tangent vector. So all we need to do + // is to get the quadratic bezier + let mut tangent = quadratic_interpolate_bezier(&[ + BezierControlPoint { x: weight_1_x, y: weight_1_y }, + BezierControlPoint { x: weight_2_x, y: weight_2_y }, + BezierControlPoint { x: weight_3_x, y: weight_3_y }, + ], t); + + // We normalize the tangent to have a lenght of 1 + let tangent_length = (tangent.x.powi(2) + tangent.y.powi(2)).sqrt(); + tangent.x /= tangent_length; + tangent.y /= tangent_length; + + // The tangent is the vector that runs "along" the curve at a specific point. + // To get the normal (to calcuate the rotation of the characters), we need to + // rotate the tangent vector by 90 degrees. + // + // Rotating by 90 degrees is very simple, as we only need to flip the x and y axis + + BezierNormalVector { + x: -tangent.y, + y: tangent.x, + } +} + impl Svg { #[inline] From 3f4743adcda0bbf926ca2439d749c489d4392019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 1 Aug 2018 23:04:22 +0200 Subject: [PATCH 171/868] Fixed rotation for text-on-curve SVG text --- examples/debug.rs | 4 ++-- src/widgets/svg.rs | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index da392a802..8918340ac 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -39,7 +39,7 @@ impl Layout for MyAppData { } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file - Button::with_label("Open SVG file...").dom() + Button::with_label("Hello Worldaslfkdlfkasdjfldkjf").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } @@ -61,7 +61,7 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo BezierControlPoint { x: 120.0, y: 0.0 }, ]); - layers.push(text_on_curve("Hello World", &test_curve, text_style, &font.0, vectorized_font, font_size)); + layers.push(text_on_curve("Hello Worldaslfkdlfkasdjfldkjf", &test_curve, text_style, &font.0, vectorized_font, font_size)); layers.push(test_curve.draw_circles()); layers.push(test_curve.draw_lines()); layers.push(test_curve.draw_control_handles()); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index ed805ad91..d84227f66 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -573,7 +573,7 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio } } -const BEZIER_SAMPLE_RATE: usize = 10; +const BEZIER_SAMPLE_RATE: usize = 20; type ArcLength = f32; @@ -710,13 +710,18 @@ impl SampledBezierCurve { // NOTE: g.point.x is the offset from the start, not the advance! let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); + let mut last_offset = start_offset; for glyph_idx in 0..glyphs.len() { let char_bezier_percentage = self.get_bezier_percentage_from_offset(current_offset); let char_bezier_pt = cubic_interpolate_bezier(&self.original_curve, char_bezier_percentage); - let rotation = cubic_bezier_normal(&self.original_curve, char_bezier_percentage).to_rotation(); glyph_offsets.push((char_bezier_pt.x, char_bezier_pt.y)); + + let char_rotation_percentage = self.get_bezier_percentage_from_offset(last_offset); + let rotation = cubic_bezier_normal(&self.original_curve, char_rotation_percentage).to_rotation(); glyph_rotations.push(rotation); + + last_offset = current_offset; current_offset = start_offset + glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); } @@ -1625,7 +1630,7 @@ pub struct BezierNormalVector { impl BezierNormalVector { pub fn to_rotation(&self) -> BezierCharacterRotation { - BezierCharacterRotation((self.y / self.x).atan()) + BezierCharacterRotation((-self.x).atan2(self.y)) } } From 3bdc0290406965f40c6be3a5e963eda5c7f24829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 2 Aug 2018 00:09:56 +0200 Subject: [PATCH 172/868] Fixed text layout widths --- examples/debug.rs | 19 +++++++++++++------ src/styles/native_windows.css | 4 ++-- src/text_layout.rs | 17 ++++++++++++----- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 8918340ac..54c396d38 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -9,7 +9,8 @@ use azul::text_layout::*; use std::fs; -const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); +const FONT_ID: FontId = FontId::BuiltinFont("serif"); +const FONT_BYTES: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); #[derive(Debug)] pub struct MyAppData { @@ -39,7 +40,7 @@ impl Layout for MyAppData { } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file - Button::with_label("Hello Worldaslfkdlfkasdjfldkjf").dom() + Button::with_label("Azul App").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } @@ -50,9 +51,10 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); + let font_id = FontId::ExternalFont(String::from("Webly Sleeky UI")); let text_style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - let font = resources.get_font(&FONT_ID).unwrap(); - let vectorized_font = vector_font_cache.get_font(&FONT_ID).unwrap(); + let font = resources.get_font(&font_id).unwrap(); + let vectorized_font = vector_font_cache.get_font(&font_id).unwrap(); let font_size = FontSize::px(10.0); let test_curve = SampledBezierCurve::from_curve(&[ BezierControlPoint { x: 0.0, y: 0.0 }, @@ -171,10 +173,12 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let mut svg_cache = SvgCache::empty(); let svg_layers = svg_cache.add_svg(&contents).ok()?; + let font_id = FontId::ExternalFont(String::from("Webly Sleeky UI")); + // Pre-vectorize the glyphs of the font into vertex buffers - let (font, _) = app_state.get_font(&FONT_ID)?; + let (font, _) = app_state.get_font(&font_id)?; let mut vectorized_font_cache = VectorizedFontCache::new(); - vectorized_font_cache.insert_if_not_exist(FONT_ID, font); + vectorized_font_cache.insert_if_not_exist(font_id, font); app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, @@ -193,7 +197,10 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { + use std::io::Cursor; + let mut app = App::new(MyAppData { map: None }, AppConfig::default()); + app.add_font("Webly Sleeky UI", &mut Cursor::new(FONT_BYTES)).unwrap(); app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/styles/native_windows.css b/src/styles/native_windows.css index ab40fb82a..dbd8d7ceb 100644 --- a/src/styles/native_windows.css +++ b/src/styles/native_windows.css @@ -9,8 +9,8 @@ } * { - font-size: 12px; - font-family: sans-serif; + font-size: 14.66px; + font-family: serif; color: #000; background-color: #f0f0f0; /* Windows Background color, rgb(240, 240, 240) */ } \ No newline at end of file diff --git a/src/text_layout.rs b/src/text_layout.rs index ad00cbe19..7b5e88311 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -17,7 +17,7 @@ pub use webrender::api::GlyphInstance; /// Rusttype has a certain sizing hack, I have no idea where this number comes from /// Without this adjustment, we won't have the correct horizontal spacing -pub(crate) const RUSTTYPE_SIZE_HACK: f32 = 72.0 / 41.0; +pub(crate) const RUSTTYPE_SIZE_HACK: f32 = /* 72.0 / 41.0 */ 1.0; pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; @@ -387,6 +387,11 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: *cur_word_length = 0.0; } + let v_metrics_font = font.v_metrics_unscaled(); + // Warning: rusttype has a bit of a weird layout system - you have to + // subtract the descent from the ascent to get the proper vertical height + let v_metrics_height_unscaled = Scale::uniform(v_metrics_font.ascent - v_metrics_font.descent); + for cur_char in text.nfc() { match cur_char { '\t' => { @@ -428,14 +433,16 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: use rusttype::Point; let g = font.glyph(cur_char); + let id = g.id(); // calculate the real width let glyph_metrics = g.standalone().get_data().unwrap(); - let extents = glyph_metrics.extents.unwrap(); - let horz_rect_width = extents.max.x - extents.min.x; - let horiz_advance = horz_rect_width as f32 * glyph_metrics.scale_for_1_pixel * font_size.x; + let h_metrics = g.scaled(v_metrics_height_unscaled).h_metrics(); + let mut horiz_advance = h_metrics.advance_width + * glyph_metrics.scale_for_1_pixel + * (font_size.x * (96.0 / 72.0)); - let id = g.id(); + // horiz_advance *= 96.0 / 72.0; if let Some(last) = last_glyph { word_caret += font.pair_kerning(font_size, last, id); From 61cdea54c3f02bf54790fc2a7ebbea42dc400fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 5 Aug 2018 01:25:36 +0200 Subject: [PATCH 173/868] Non-working cleanup of OpenGL textures (crashes after first frame) Right now I'm not entirely sure on how to clean up OpenGL textures properly. This commit should also be used as a demo later on how to debug Rust code. --- src/app.rs | 33 +++++++++++++++++++++++++++------ src/compositor.rs | 33 +++++++++++++++++++++++++++------ src/display_list.rs | 13 ++++++++++--- src/styles/native_windows.css | 2 +- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/app.rs b/src/app.rs index cdb2c7e1d..1f719dd26 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,8 @@ use std::{ }; use glium::{SwapBuffersError, glutin::Event}; use glium::glutin::dpi::{LogicalPosition, LogicalSize}; -use webrender::api::{RenderApi, HitTestFlags, DevicePixel}; +use webrender::api::{RenderApi, HitTestFlags, DevicePixel, PipelineId, Epoch}; +use webrender::PipelineInfo; use image::ImageError; use euclid::{TypedScale, TypedSize2D}; #[cfg(feature = "logging")] @@ -234,10 +235,7 @@ impl<'a, T: Layout> App<'a, T> { if let Some(i) = force_redraw_cache.get_mut(idx) { if *i > 0 { *i -= 1 }; if *i == 0 { - use compositor::{TO_DELETE_TEXTURES, ACTIVE_GL_TEXTURES}; - let mut to_delete_lock = TO_DELETE_TEXTURES.lock().unwrap(); - let mut active_textures_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); - to_delete_lock.drain().for_each(|tex| { active_textures_lock.remove(&tex); }); + clean_up_unused_opengl_textures(window.renderer.as_mut().unwrap().flush_pipeline_info()); } } } @@ -647,10 +645,12 @@ fn render( use webrender::api::*; use display_list::DisplayList; use euclid::TypedSize2D; + use std::u32; let display_list = DisplayList::new_from_ui_description(ui_description); let builder = display_list.into_display_list_builder( window.internal.pipeline_id, + window.internal.epoch, &mut window.solver, &mut window.css, app_resources, @@ -678,15 +678,36 @@ fn render( true, ); + // We don't want the epoch to increase to u32::MAX, since u32::MAX represents + // an invalid epoch, which could confuse webrender + window.internal.epoch = Epoch(if window.internal.epoch.0 == (u32::MAX - 1) { + 0 + } else { + window.internal.epoch.0 + 1 + }); + txn.set_root_pipeline(window.internal.pipeline_id); txn.generate_frame(); window.internal.api.send_transaction(window.internal.document_id, txn); window.renderer.as_mut().unwrap().update(); - render_inner(window, framebuffer_size); } +fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { + + use compositor::ACTIVE_GL_TEXTURES; + use std::collections::HashSet; + + let currently_active_epochs: HashSet<&Epoch> = pipeline_info.epochs.values().collect(); + println!("currently active epochs: {:?}", currently_active_epochs); + + let mut active_textures_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); + + // Remove all unused OpenGL textures and keep only those currently in use + active_textures_lock.retain(|key, _| currently_active_epochs.get(key).is_some()); +} + // See: https://github.com/servo/webrender/pull/2880 // webrender doesn't reset the active shader back to what it was, but rather sets it // to zero, which glium doesn't know about, so on the next frame it tries to draw with shader 0 diff --git a/src/compositor.rs b/src/compositor.rs index fcd6d5802..b83510b83 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}; use webrender::{ ExternalImageHandler, ExternalImage, ExternalImageSource, - api::{ExternalImageId, TexelRect, DevicePixel}, + api::{ExternalImageId, TexelRect, DevicePixel, Epoch}, }; use glium::{ Program, VertexBuffer, Display, @@ -19,6 +19,12 @@ use { dom::Texture, }; +static LAST_OPENGL_ID: AtomicUsize = AtomicUsize::new(0); + +pub fn new_opengl_texture_id() -> usize { + LAST_OPENGL_ID.fetch_add(1, Ordering::SeqCst) +} + lazy_static! { /// Non-cleaned up textures. When a GlTexture is registered, it has to stay active as long /// as webrender needs it for drawing. To transparently do this, we store the epoch that the @@ -27,8 +33,13 @@ lazy_static! { /// /// Because the Texture2d is wrapped in an Rc, the destructor (which cleans up the OpenGL /// texture) does not run until we remove the textures - pub(crate) static ref ACTIVE_GL_TEXTURES: Mutex> = Mutex::new(FastHashMap::default()); - pub(crate) static ref TO_DELETE_TEXTURES: Mutex> = Mutex::new(FastHashSet::default()); + /// + /// Note: Because textures could be used after the current draw call (ex. for scrolling), + /// the ACTIVE_GL_TEXTURES are indexed by their epoch. Use `renderer.flush_pipeline_info()` + /// to see which textures are still active and which ones can be safely removed. + /// + /// See: https://github.com/servo/webrender/issues/2940 + pub(crate) static ref ACTIVE_GL_TEXTURES: Mutex>> = Mutex::new(FastHashMap::default()); } /// The Texture struct is public to the user @@ -59,7 +70,17 @@ impl ExternalImageHandler for Compositor { use glium::GlObject; let gl_tex_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); - let tex = &gl_tex_lock[&key]; + + println!("gl textures currently active: {:#?}", *gl_tex_lock); + println!("search for key: {:?}", key); + + // Search all epoch hash maps for the given key + // There does not seemt to be a way to get the epoch for the key, so we simply have to search all active epochs + let tex = gl_tex_lock + .values() + .filter_map(|epoch_map| epoch_map.get(&key)) + .next() + .expect("Missing OpenGL texture"); ExternalImage { uv: TexelRect { @@ -70,8 +91,8 @@ impl ExternalImageHandler for Compositor { } } - fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) { - TO_DELETE_TEXTURES.lock().unwrap().insert(key); + fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) { + // Since the renderer is currently single-threaded, there is nothing to do here } } diff --git a/src/display_list.rs b/src/display_list.rs index 76edfdf2c..c4111fd91 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -28,6 +28,7 @@ use { text_layout::{TextOverflowPass2, ScrollbarInfo}, images::ImageId, text_cache::TextId, + compositor::new_opengl_texture_id, }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); @@ -67,7 +68,7 @@ pub(crate) struct SolvedLayout { /// In the cached version, you can lookup the text as well as the dimensions of /// the words in the `AppResources`. For the `Uncached` version, you'll have to re- /// calculate it on every frame. -/// +/// /// TODO: It should be possible to switch this over to a `&'a str`, but currently /// this leads to unsolvable borrowing issues. pub(crate) enum TextInfo { @@ -250,6 +251,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { pub fn into_display_list_builder( &self, pipeline_id: PipelineId, + current_epoch: Epoch, ui_solver: &mut UiSolver, css: &mut Css, app_resources: &mut AppResources, @@ -323,6 +325,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // temporary: fill the whole window with each rectangle displaylist_handle_rect( &mut builder, + current_epoch, rect_idx, &self.rectangles, node_type, @@ -341,6 +344,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { fn displaylist_handle_rect<'a>( builder: &mut DisplayListBuilder, + current_epoch: Epoch, rect_idx: NodeId, arena: &Arena>, html_node: &NodeType, @@ -448,7 +452,8 @@ fn displaylist_handle_rect<'a>( let allow_mipmaps = true; let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); let key = render_api.generate_image_key(); - let external_image_id = ExternalImageId(texture.inner.get_id() as u64); + let external_image_id = ExternalImageId(new_opengl_texture_id() as u64); + println!("pusing external texture with id: {:?}", external_image_id); let data = ImageData::External(ExternalImageData { id: external_image_id, @@ -456,7 +461,9 @@ fn displaylist_handle_rect<'a>( image_type: ExternalImageType::TextureHandle(TextureTarget::Default), }); - ACTIVE_GL_TEXTURES.lock().unwrap().insert(external_image_id, ActiveTexture { texture: texture.clone() }); + ACTIVE_GL_TEXTURES.lock().unwrap() + .entry(current_epoch).or_insert_with(|| FastHashMap::default()) + .insert(external_image_id, ActiveTexture { texture: texture.clone() }); resource_updates.push(ResourceUpdate::AddImage( AddImage { key, descriptor, data, tiling: None } diff --git a/src/styles/native_windows.css b/src/styles/native_windows.css index dbd8d7ceb..b59b8f29f 100644 --- a/src/styles/native_windows.css +++ b/src/styles/native_windows.css @@ -10,7 +10,7 @@ * { font-size: 14.66px; - font-family: serif; + font-family: sans-serif; color: #000; background-color: #f0f0f0; /* Windows Background color, rgb(240, 240, 240) */ } \ No newline at end of file From 3e4d2670795d90daf6d86fd3976de252e9eddabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 5 Aug 2018 02:59:26 +0200 Subject: [PATCH 174/868] Fixed crashing with webrender epochs See https://github.com/servo/webrender/issues/2940 for more info --- src/app.rs | 25 +++++++++++++++++++++---- src/compositor.rs | 3 --- src/display_list.rs | 1 - 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1f719dd26..abdf17338 100644 --- a/src/app.rs +++ b/src/app.rs @@ -699,13 +699,30 @@ fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { use compositor::ACTIVE_GL_TEXTURES; use std::collections::HashSet; - let currently_active_epochs: HashSet<&Epoch> = pipeline_info.epochs.values().collect(); - println!("currently active epochs: {:?}", currently_active_epochs); + // TODO: currently active epochs can be empty, why? + // + // I mean, while the renderer is rendering, there can never be "no epochs" active, + // at least one epoch must always be active. + if pipeline_info.epochs.is_empty() { + return; + } + + // TODO: pipeline_info.epochs does not contain all active epochs, + // at best it contains the lowest in-use epoch. I.e. if `Epoch(43)` + // is listed, you can remove all textures from Epochs **lower than 43** + // BUT NOT EPOCHS HIGHER THAN 43. + // + // This means that "all active epochs" (in the documentation) is misleading + // since it doesn't actually list all active epochs, otherwise it'd list Epoch(43), + // Epoch(44), Epoch(45), which are currently active. + let oldest_to_remove_epoch = pipeline_info.epochs.values().min().unwrap(); let mut active_textures_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); - // Remove all unused OpenGL textures and keep only those currently in use - active_textures_lock.retain(|key, _| currently_active_epochs.get(key).is_some()); + // Retain all OpenGL textures from epochs higher than the lowest epoch + // + // TODO: Handle overflow of Epochs correctly (low priority) + active_textures_lock.retain(|key, _| key > oldest_to_remove_epoch); } // See: https://github.com/servo/webrender/pull/2880 diff --git a/src/compositor.rs b/src/compositor.rs index b83510b83..01a4bc201 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -71,9 +71,6 @@ impl ExternalImageHandler for Compositor { let gl_tex_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); - println!("gl textures currently active: {:#?}", *gl_tex_lock); - println!("search for key: {:?}", key); - // Search all epoch hash maps for the given key // There does not seemt to be a way to get the epoch for the key, so we simply have to search all active epochs let tex = gl_tex_lock diff --git a/src/display_list.rs b/src/display_list.rs index c4111fd91..b9b9526e1 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -453,7 +453,6 @@ fn displaylist_handle_rect<'a>( let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); let key = render_api.generate_image_key(); let external_image_id = ExternalImageId(new_opengl_texture_id() as u64); - println!("pusing external texture with id: {:?}", external_image_id); let data = ImageData::External(ExternalImageData { id: external_image_id, From b7f86752d17aa9905791e18b549c54ed2930312f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 5 Aug 2018 04:01:05 +0200 Subject: [PATCH 175/868] Updated webrender --- Cargo.toml | 6 +++--- src/app.rs | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 98d5ec95a..86b0849b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,10 @@ simplecss = "0.1.0" twox-hash = "1.1.0" glium = "0.22.0" gleam = "0.6" -euclid = "0.18" +euclid = "0.19" image = "0.19.0" rusttype = { git = "https://github.com/fschutt/rusttype" } -app_units = "0.6" +app_units = "0.7" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" lyon = { version = "0.10.0", features = ["extra"] } @@ -47,7 +47,7 @@ rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "691063334823467a71af27d39cc3b6dcb68763fa" +rev = "c939a61b83bcc9dc10742977704793e9a85b3858" [features] default = ["logging"] diff --git a/src/app.rs b/src/app.rs index abdf17338..31e1a067b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -697,7 +697,6 @@ fn render( fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { use compositor::ACTIVE_GL_TEXTURES; - use std::collections::HashSet; // TODO: currently active epochs can be empty, why? // From 7fa4c742db06d98fb37494a4fc73e6b767376352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 00:00:16 +0200 Subject: [PATCH 176/868] Removed NonZeroUsizeHack, since NonZeroUsize is now on stable - Bump required compiler to rustc 1.28 (because of NonZeroUsize) - Remove pointer-hacking code regading NonZeroUsize - Make NonZeroOptimizations private and disallow direct access to the NodeId.index field --- .travis.yml | 3 +- src/cache.rs | 6 +- src/id_tree.rs | 192 +++++++++++++++------------------------------ src/widgets/svg.rs | 5 +- 4 files changed, 69 insertions(+), 137 deletions(-) diff --git a/.travis.yml b/.travis.yml index 10c45b96c..e8247aa90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ language: rust cache: cargo rust: - - 1.26.0 - - 1.27.2 + - 1.28.0 os: - linux diff --git a/src/cache.rs b/src/cache.rs index e3a50acb3..10c2d5ef2 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -112,8 +112,6 @@ impl DomTreeCache { fn update_tree_inner_2(previous_arena: &Arena, next_arena: &Arena) -> DomChangeSet { - use id_tree::NonZeroUsizeHack; - let mut previous_iter = previous_arena.nodes.iter(); let mut next_iter = next_arena.nodes.iter().enumerate(); let mut changeset = DomChangeSet::empty(); @@ -121,11 +119,11 @@ impl DomTreeCache { while let Some((next_idx, next_hash)) = next_iter.next() { if let Some(old_hash) = previous_iter.next() { if old_hash.data != next_hash.data { - changeset.added_nodes.insert(NodeId { index: NonZeroUsizeHack::new(next_idx) }, next_hash.data); + changeset.added_nodes.insert(NodeId::new(next_idx), next_hash.data); } } else { // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - changeset.added_nodes.insert(NodeId { index: NonZeroUsizeHack::new(next_idx) }, next_hash.data); + changeset.added_nodes.insert(NodeId::new(next_idx), next_hash.data); } } /* diff --git a/src/id_tree.rs b/src/id_tree.rs index 2b8443471..d341b816f 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -9,136 +9,74 @@ use std::{ cmp::Ordering, }; -/// See: https://github.com/rust-lang/rust/issues/27730#issuecomment-311919692 -/// -/// This hack allows us to save some memory. Credit to @nox for inventing this. -/// Currently, rust optimizes an `Option<&T>` to be 8 bytes large instead of 16, -/// because Rust knows that pointers in Rust can never be 0 / NULL. -/// -/// The `NonZeroUsizeHack` adds 1 to a usize, then casts it to a pointer. -/// On retrieval, it casts it back to a usize and subtracts 1, to get the original value. -/// So in the end, `Option` is only 8 bytes large instead of 16, which gives -/// possibly better cache access and less memory usage at the cost of 1 or 2 extra -/// assembly instructions. -/// -/// Note that the Rust spec says that the pointer may never be null, even though it is -/// never dereferenced. -/// -/// NEVER MAKE THE INTERNAL FIELD PUBLIC, ALWAYS USE `::new()` and `.get()`! -#[derive(Copy, Clone)] -pub struct NonZeroUsizeHack(&'static ()); - -impl NonZeroUsizeHack { - /// **NOTE**: In debug mode, it panics on overflow, since having a - /// pointer that is zero is undefined behaviour (it would bascially be - /// casted to a `None`), which is incorrect, so we rather panic on overflow - /// to prevent that. - /// - /// To trigger an overflow however, you'd need more that 4 billion DOM nodes - - /// it is more likely that you run out of RAM before you do that. The only thing - /// that could lead to an overflow would be a bug. Therefore, overflow-checking is - /// disabled in release mode. - #[cfg_attr(not(debug_assertions), inline(always))] - pub fn new(value: usize) -> Self { - // Add 1 on insertion - #[cfg(debug_assertions)] { - let (new_value, has_overflown) = value.overflowing_add(1); - if has_overflown { - panic!("Overflow when creating DOM Node with ID {}", value); - } else { - unsafe { NonZeroUsizeHack(&*(new_value as *const ())) } +pub use self::node_id::NodeId; + +// Since private fields are module-based, this prevents any module from accessing +// `NodeId.index` directly. To get the correct node index is by using `NodeId::index()`, +// which subtracts 1 from the ID (because of Option optimizations) +mod node_id { + + use std::{ + fmt, + num::NonZeroUsize, + ops::{Add, AddAssign}, + }; + + /// A node identifier within a particular `Arena`. + #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] + pub struct NodeId { + index: NonZeroUsize, + } + + impl NodeId { + /// **NOTE**: In debug mode, it panics on overflow, since having a + /// pointer that is zero is undefined behaviour (it would bascially be + /// casted to a `None`), which is incorrect, so we rather panic on overflow + /// to prevent that. + /// + /// To trigger an overflow however, you'd need more that 4 billion DOM nodes - + /// it is more likely that you run out of RAM before you do that. The only thing + /// that could lead to an overflow would be a bug. Therefore, overflow-checking is + /// disabled in release mode. + #[cfg_attr(not(debug_assertions), inline(always))] + pub(crate) fn new(value: usize) -> Self { + + #[cfg(debug_assertions)] { + let (new_value, has_overflown) = value.overflowing_add(1); + if has_overflown { + panic!("Overflow when creating DOM Node with ID {}", value); + } else { + NodeId { index: NonZeroUsize::new(new_value).unwrap() } + } } - } - #[cfg(not(debug_assertions))] { - unsafe { NonZeroUsizeHack(&*((value + 1) as *const ())) } + #[cfg(not(debug_assertions))] { + unsafe { NonZeroUsizeHack(NonZeroUsize::new_unchecked(value + 1)) } + } } - } - - #[inline(always)] - pub fn get(self) -> usize { - // Remove 1 on retrieval - let value = self.0 as *const () as usize; - value - 1 - } -} - -impl Add for NonZeroUsizeHack { - type Output = NonZeroUsizeHack; - fn add(self, other: usize) -> NonZeroUsizeHack { - NonZeroUsizeHack::new(self.get() + other) - } -} - -impl PartialOrd for NonZeroUsizeHack { - fn partial_cmp(&self, other: &NonZeroUsizeHack) -> Option { - Some(self.get().cmp(&other.get())) - } -} - -impl Ord for NonZeroUsizeHack { - fn cmp(&self, other: &NonZeroUsizeHack) -> Ordering { - self.get().cmp(&other.get()) - } -} - -impl PartialEq for NonZeroUsizeHack { - fn eq(&self, other: &NonZeroUsizeHack) -> bool { - self.get() == other.get() - } -} - -impl Eq for NonZeroUsizeHack { } -impl fmt::Debug for NonZeroUsizeHack { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.get()) - } -} - -impl Hash for NonZeroUsizeHack { - fn hash(&self, state: &mut H) { - self.get().hash(state); - } -} - -/// A node identifier within a particular `Arena`. -#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct NodeId { - // FIXME: Change this to NonZero once NonZero is stabilized - pub(crate) index: NonZeroUsizeHack, -} - -impl NodeId { - pub(crate) fn new(value: usize) -> Self { - Self { - index: NonZeroUsizeHack::new(value), + pub fn index(&self) -> usize { + self.index.get() - 1 } } -} -impl AddAssign for NodeId { - fn add_assign(&mut self, other: usize) { - *self = NodeId { - index: self.index + other - }; + impl Add for NodeId { + type Output = NodeId; + fn add(self, other: usize) -> NodeId { + NodeId::new(self.index() + other) + } } -} - -impl Add for NodeId { - type Output = NodeId; - - fn add(self, other: usize) -> NodeId { - NodeId { - index: self.index + other + impl AddAssign for NodeId { + fn add_assign(&mut self, other: usize) { + *self = *self + other; } } -} -impl fmt::Display for NodeId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.index.get()) + impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.index()) + } } } @@ -245,9 +183,7 @@ impl Arena { next_sibling: None, data: data, }); - NodeId { - index: NonZeroUsizeHack::new(next_index), - } + NodeId::new(next_index) } // Returns how many nodes there are in the arena @@ -271,7 +207,7 @@ impl Arena { pub fn get_all_node_ids(&self) -> BTreeMap { use std::iter::FromIterator; BTreeMap::from_iter(self.nodes.iter().enumerate().map(|(i, node)| - (NodeId { index: NonZeroUsizeHack::new(i) }, node.data) + (NodeId::new(i), node.data) )) } } @@ -303,13 +239,13 @@ impl Index for Arena { type Output = Node; fn index(&self, node: NodeId) -> &Node { - &self.nodes[node.index.get()] + &self.nodes[node.index()] } } impl IndexMut for Arena { fn index_mut(&mut self, node: NodeId) -> &mut Node { - &mut self.nodes[node.index.get()] + &mut self.nodes[node.index()] } } @@ -451,7 +387,7 @@ impl NodeId { let last_child_opt; { let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index.get(), new_child.index.get(), "Can not append a node to itself"); + self.index(), new_child.index(), "Can not append a node to itself"); new_child_borrow.parent = Some(self); last_child_opt = mem::replace(&mut self_borrow.last_child, Some(new_child)); if let Some(last_child) = last_child_opt { @@ -473,7 +409,7 @@ impl NodeId { let first_child_opt; { let (self_borrow, new_child_borrow) = arena.nodes.get_pair_mut( - self.index.get(), new_child.index.get(), "Can not prepend a node to itself"); + self.index(), new_child.index(), "Can not prepend a node to itself"); new_child_borrow.parent = Some(self); first_child_opt = mem::replace(&mut self_borrow.first_child, Some(new_child)); if let Some(first_child) = first_child_opt { @@ -496,7 +432,7 @@ impl NodeId { let parent_opt; { let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index.get(), new_sibling.index.get(), "Can not insert a node after itself"); + self.index(), new_sibling.index(), "Can not insert a node after itself"); parent_opt = self_borrow.parent; new_sibling_borrow.parent = parent_opt; new_sibling_borrow.previous_sibling = Some(self); @@ -521,7 +457,7 @@ impl NodeId { let parent_opt; { let (self_borrow, new_sibling_borrow) = arena.nodes.get_pair_mut( - self.index.get(), new_sibling.index.get(), "Can not insert a node before itself"); + self.index(), new_sibling.index(), "Can not insert a node before itself"); parent_opt = self_borrow.parent; new_sibling_borrow.parent = parent_opt; new_sibling_borrow.next_sibling = Some(self); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index d84227f66..ee8d343cb 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -34,7 +34,6 @@ use { FastHashMap, dom::{Dom, NodeType, Callback}, traits::Layout, - id_tree::NonZeroUsizeHack, window::ReadOnlyWindow, css_parser::{FontId, FontSize}, }; @@ -47,10 +46,10 @@ static SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); static SVG_VIEW_BOX_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct SvgTransformId(NonZeroUsizeHack); +pub struct SvgTransformId(usize); pub fn new_svg_transform_id() -> SvgTransformId { - SvgTransformId(NonZeroUsizeHack::new(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst))) + SvgTransformId(SVG_TRANSFORM_ID.fetch_add(1, Ordering::SeqCst)) } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] From e0c3c1a9417be112c6e728a27fb1edb77714ea84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 00:25:17 +0200 Subject: [PATCH 177/868] Replace UnsafeCell with RefCell and Rc While not necessary in theory (the UnsafeCell was perfectly safe), the performance cost for this is negligible and it's better to keep the amount of unsafe code to a minimum. --- src/widgets/svg.rs | 77 ++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index ee8d343cb..9d5cf6f54 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -3,7 +3,7 @@ use std::{ rc::Rc, io::{Error as IoError, Read}, sync::{Mutex, atomic::{Ordering, AtomicUsize}}, - cell::{UnsafeCell, RefCell}, + cell::{UnsafeCell, RefCell, RefMut}, hash::{Hash, Hasher}, collections::hash_map::Entry::*, }; @@ -290,8 +290,8 @@ pub struct SvgCache { // Stores the vertices and indices necessary for drawing. Must be synchronized with the `layers` gpu_ready_to_upload_cache: FastHashMap, Vec)>, stroke_gpu_ready_to_upload_cache: FastHashMap, Vec)>, - vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, - stroke_vertex_index_buffer_cache: UnsafeCell, IndexBuffer)>>, + vertex_index_buffer_cache: RefCell, IndexBuffer)>>>, + stroke_vertex_index_buffer_cache: RefCell, IndexBuffer)>>>, shader: Mutex>, // Stores the 2D transforms of the shapes on the screen. The vertices are // offset by the X, Y value in the transforms struct. This should be expanded @@ -307,8 +307,8 @@ impl Default for SvgCache { layers: FastHashMap::default(), gpu_ready_to_upload_cache: FastHashMap::default(), stroke_gpu_ready_to_upload_cache: FastHashMap::default(), - vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), - stroke_vertex_index_buffer_cache: UnsafeCell::new(FastHashMap::default()), + vertex_index_buffer_cache: RefCell::new(FastHashMap::default()), + stroke_vertex_index_buffer_cache: RefCell::new(FastHashMap::default()), shader: Mutex::new(None), transforms: FastHashMap::default(), view_boxes: FastHashMap::default(), @@ -318,10 +318,9 @@ impl Default for SvgCache { fn fill_vertex_buffer_cache<'a, F: Facade>( id: &SvgLayerId, - rmut: &'a mut FastHashMap, IndexBuffer)>, + mut rmut: RefMut<'a, FastHashMap, IndexBuffer)>>>, rnotmut: &FastHashMap, Vec)>, window: &F) - -> Option<&'a (VertexBuffer, IndexBuffer)> { use std::collections::hash_map::Entry::*; @@ -330,16 +329,13 @@ fn fill_vertex_buffer_cache<'a, F: Facade>( Vacant(v) => { let (vbuf, ibuf) = match rnotmut.get(id).as_ref() { Some(s) => s, - None => return None, + None => return, }; let vertex_buffer = VertexBuffer::new(window, vbuf).unwrap(); let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, ibuf).unwrap(); - ; - v.insert((vertex_buffer, index_buffer)); + v.insert(Rc::new((vertex_buffer, index_buffer))); } } - - rmut.get(id) } impl SvgCache { @@ -359,15 +355,18 @@ impl SvgCache { } fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) - -> Option<&'a (VertexBuffer, IndexBuffer)> + -> Option, IndexBuffer)>> { use std::collections::hash_map::Entry::*; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; - let rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; - let rnotmut = &self.stroke_gpu_ready_to_upload_cache; + { + let rmut = self.stroke_vertex_index_buffer_cache.borrow_mut(); + let rnotmut = &self.stroke_gpu_ready_to_upload_cache; + fill_vertex_buffer_cache(id, rmut, rnotmut, window); + } - Some(fill_vertex_buffer_cache(id, rmut, rnotmut, window)?) + self.stroke_vertex_index_buffer_cache.borrow().get(id).and_then(|x| Some(x.clone())) } /// Note: panics if the ID isn't found. @@ -375,25 +374,21 @@ impl SvgCache { /// Since we are required to keep the `self.layers` and the `self.gpu_buffer_cache` /// in sync, a panic should never happen fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) - -> Option<&'a (VertexBuffer, IndexBuffer)> + -> Option, IndexBuffer)>> { use std::collections::hash_map::Entry::*; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; - // First, we need the SvgCache to call this function immutably, otherwise we can't + // We need the SvgCache to call this function immutably, otherwise we can't // use it from the Layout::layout() function - // - // Rust does also not "understand" that we want to return a reference into - // self.vertex_index_buffer_cache, so the reference that we are returning lives as - // long as the self.gpu_ready_to_upload_cache (at least until it's removed) - - // We need to use UnsafeCell here - when using a regular RefCell, Rust thinks we - // are destroying the reference after the borrow, but that isn't true. + { + let rmut = self.vertex_index_buffer_cache.borrow_mut(); + let rnotmut = &self.gpu_ready_to_upload_cache; - let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; - let rnotmut = &self.gpu_ready_to_upload_cache; + fill_vertex_buffer_cache(id, rmut, rnotmut, window); + } - Some(fill_vertex_buffer_cache(id, rmut, rnotmut, window)?) + self.vertex_index_buffer_cache.borrow().get(id).and_then(|x| Some(x.clone())) } fn get_style(&self, id: &SvgLayerId) @@ -424,8 +419,8 @@ impl SvgCache { self.layers.remove(&svg_id); self.gpu_ready_to_upload_cache.remove(&svg_id); self.stroke_gpu_ready_to_upload_cache.remove(&svg_id); - let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; - let stroke_rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; + let rmut = self.vertex_index_buffer_cache.get_mut(); + let stroke_rmut = self.stroke_vertex_index_buffer_cache.get_mut(); rmut.remove(&svg_id); stroke_rmut.remove(&svg_id); } @@ -436,10 +431,10 @@ impl SvgCache { self.gpu_ready_to_upload_cache.clear(); self.stroke_gpu_ready_to_upload_cache.clear(); - let rmut = unsafe { &mut *self.vertex_index_buffer_cache.get() }; + let rmut = self.vertex_index_buffer_cache.get_mut(); rmut.clear(); - let stroke_rmut = unsafe { &mut *self.stroke_vertex_index_buffer_cache.get() }; + let stroke_rmut = self.stroke_vertex_index_buffer_cache.get_mut(); stroke_rmut.clear(); } @@ -1746,15 +1741,14 @@ impl Svg { }; if let Some(color) = style.fill { - let mut direct_fill = None; - if let Some((fill_vertices, fill_indices)) = match &layer { + if let Some(fill_vi) = match &layer { SvgLayerResource::Reference(layer_id) => svg_cache.get_vertices_and_indices(window, layer_id), SvgLayerResource::Direct { fill, .. } => fill.as_ref().and_then(|f| { let vertex_buffer = VertexBuffer::new(window, &f.vertices).unwrap(); let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, &f.indices).unwrap(); - direct_fill = Some((vertex_buffer, index_buffer)); - Some(direct_fill.as_ref().unwrap()) + Some(Rc::new((vertex_buffer, index_buffer))) })} { + let (ref fill_vertices, ref fill_indices) = *fill_vi; draw_vertex_buffer_to_surface( &mut surface, &shader.program, @@ -1770,17 +1764,14 @@ impl Svg { } if let Some((stroke_color, _)) = style.stroke { - - let mut direct_stroke = None; - if let Some((stroke_vertices, stroke_indices)) = match &layer { + if let Some(stroke_vi) = match &layer { SvgLayerResource::Reference(layer_id) => svg_cache.get_stroke_vertices_and_indices(window, layer_id), SvgLayerResource::Direct { stroke, .. } => stroke.as_ref().and_then(|f| { let vertex_buffer = VertexBuffer::new(window, &f.vertices).unwrap(); let index_buffer = IndexBuffer::new(window, PrimitiveType::TrianglesList, &f.indices).unwrap(); - direct_stroke = Some((vertex_buffer, index_buffer)); - Some(direct_stroke.as_ref().unwrap()) - })} - { + Some(Rc::new((vertex_buffer, index_buffer))) + })} { + let (ref stroke_vertices, ref stroke_indices) = *stroke_vi; draw_vertex_buffer_to_surface( &mut surface, &shader.program, From 782f316188d97f8f8883779386624a8c063f4ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 00:44:07 +0200 Subject: [PATCH 178/868] Removed unused imports, removed commented-out code Also updated README to show the change to rustc 1.28 --- README.md | 8 ++++---- src/app.rs | 12 +++++------- src/app_state.rs | 1 - src/cache.rs | 31 +------------------------------ src/compositor.rs | 10 ++-------- src/css.rs | 3 +-- src/display_list.rs | 19 ++----------------- src/dom.rs | 3 --- src/id_tree.rs | 3 +-- src/lib.rs | 4 ++-- src/logging.rs | 1 - src/resources.rs | 35 ++++++++++++----------------------- src/task.rs | 1 - src/text_cache.rs | 5 +---- src/text_layout.rs | 17 +++++------------ src/traits.rs | 14 +++++++------- src/widgets/svg.rs | 23 ++++++++--------------- src/window.rs | 10 +--------- src/window_state.rs | 13 +++++++------ 19 files changed, 59 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 96c4a5e25..e7ce1f58b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) [![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) -[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.26%20stable-blue.svg)]() +[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.28%20stable-blue.svg)]() azul is a cross-platform, stylable GUI framework using Mozillas `webrender` engine for rendering and a CSS / DOM model for layout and rendering @@ -22,10 +22,10 @@ On Linux, you currently need to install `cmake` before you can use azul. CMake is used during the build process to compile servo-freetype. For interfacing with the system clipboard, you also need `libxcb-xkb-dev`. -Since azul uses the system-native fonts by default, you'll also need -`libfontconfig1-dev` (which includes expat and freetype2). +Since azul uses the system-native fonts by default, you'll also need +`libfontconfig1-dev` (which includes expat and freetype2). -Your users will need to install `libfontconfig` and +Your users will need to install `libfontconfig` and `libxcb-xkb1` installed (remember this for packaging rpm or deb packages). ``` diff --git a/src/app.rs b/src/app.rs index 31e1a067b..eeabdab30 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,17 +5,17 @@ use std::{ }; use glium::{SwapBuffersError, glutin::Event}; use glium::glutin::dpi::{LogicalPosition, LogicalSize}; -use webrender::api::{RenderApi, HitTestFlags, DevicePixel, PipelineId, Epoch}; +use webrender::api::{HitTestFlags, DevicePixel}; use webrender::PipelineInfo; use image::ImageError; -use euclid::{TypedScale, TypedSize2D}; +use euclid::TypedSize2D; #[cfg(feature = "logging")] use log::LevelFilter; use { images::ImageType, errors::{FontError, ClipboardError}, window::{Window, WindowCreateOptions, WindowCreateError, WindowId}, - css_parser::{FontId, PixelValue, FontSize}, + css_parser::{FontId, PixelValue}, text_cache::TextId, dom::UpdateScreen, window::FakeWindow, @@ -184,7 +184,7 @@ impl<'a, T: Layout> App<'a, T> { fn run_inner(&mut self) -> Result<(), RuntimeError> { use std::{thread, time::{Duration, Instant}}; - use window::{ReadOnlyWindow, WindowInfo}; + use window::ReadOnlyWindow; let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; @@ -316,7 +316,7 @@ impl<'a, T: Layout> App<'a, T> { fn initialize_ui_state(windows: &[Window], app_state: &AppState<'a, T>) -> Vec> { - use window::{ReadOnlyWindow, WindowInfo}; + use window::ReadOnlyWindow; windows.iter().enumerate().map(|(idx, w)| { let window_id = WindowId { id: idx }; @@ -520,7 +520,6 @@ enum WindowCloseEvent { fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> WindowCloseEvent { use glium::glutin::WindowEvent; - use glium::glutin::dpi::LogicalSize; match event { Event::WindowEvent { event, .. } => { @@ -729,7 +728,6 @@ fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { // to zero, which glium doesn't know about, so on the next frame it tries to draw with shader 0 fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D) { - use glium::backend::Facade; use gleam::gl; use window::get_gl_context; diff --git a/src/app_state.rs b/src/app_state.rs index 35d8e98b1..e38bba1d3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,7 +9,6 @@ use { FastHashMap, text_cache::TextId, window::FakeWindow, - window_state::WindowState, task::Task, dom::UpdateScreen, traits::Layout, diff --git a/src/cache.rs b/src/cache.rs index 10c2d5ef2..a46b1cb78 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -90,8 +90,6 @@ impl DomTreeCache { pub(crate) fn update(&mut self, new_root: NodeId, new_nodes_arena: &Arena>) -> DomChangeSet { - use std::hash::Hash; - if let Some(previous_root) = self.previous_layout.root { // let mut changeset = DomChangeSet::empty(); let new_tree = new_nodes_arena.transform(|data, _| data.calculate_node_data_hash()); @@ -101,7 +99,6 @@ impl DomTreeCache { changeset } else { // initialize arena - use std::iter::FromIterator; self.previous_layout.arena = new_nodes_arena.transform(|data, _| data.calculate_node_data_hash()); self.previous_layout.root = Some(new_root); DomChangeSet { @@ -126,33 +123,7 @@ impl DomTreeCache { changeset.added_nodes.insert(NodeId::new(next_idx), next_hash.data); } } -/* - loop { - match (previous_iter.next(), next_iter.next().enumerate()) { - (None, None) => { - // println!("chrildren: old has no children, new has no children!"); - break; - }, - (Some(_), None) => { - prev = previous_iter.next(); - }, - (None, Some(next_hash)) => { - // println!("chrildren: no old hash, but subtree has to be added: {:?}!", new_next_id); - // TODO: add subtree - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - next = next_iter.next(); - next_idx += 1; - }, - (Some(old_hash), Some(next_hash)) => { - if old_hash.data != next_hash.data { - changeset.added_nodes.insert(NodeId { index: next_idx }, next_hash.data); - } - next = next_iter.next(); - next_idx += 1; - } - } - } -*/ + changeset } diff --git a/src/compositor.rs b/src/compositor.rs index 01a4bc201..1f8c8280a 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -2,20 +2,14 @@ //! This makes it possible to use OpenGL images in the background and compose SVG elements //! into the UI. -use std::sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}; +use std::sync::{Mutex, atomic::{Ordering, AtomicUsize}}; use webrender::{ ExternalImageHandler, ExternalImage, ExternalImageSource, api::{ExternalImageId, TexelRect, DevicePixel, Epoch}, }; -use glium::{ - Program, VertexBuffer, Display, - index::{NoIndices, PrimitiveType::TriangleStrip}, - texture::texture2d::Texture2d, - backend::Facade, -}; use euclid::TypedPoint2D; use { - FastHashMap, FastHashSet, + FastHashMap, dom::Texture, }; diff --git a/src/css.rs b/src/css.rs index 3059f6539..683e12330 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,5 +1,4 @@ //! CSS parsing and styling -use std::ops::Add; use { FastHashMap, traits::IntoParsedCssProperty, @@ -373,7 +372,7 @@ fn determine_static_or_dynamic_css_property<'a>(key: &'a str, value: &'a str) #[test] fn test_detect_static_or_dynamic_property() { - use css_parser::{TextAlignmentHorz, PixelParseError, InvalidValueErr}; + use css_parser::{TextAlignmentHorz, InvalidValueErr}; assert_eq!( determine_static_or_dynamic_css_property("text-align", " center "), Ok(CssDeclaration::Static(ParsedCssProperty::TextAlign(TextAlignmentHorz::Center))) diff --git a/src/display_list.rs b/src/display_list.rs index b9b9526e1..051882e56 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1,15 +1,10 @@ #![allow(unused_variables)] #![allow(unused_macros)] -use std::{ - collections::BTreeMap, - sync::atomic::{Ordering, AtomicUsize}, - fmt::Debug, -}; use webrender::api::*; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; -use cassowary::{Constraint, Solver, Variable}; +use cassowary::Constraint; use { FastHashMap, @@ -20,11 +15,9 @@ use { window::UiSolver, window_state::WindowSize, id_tree::{Arena, NodeId}, - css_parser::{self, *}, + css_parser::*, dom::{NodeData, NodeType::{self, *}}, css::Css, - cache::DomChangeSet, - ui_description::CssConstraintList, text_layout::{TextOverflowPass2, ScrollbarInfo}, images::ImageId, text_cache::TextId, @@ -260,8 +253,6 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { window_size: &WindowSize) -> Option { - use euclid::TypedScale; - let mut changeset = None; if let Some(root) = self.ui_descr.ui_descr_root { @@ -445,7 +436,6 @@ fn displaylist_handle_rect<'a>( }, GlTexture(texture) => { - use glium::GlObject; use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; let opaque = true; @@ -545,10 +535,7 @@ fn push_text( horz_alignment: TextAlignmentHorz, vert_alignment: TextAlignmentVert) { - use dom::NodeType::*; - use euclid::{TypedPoint2D, Length}; use text_layout; - use css_parser::{TextAlignmentHorz, TextOverflowBehaviour}; if text.is_empty_text(&*app_resources) { return; @@ -715,7 +702,6 @@ fn push_triangle( background_color: &BackgroundColor, direction: TriangleDirection) { - use euclid::TypedPoint2D; use self::TriangleDirection::*; // see: https://css-tricks.com/snippets/css/css-triangle/ @@ -1011,7 +997,6 @@ fn create_layout_constraints<'a>( window_size: &WindowSize) -> Vec { - use css_parser; use cassowary::strength::*; use constraints::{SizeConstraint, Strength}; diff --git a/src/dom.rs b/src/dom.rs index 1ae07b85f..7c07b22b4 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -6,7 +6,6 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, collections::BTreeMap, }; -use webrender::api::ColorU; use glium::{Texture2d, framebuffer::SimpleFrameBuffer}; use { window::WindowEvent, @@ -661,8 +660,6 @@ impl Dom { #[test] fn test_dom_sibling_1() { - use window::WindowInfo; - struct TestLayout { } impl Layout for TestLayout { diff --git a/src/id_tree.rs b/src/id_tree.rs index d341b816f..ec0161d4b 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -3,10 +3,9 @@ use std::{ mem, fmt, - ops::{Index, Add, AddAssign, IndexMut, Deref}, + ops::{Index, IndexMut}, hash::{Hasher, Hash}, collections::BTreeMap, - cmp::Ordering, }; pub use self::node_id::NodeId; diff --git a/src/lib.rs b/src/lib.rs index 0852f2a04..133f9925e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ #![deny(unused_must_use)] #![deny(missing_copy_implementations)] #![allow(dead_code)] -#![allow(unused_imports)] +#![deny(unused_imports)] #![windows_subsystem = "windows"] @@ -53,7 +53,7 @@ extern crate harfbuzz_rs; extern crate tinyfiledialogs; extern crate clipboard2; extern crate font_loader; -#[macro_use(error, debug, info, log)] +#[macro_use(error, log)] extern crate log; #[cfg(feature = "logging")] extern crate fern; diff --git a/src/logging.rs b/src/logging.rs index d19292b87..bc18cec89 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -13,7 +13,6 @@ pub(crate) fn set_up_logging(log_file_path: Option, log_level: LevelFilt { use std::io::{Error as IoError, ErrorKind as IoErrorKind}; - use log::LevelFilter; use fern::{Dispatch, log_file}; let log_location = { diff --git a/src/resources.rs b/src/resources.rs index 4ed0100c2..2c53c0f10 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,27 +1,19 @@ -use images::ImageId; -use css_parser::FontSize; -use text_layout::RUSTTYPE_SIZE_HACK; -use text_layout::PX_TO_PT; -use text_layout::split_text_into_words; -use webrender::api::Epoch; -use dom::Texture; -use text_cache::TextCache; -use traits::Layout; -use std::sync::atomic::{AtomicUsize, Ordering}; -use webrender::api::{ImageKey, FontKey, FontInstanceKey}; -use FastHashMap; use std::io::Read; -use images::{ImageState, ImageType}; -use font::{FontState, FontError}; -use image::{self, ImageError, DynamicImage, GenericImage}; -use webrender::api::{ImageData, ImageDescriptor, ImageFormat}; use std::collections::hash_map::Entry::*; +use text_layout::{PX_TO_PT, split_text_into_words}; +use text_cache::{TextId, TextCache}; +use webrender::api::{FontKey, FontInstanceKey}; +use FastHashMap; +use font::{FontState, FontError}; +use image::{self, ImageError}; +use images::{ImageId, ImageState, ImageType}; use app_units::Au; -use css_parser; -use css_parser::FontId::{self, ExternalFont}; -use text_cache::TextId; use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; use rusttype::Font; +use css_parser::{ + FontSize, + FontId::{self, ExternalFont} +}; /// Font and image keys /// @@ -221,9 +213,6 @@ impl<'a> AppResources<'a> { pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) -> TextId { - use rusttype::Scale; - use std::rc::Rc; - // First, insert the text into the text cache let id = self.add_text_uncached(text); self.cache_text(id, font_id.clone(), font_size); @@ -238,7 +227,7 @@ impl<'a> AppResources<'a> { // We need to assume that the actual string contents have already been stored in self.text_cache // Otherwise, how would the TextId be valid? let text = self.text_cache.string_cache.get(&id).expect("Invalid text Id"); - let font_size_no_line_height = Scale::uniform(size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT); + let font_size_no_line_height = Scale::uniform(size.0.to_pixels() * PX_TO_PT); let rusttype_font = self.font_data.get(&font).expect("Invalid font ID"); let words = split_text_into_words(text.as_ref(), &rusttype_font.0, font_size_no_line_height); diff --git a/src/task.rs b/src/task.rs index 8efa2492f..8a09f624f 100644 --- a/src/task.rs +++ b/src/task.rs @@ -5,7 +5,6 @@ use std::{ thread::{spawn, JoinHandle}, }; use { - app_state::AppState, traits::Layout, }; diff --git a/src/text_cache.rs b/src/text_cache.rs index 5ea7ab090..11971a285 100644 --- a/src/text_cache.rs +++ b/src/text_cache.rs @@ -1,7 +1,4 @@ -use std::{ - rc::Rc, - sync::atomic::{Ordering, AtomicUsize}, -}; +use std::sync::atomic::{Ordering, AtomicUsize}; use { FastHashMap, css_parser::{FontId, FontSize}, diff --git a/src/text_layout.rs b/src/text_layout.rs index 7b5e88311..b703ff056 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -1,7 +1,7 @@ #![allow(unused_variables, dead_code)] use webrender::api::LayoutPixel; -use euclid::{Length, TypedRect, TypedSize2D, TypedPoint2D}; +use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; use { resources::AppResources, @@ -15,10 +15,6 @@ use { pub use webrender::api::GlyphInstance; -/// Rusttype has a certain sizing hack, I have no idea where this number comes from -/// Without this adjustment, we won't have the correct horizontal spacing -pub(crate) const RUSTTYPE_SIZE_HACK: f32 = /* 72.0 / 41.0 */ 1.0; - pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; /// Words are a collection of glyph information, i.e. how much @@ -143,7 +139,7 @@ pub struct FontMetrics { vertical_advance: f32, /// Offset of the font from the top of the bounding rectangle offset_top: f32, - /// Font size (for rusttype, includes the `RUSTTYPE_SIZE_HACK`) in **pt** (not px) + /// Font size (for rusttype) in **pt** (not px) /// Used for vertical layouting (since it includes the line height) font_size_with_line_height: Scale, /// Same as `font_size_with_line_height` but without the line height incorporated. @@ -187,8 +183,6 @@ pub(crate) fn get_glyphs( scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) { - use css_parser::{TextOverflowBehaviour, TextOverflowBehaviourInner}; - let target_font = app_resources.font_data.get(target_font_id).expect("Drawing with invalid font!"); let font_metrics = calculate_font_metrics(&target_font.0, target_font_size, line_height); @@ -258,7 +252,7 @@ impl FontMetrics { fn calculate_font_metrics<'a>(font: &Font<'a>, font_size: &FontSize, line_height: Option) -> FontMetrics { - let font_size_f32 = font_size.0.to_pixels() * RUSTTYPE_SIZE_HACK * PX_TO_PT; + let font_size_f32 = font_size.0.to_pixels() * PX_TO_PT; let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; let font_size_with_line_height = Scale::uniform(font_size_f32 * line_height); let font_size_no_line_height = Scale::uniform(font_size_f32); @@ -430,8 +424,6 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: }, cur_char => { // Regular character - use rusttype::Point; - let g = font.glyph(cur_char); let id = g.id(); @@ -639,9 +631,10 @@ fn estimate_overflow_pass_2( fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) -> Vec { + /* use harfbuzz_rs::*; use harfbuzz_rs::rusttype::SetRustTypeFuncs; - /* + let path = "path/to/some/font_file.otf"; let index = 0; //< face index in the font file let face = Face::from_file(path, index).unwrap(); diff --git a/src/traits.rs b/src/traits.rs index cf2d9ac9c..8ff3ce233 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,17 +2,17 @@ use std::{ collections::BTreeMap, rc::Rc, cell::RefCell, - hash::Hash, sync::{Arc, Mutex}, }; use { dom::{NodeData, Dom}, ui_description::{StyledNode, CssConstraintList, UiDescription}, css::{Css, CssRule}, - window::WindowInfo, id_tree::{NodeId, Arena}, css_parser::{ParsedCssProperty, CssParsingError}, }; +#[cfg(not(test))] +use window::WindowInfo; /// The core trait that has to be implemented for the app model to provide a /// Model -> View serialization. @@ -45,23 +45,23 @@ pub(crate) struct ParsedCss<'a> { } /// Convenience trait for the `css.set_dynamic_property()` function. -/// +/// /// This trait exists because `TryFrom` / `TryInto` are not yet stabilized. /// This is the same as `Into`, but with an additional error /// case (since the parsing of the CSS value could potentially fail) /// /// Using this trait you can write: `css.set_dynamic_property("var", ("width", "500px"))` -/// because `IntoParsedCssProperty` is implemented for `(&str, &str)`. +/// because `IntoParsedCssProperty` is implemented for `(&str, &str)`. /// -/// Note that the properties have to be re-parsed on every frame (which incurs a -/// small per-frame performance hit), however `("width", "500px")` is easier to +/// Note that the properties have to be re-parsed on every frame (which incurs a +/// small per-frame performance hit), however `("width", "500px")` is easier to /// read than `ParsedCssProperty::Width(PixelValue::Pixels(500))` pub trait IntoParsedCssProperty<'a> { fn into_parsed_css_property(self) -> Result>; } /// Convenience trait that allows the `app_state.modify()` - only implemented for -/// `Arc` - shortly locks the app state mutex, modifies it and unlocks +/// `Arc` - shortly locks the app state mutex, modifies it and unlocks /// it again. /// /// Note: Usually when doing asynchronous programming you don't want to block the main diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 9d5cf6f54..eacec0d30 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1,16 +1,16 @@ use std::{ fmt, rc::Rc, - io::{Error as IoError, Read}, + io::{Error as IoError}, sync::{Mutex, atomic::{Ordering, AtomicUsize}}, - cell::{UnsafeCell, RefCell, RefMut}, + cell::{RefCell, RefMut}, hash::{Hash, Hasher}, collections::hash_map::Entry::*, }; use glium::{ backend::Facade, index::PrimitiveType, - DrawParameters, IndexBuffer, VertexBuffer, Display, - Texture2d, Program, Api, Surface, + DrawParameters, IndexBuffer, VertexBuffer, + Program, Api, Surface, }; use lyon::{ tessellation::{ @@ -22,13 +22,13 @@ use lyon::{ }, }, path::{ - default::{Builder, Path}, + default::{Builder}, builder::{PathBuilder, FlatPathBuilder}, PathEvent, }, geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; -use webrender::api::{ColorU, ColorF, LayoutPixel, GlyphInstance}; +use webrender::api::{ColorU, ColorF, GlyphInstance}; use rusttype::{Font, Glyph}; use { FastHashMap, @@ -357,8 +357,6 @@ impl SvgCache { fn get_stroke_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) -> Option, IndexBuffer)>> { - use std::collections::hash_map::Entry::*; - use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; { let rmut = self.stroke_vertex_index_buffer_cache.borrow_mut(); @@ -376,9 +374,6 @@ impl SvgCache { fn get_vertices_and_indices<'a, F: Facade>(&'a self, window: &F, id: &SvgLayerId) -> Option, IndexBuffer)>> { - use std::collections::hash_map::Entry::*; - use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; - // We need the SvgCache to call this function immutably, otherwise we can't // use it from the Layout::layout() function { @@ -1362,13 +1357,11 @@ pub struct SvgRect { mod svg_to_lyon { - use std::{slice, iter, io::Read}; use lyon::{ math::Point, - path::{PathEvent, iterator::PathIter}, - tessellation::{self, StrokeOptions}, + path::PathEvent, }; - use resvg::usvg::{self, ViewBox, Transform, Tree, Path, PathSegment, + use resvg::usvg::{ViewBox, Transform, Tree, PathSegment, Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; use widgets::svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, SvgLayerType, SvgStyle, SvgCallbacks, SvgParseError, SvgTransformId, diff --git a/src/window.rs b/src/window.rs index 1d6ea9aba..a744ea79c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,7 @@ use glium::{ IncompatibleOpenGl, Display, debug::DebugCallbackBehavior, glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, - MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder, dpi::LogicalSize}, + MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, backend::{Context, Facade, glutin::DisplayCreationError}, }; use gleam::gl::{self, Gl}; @@ -33,7 +33,6 @@ use { cache::{EditVariableCache, DomTreeCache}, id_tree::NodeId, compositor::Compositor, - text_cache::TextCache, app::FrameEventInfo, resources::AppResources, }; @@ -105,10 +104,6 @@ impl Facade for ReadOnlyWindow { } } -use glium::{Vertex, VertexBuffer, IndexBuffer, index::PrimitiveType}; -use glium::vertex::BufferCreationError as VertexBufferCreationError; -use glium::index::BufferCreationError as IndexBufferCreationError; - impl ReadOnlyWindow { pub fn get_physical_size(&self) -> (u32, u32) { @@ -547,8 +542,6 @@ impl Window { /// Creates a new window pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { - use glium::glutin::dpi::{LogicalPosition, LogicalSize}; - let events_loop = EventsLoop::new(); let monitor = match options.monitor { @@ -800,7 +793,6 @@ impl Window { } pub(crate) fn update_from_external_window_state(&mut self, frame_event_info: &mut FrameEventInfo) { - use webrender::api::{DeviceUintSize, WorldPoint, LayoutSize}; if let Some(new_size) = frame_event_info.new_window_size { self.state.size.dimensions = new_size; diff --git a/src/window_state.rs b/src/window_state.rs index ae47d6264..74a8c4be6 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -3,7 +3,7 @@ use glium::glutin::{ Window, Event, WindowEvent, KeyboardInput, ElementState, - MouseCursor, VirtualKeyCode, MouseButton, MouseScrollDelta, TouchPhase, + MouseCursor, VirtualKeyCode, MouseScrollDelta, ModifiersState, dpi::{LogicalPosition, LogicalSize}, }; use std::collections::HashSet; @@ -173,11 +173,12 @@ impl WindowState // so that we are ready for the next frame pub(crate) fn determine_callbacks(&mut self, event: &Event) -> Vec { - use glium::glutin::Event::WindowEvent; - use glium::glutin::WindowEvent::*; - use glium::glutin::{ElementState, MouseButton }; - use glium::glutin::MouseButton::*; - use glium::glutin::dpi::LogicalPosition; + use glium::glutin::{ + Event::WindowEvent, + WindowEvent::*, + MouseButton::*, + dpi::LogicalPosition, + }; let event = if let WindowEvent { event, .. } = event { event } else { return Vec::new(); }; From 9423926eebd3da4cc4ca83855a21bcb9ef19fec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 00:52:17 +0200 Subject: [PATCH 179/868] Fixed compile errors in release mode --- src/id_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/id_tree.rs b/src/id_tree.rs index ec0161d4b..d7492f771 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -50,7 +50,7 @@ mod node_id { } #[cfg(not(debug_assertions))] { - unsafe { NonZeroUsizeHack(NonZeroUsize::new_unchecked(value + 1)) } + NodeId { index: unsafe { NonZeroUsize::new_unchecked(value + 1) } } } } From 4282c13d4c05edfcbc82e5416312f730e5e9e155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 00:54:35 +0200 Subject: [PATCH 180/868] Try removing unsafe code regarding wayland Send / Sync --- src/window.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/window.rs b/src/window.rs index a744ea79c..2144954fa 100644 --- a/src/window.rs +++ b/src/window.rs @@ -384,17 +384,6 @@ struct Notifier { events_loop_proxy: EventsLoopProxy, } -// For some reason, the wayland implementation has problems with this (?) -// However, the glium documentation explicitly says that EventsLoopProxy can -// be shared across threads. -// -// This was working absolutely fine before #cc6a8b, so I don't really get why -// this code suddenly doesn't compile anymore on wayland - I didn't even touch -// the code related to the notifier in months and didn't change the glium version -// and now it suddenly doesn't want to compile anymore. -unsafe impl Send for Notifier { } -unsafe impl Sync for Notifier { } - impl Notifier { fn new(events_loop_proxy: EventsLoopProxy) -> Notifier { Notifier { From bbbc28fa2a6fffd43f78cfa7a091e47efd34c632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 6 Aug 2018 02:41:28 +0200 Subject: [PATCH 181/868] Reduced unsafe code - Reduced one unsafe block by using Vec::split_at_mut - Replaced manual gl.get_proc_adress with existing get_gl_context function --- src/app.rs | 10 ++++------ src/id_tree.rs | 37 ++++++++++++++++++++++++++++--------- src/lib.rs | 1 - src/window.rs | 20 ++++---------------- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/app.rs b/src/app.rs index eeabdab30..5ff0de9d3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -196,12 +196,7 @@ impl<'a, T: Layout> App<'a, T> { let mut closed_windows = Vec::::new(); 'window_loop: for (idx, ref mut window) in self.windows.iter_mut().enumerate() { -/* - unsafe { - use glium::glutin::GlContext; - window.display.gl_window().make_current().unwrap(); - } -*/ + let window_id = WindowId { id: idx }; let mut frame_event_info = FrameEventInfo::default(); @@ -731,6 +726,9 @@ fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D use gleam::gl; use window::get_gl_context; + // use glium::glutin::GlContext; + // unsafe { window.display.gl_window().make_current().unwrap(); } + let mut current_program = [0_i32]; unsafe { get_gl_context(&window.display).unwrap().get_integer_v(gl::CURRENT_PROGRAM, &mut current_program) }; window.renderer.as_mut().unwrap().render(framebuffer_size).unwrap(); diff --git a/src/id_tree.rs b/src/id_tree.rs index d7492f771..a52198b29 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -54,6 +54,7 @@ mod node_id { } } + #[inline] pub fn index(&self) -> usize { self.index.get() - 1 } @@ -194,8 +195,10 @@ impl Arena { self.nodes_len() == 0 } - /// Appends another arena to the end of the current arena. - /// Highly unsafe if you don't know what you're doing + /// Appends another arena to the end of the current arena + /// (by simply appending the two Vec of nodes) + /// Can potentially mess up internal IDs, only use this if you + /// know what you're doing pub(crate) fn append(&mut self, other: &mut Arena) { self.nodes.append(&mut other.nodes); } @@ -223,13 +226,30 @@ trait GetPairMut { impl GetPairMut for Vec { fn get_pair_mut(&mut self, a: usize, b: usize, same_index_error_message: &'static str) - -> (&mut T, &mut T) { - if a == b { - panic!(same_index_error_message) + -> (&mut T, &mut T) + { + #[cfg(debug_assertions)] { + if a == b { + panic!(same_index_error_message) + } } - unsafe { - let self2 = mem::transmute_copy::<&mut Vec, &mut Vec>(&self); - (&mut self[a], &mut self2[b]) + + let a_is_lower; + + let min = if a < b { + a_is_lower = true; + a + } else { + a_is_lower = false; + b + }; + + let (low, high) = self.split_at_mut(min + 1); + + if a_is_lower { + (&mut low[a], &mut high[b - (min + 1)]) + } else { + (&mut high[a - (min + 1)], &mut low[b]) } } } @@ -555,7 +575,6 @@ pub struct ReverseChildren<'a, T: 'a> { impl_node_iterator!(ReverseChildren, |node: &Node| node.previous_sibling); - /// An iterator of references to a given node and its descendants, in tree order. pub struct Descendants<'a, T: 'a>(Traverse<'a, T>); diff --git a/src/lib.rs b/src/lib.rs index 133f9925e..49620bca5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,6 @@ #![deny(unused_must_use)] #![deny(missing_copy_implementations)] #![allow(dead_code)] -#![deny(unused_imports)] #![windows_subsystem = "windows"] diff --git a/src/window.rs b/src/window.rs index 2144954fa..68eba1042 100644 --- a/src/window.rs +++ b/src/window.rs @@ -89,7 +89,6 @@ impl FakeWindow { pub fn get_mouse_state(&self) -> MouseState { self.state.mouse_state } - } /// Read-only window which can be used to create / draw @@ -123,10 +122,9 @@ impl ReadOnlyWindow { /// Make the window active (OpenGL) - necessary before /// starting to draw on any window-owned texture pub fn make_current(&self) { - unsafe { - use glium::glutin::GlContext; - self.inner.gl_window().make_current().unwrap(); - } + use glium::glutin::GlContext; + let gl_window = self.inner.gl_window(); + unsafe { gl_window.make_current().unwrap() }; } /// Unbind the current framebuffer manually. Is also executed on `Drop`. @@ -134,17 +132,7 @@ impl ReadOnlyWindow { /// TODO: Is it necessary to expose this or is it enough to just /// unbind the framebuffer on drop? pub fn unbind_framebuffer(&self) { - let gl = match self.inner.gl_window().get_api() { - glutin::Api::OpenGl => unsafe { - gl::GlFns::load_with(|symbol| - self.inner.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::OpenGlEs => unsafe { - gl::GlesFns::load_with(|symbol| - self.inner.gl_window().get_proc_address(symbol) as *const _) - }, - glutin::Api::WebGl => unreachable!(), - }; + let gl = get_gl_context(&*self.inner).unwrap(); gl.bind_framebuffer(gl::FRAMEBUFFER, 0); } From 0cf94571dd816dac924dece7a1b1a22b6f08d31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 7 Aug 2018 01:29:26 +0200 Subject: [PATCH 182/868] Fixed crash for background: none --- examples/debug.rs | 6 +++--- src/css_parser.rs | 4 +++- src/display_list.rs | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 54c396d38..20ac4963d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -9,7 +9,7 @@ use azul::text_layout::*; use std::fs; -const FONT_ID: FontId = FontId::BuiltinFont("serif"); +const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); const FONT_BYTES: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); #[derive(Debug)] @@ -40,8 +40,8 @@ impl Layout for MyAppData { } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file - Button::with_label("Azul App").dom() - .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) + Button::with_label("Load SVG file...").dom() + .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) } } } diff --git a/src/css_parser.rs b/src/css_parser.rs index eefe0ed9b..49f2f36f6 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -980,7 +980,8 @@ impl_from!(CssImageParseError, CssBackgroundParseError::ImageParseError); pub enum Background { LinearGradient(LinearGradientPreInfo), RadialGradient(RadialGradientPreInfo), - Image(CssImageId) + Image(CssImageId), + NoBackground, } impl<'a> From for Background { @@ -1108,6 +1109,7 @@ fn parse_css_background<'a>(input: &'a str) let first_item = input_iter.next(); let background_type = match first_item { + Some("none") => { return Ok(Background::NoBackground); }, Some("linear-gradient") => LinearGradient, Some("repeating-linear-gradient") => RepeatingLinearGradient, Some("radial-gradient") => RadialGradient, diff --git a/src/display_list.rs b/src/display_list.rs index 051882e56..8708093e5 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -830,7 +830,8 @@ fn push_background( if let Some(image_id) = app_resources.css_ids_to_image_ids.get(&css_image_id.0) { push_image(info, builder, bounds, app_resources, image_id); } - } + }, + Background::NoBackground => { }, } } From fdaa1a15044e9305afdfac924b474c47fbfde114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 7 Aug 2018 02:45:04 +0200 Subject: [PATCH 183/868] Added SvgText and SvgTextPlacement API Slightly duplicated code in svg.rs due to different rotation matrices applications - not optimal, this needs to be reworked if possible --- examples/debug.rs | 111 +++++++----------------- src/widgets/mod.rs | 1 + src/widgets/svg.rs | 205 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 83 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 20ac4963d..29ffe89a4 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -5,7 +5,6 @@ extern crate azul; use azul::prelude::*; use azul::widgets::*; use azul::dialogs::*; -use azul::text_layout::*; use std::fs; @@ -52,95 +51,43 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); let font_id = FontId::ExternalFont(String::from("Webly Sleeky UI")); - let text_style = SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }); - let font = resources.get_font(&font_id).unwrap(); - let vectorized_font = vector_font_cache.get_font(&font_id).unwrap(); - let font_size = FontSize::px(10.0); - let test_curve = SampledBezierCurve::from_curve(&[ + let curve = SampledBezierCurve::from_curve(&[ BezierControlPoint { x: 0.0, y: 0.0 }, BezierControlPoint { x: 40.0, y: 120.0 }, BezierControlPoint { x: 80.0, y: 120.0 }, BezierControlPoint { x: 120.0, y: 0.0 }, ]); - layers.push(text_on_curve("Hello Worldaslfkdlfkasdjfldkjf", &test_curve, text_style, &font.0, vectorized_font, font_size)); - layers.push(test_curve.draw_circles()); - layers.push(test_curve.draw_lines()); - layers.push(test_curve.draw_control_handles()); + layers.push(SvgText { + font_size: FontSize::px(10.0), + font_id: &font_id, + text: "On Curve!!!!", + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::OnCubicBezierCurve(curve), + }.to_svg_layer(vector_font_cache, resources)); + + layers.push(SvgText { + font_size: FontSize::px(10.0), + font_id: &font_id, + text: "Rotated", + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Rotated(-30.0), + }.to_svg_layer(vector_font_cache, resources)); + + layers.push(SvgText { + font_size: FontSize::px(10.0), + font_id: &font_id, + text: "Unmodified", + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Unmodified, + }.to_svg_layer(vector_font_cache, resources)); + + layers.push(curve.draw_lines()); + layers.push(curve.draw_control_handles()); layers } -fn text_on_curve( - text: &str, - curve: &SampledBezierCurve, - text_style: SvgStyle, - font: &Font, - vector_font: &VectorizedFont, - font_size: FontSize) --> SvgLayerResource -{ - let font_metrics = FontMetrics::new(font, &font_size, None); - let layout = layout_text(text, font, &font_metrics); - - let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs, 0.0); - - let fill_vertices = text_style.fill.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_fill_vertices)) - }); - - let stroke_vertices = text_style.stroke.and_then(|_| { - Some(vector_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) - }); - - SvgLayerResource::Direct { - style: text_style, - fill: fill_vertices, - stroke: stroke_vertices, - } -} - -// Calculates the layout for one word block -fn vector_text_to_vertices( - font_size: &FontSize, - glyph_ids: &[GlyphInstance], - vectorized_font: &VectorizedFont, - original_font: &Font, - char_offsets: &[(f32, f32)], - char_rotations: &[BezierCharacterRotation], - transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> -) -> VerticesIndicesBuffer -{ - let fill_buf = glyph_ids.iter() - .filter_map(|gid| { - // 1. Transform glyph to vertex buffer && filter out all glyphs - // that don't have a vertex buffer - transform_func(vectorized_font, original_font, &GlyphId(gid.index)) - }) - .zip(char_rotations.into_iter()) - .zip(char_offsets.iter()) - .map(|((mut vertex_buf, char_rot), char_offset)| { - - let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue - - // 2. Scale characters to the final size - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - - // 3. Rotate individual characters inside of the word - let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); - - rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); - - // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x, *char_offset_y); - - vertex_buf - }) - .collect::>(); - - join_vertex_buffers(&fill_buf) -} - fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { @@ -197,10 +144,8 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { - use std::io::Cursor; - let mut app = App::new(MyAppData { map: None }, AppConfig::default()); - app.add_font("Webly Sleeky UI", &mut Cursor::new(FONT_BYTES)).unwrap(); + app.add_font("Webly Sleeky UI", &mut FONT_BYTES.clone()).unwrap(); app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 22d7fef18..20a0275bf 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,6 +9,7 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, + SvgText, SvgTextPlacement, BezierNormalVector, BezierCharacterRotation, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index eacec0d30..49854264e 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1668,6 +1668,211 @@ pub fn cubic_bezier_normal(curve: &[BezierControlPoint;4], t: f32) -> BezierNorm } } +#[derive(Debug, Copy, Clone)] +pub enum SvgTextPlacement { + /// Text is simply layouted from left-to-right + Unmodified, + /// Text is rotated by X degrees + Rotated(f32), + /// Text is placed on a cubic bezier curve + OnCubicBezierCurve(SampledBezierCurve), +} + +#[derive(Debug, Clone)] +pub struct SvgText<'a> { + pub font_size: FontSize, + pub font_id: &'a FontId, + pub text: &'a str, + pub style: SvgStyle, + pub placement: SvgTextPlacement, +} + +use resources::AppResources; +use text_layout::{FontMetrics, LayoutTextResult, layout_text}; + +impl<'a> SvgText<'a> { + pub fn to_svg_layer(&self, vectorized_fonts_cache: &VectorizedFontCache, resources: &AppResources) + -> SvgLayerResource + { + let font = resources.get_font(&self.font_id).unwrap().0; + let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id).unwrap(); + let font_metrics = FontMetrics::new(&font, &self.font_size, None); + + // TODO: cache the layout somehow? + let layout = layout_text(&self.text, &font, &font_metrics); + + match self.placement { + SvgTextPlacement::Unmodified => { + normal_text(&layout, self.style, &font, vectorized_font, &self.font_size) + }, + SvgTextPlacement::Rotated(degrees) => { + rotated_text(&layout, self.style, &font, vectorized_font, &self.font_size, degrees) + }, + SvgTextPlacement::OnCubicBezierCurve(curve) => { + text_on_curve(&layout, self.style, &font, vectorized_font, &self.font_size, &curve) + } + } + } +} + +fn normal_text( + layout: &LayoutTextResult, + text_style: SvgStyle, + font: &Font, + vector_font: &VectorizedFont, + font_size: &FontSize) +-> SvgLayerResource +{ + let fill_vertices = text_style.fill.and_then(|_| { + Some(normal_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, get_fill_vertices)) + }); + + let stroke_vertices = text_style.stroke.and_then(|_| { + Some(normal_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, get_stroke_vertices)) + }); + + SvgLayerResource::Direct { + style: text_style, + fill: fill_vertices, + stroke: stroke_vertices, + } +} + +fn normal_text_to_vertices( + font_size: &FontSize, + glyph_ids: &[GlyphInstance], + vectorized_font: &VectorizedFont, + original_font: &Font, + transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> +) -> VerticesIndicesBuffer +{ + let fill_buf = glyph_ids.iter() + .filter_map(|gid| transform_func(vectorized_font, original_font, &GlyphId(gid.index)).and_then(|vbuf| Some((vbuf, gid)))) + .map(|(mut vertex_buf, gid)| { + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); + vertex_buf + }) + .collect::>(); + + join_vertex_buffers(&fill_buf) +} + +fn rotated_text( + layout: &LayoutTextResult, + text_style: SvgStyle, + font: &Font, + vector_font: &VectorizedFont, + font_size: &FontSize, + rotation_degrees: f32) +-> SvgLayerResource +{ + let fill_vertices = text_style.fill.and_then(|_| { + Some(rotated_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, rotation_degrees, get_fill_vertices)) + }); + + let stroke_vertices = text_style.stroke.and_then(|_| { + Some(rotated_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, rotation_degrees, get_stroke_vertices)) + }); + + SvgLayerResource::Direct { + style: text_style, + fill: fill_vertices, + stroke: stroke_vertices, + } +} + +fn rotated_text_to_vertices( + font_size: &FontSize, + glyph_ids: &[GlyphInstance], + vectorized_font: &VectorizedFont, + original_font: &Font, + rotation_degrees: f32, + transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> +) -> VerticesIndicesBuffer +{ + let rotation_rad = rotation_degrees.to_radians(); + let (char_sin, char_cos) = (rotation_rad.sin(), rotation_rad.cos()); + let fill_buf = glyph_ids.iter() + .filter_map(|gid| transform_func(vectorized_font, original_font, &GlyphId(gid.index)).and_then(|vbuf| Some((vbuf, gid)))) + .map(|(mut vertex_buf, gid)| { + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + vertex_buf + }) + .collect::>(); + + join_vertex_buffers(&fill_buf) +} + +fn text_on_curve( + layout: &LayoutTextResult, + text_style: SvgStyle, + font: &Font, + vector_font: &VectorizedFont, + font_size: &FontSize, + curve: &SampledBezierCurve) +-> SvgLayerResource +{ + let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs, 0.0); + + let fill_vertices = text_style.fill.and_then(|_| { + Some(curved_vector_text_to_vertices(font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_fill_vertices)) + }); + + let stroke_vertices = text_style.stroke.and_then(|_| { + Some(curved_vector_text_to_vertices(font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) + }); + + SvgLayerResource::Direct { + style: text_style, + fill: fill_vertices, + stroke: stroke_vertices, + } +} + +// Calculates the layout for one word block +fn curved_vector_text_to_vertices( + font_size: &FontSize, + glyph_ids: &[GlyphInstance], + vectorized_font: &VectorizedFont, + original_font: &Font, + char_offsets: &[(f32, f32)], + char_rotations: &[BezierCharacterRotation], + transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> +) -> VerticesIndicesBuffer +{ + let fill_buf = glyph_ids.iter() + .filter_map(|gid| { + // 1. Transform glyph to vertex buffer && filter out all glyphs + // that don't have a vertex buffer + transform_func(vectorized_font, original_font, &GlyphId(gid.index)) + }) + .zip(char_rotations.into_iter()) + .zip(char_offsets.iter()) + .map(|((mut vertex_buf, char_rot), char_offset)| { + + let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue + + // 2. Scale characters to the final size + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + + // 3. Rotate individual characters inside of the word + let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); + + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + + // 4. Transform characters to their respective positions + transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x, *char_offset_y); + + vertex_buf + }) + .collect::>(); + + join_vertex_buffers(&fill_buf) +} + impl Svg { #[inline] From 91b3fcb44c1c228ad4e255cb2b03d168827312bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 7 Aug 2018 03:10:45 +0200 Subject: [PATCH 184/868] Added SvgTextLayout to prepare hit-testing for texts --- examples/debug.rs | 17 +++++++++++------ src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 25 +++++++++++++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 29ffe89a4..7edd5fd86 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -57,27 +57,32 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo BezierControlPoint { x: 80.0, y: 120.0 }, BezierControlPoint { x: 120.0, y: 0.0 }, ]); + let font_size = FontSize::px(10.0); + let font = resources.get_font(&font_id).unwrap().0; + let text_layout_1 = SvgTextLayout::from_str("On Curve!!!!", &font, &font_size); + let text_layout_2 = SvgTextLayout::from_str("Rotated", &font, &font_size); + let text_layout_3 = SvgTextLayout::from_str("Unmodified", &font, &font_size); layers.push(SvgText { - font_size: FontSize::px(10.0), + font_size: font_size, font_id: &font_id, - text: "On Curve!!!!", + text_layout: &text_layout_1, style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), placement: SvgTextPlacement::OnCubicBezierCurve(curve), }.to_svg_layer(vector_font_cache, resources)); layers.push(SvgText { - font_size: FontSize::px(10.0), + font_size: font_size, font_id: &font_id, - text: "Rotated", + text_layout: &text_layout_2, style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), placement: SvgTextPlacement::Rotated(-30.0), }.to_svg_layer(vector_font_cache, resources)); layers.push(SvgText { - font_size: FontSize::px(10.0), + font_size: font_size, font_id: &font_id, - text: "Unmodified", + text_layout: &text_layout_3, style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), placement: SvgTextPlacement::Unmodified, }.to_svg_layer(vector_font_cache, resources)); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 20a0275bf..670ec94e0 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,7 +9,7 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, - SvgText, SvgTextPlacement, + SvgText, SvgTextPlacement, SvgTextLayout, BezierNormalVector, BezierCharacterRotation, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 49854264e..b5a5b8eb2 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -36,6 +36,8 @@ use { traits::Layout, window::ReadOnlyWindow, css_parser::{FontId, FontSize}, + resources::AppResources, + text_layout::{FontMetrics, LayoutTextResult, layout_text}, }; pub use lyon::tessellation::VertexBuffers; @@ -1682,13 +1684,20 @@ pub enum SvgTextPlacement { pub struct SvgText<'a> { pub font_size: FontSize, pub font_id: &'a FontId, - pub text: &'a str, + pub text_layout: &'a SvgTextLayout, pub style: SvgStyle, pub placement: SvgTextPlacement, } -use resources::AppResources; -use text_layout::{FontMetrics, LayoutTextResult, layout_text}; +#[derive(Debug, Clone)] +pub struct SvgTextLayout(LayoutTextResult); + +impl SvgTextLayout { + pub fn from_str(text: &str, font: &Font, font_size: &FontSize) -> Self { + let font_metrics = FontMetrics::new(font, font_size, None); + SvgTextLayout(layout_text(text, font, &font_metrics)) + } +} impl<'a> SvgText<'a> { pub fn to_svg_layer(&self, vectorized_fonts_cache: &VectorizedFontCache, resources: &AppResources) @@ -1696,20 +1705,16 @@ impl<'a> SvgText<'a> { { let font = resources.get_font(&self.font_id).unwrap().0; let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id).unwrap(); - let font_metrics = FontMetrics::new(&font, &self.font_size, None); - - // TODO: cache the layout somehow? - let layout = layout_text(&self.text, &font, &font_metrics); match self.placement { SvgTextPlacement::Unmodified => { - normal_text(&layout, self.style, &font, vectorized_font, &self.font_size) + normal_text(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size) }, SvgTextPlacement::Rotated(degrees) => { - rotated_text(&layout, self.style, &font, vectorized_font, &self.font_size, degrees) + rotated_text(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size, degrees) }, SvgTextPlacement::OnCubicBezierCurve(curve) => { - text_on_curve(&layout, self.style, &font, vectorized_font, &self.font_size, &curve) + text_on_curve(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size, &curve) } } } From 0b3b4b2b6efe676084789b52fab52bb089a08c7b Mon Sep 17 00:00:00 2001 From: Brian Harris Date: Mon, 6 Aug 2018 22:19:42 -0500 Subject: [PATCH 185/868] Update README.md to indicate cmake is required on Windows too. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e7ce1f58b..e7b2b09fd 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ engine for rendering and a CSS / DOM model for layout and rendering ## Installation notes -On Linux, you currently need to install `cmake` before you can use azul. -CMake is used during the build process to compile servo-freetype. +You currently need to install [cmake](https://cmake.org/download/) before you can use azul. +CMake is used during the build process to compile servo-freetype and +harfbuzz-sys. For interfacing with the system clipboard, you also need `libxcb-xkb-dev`. Since azul uses the system-native fonts by default, you'll also need From b6e66b703bb7bc7e7b4425d77744fcdccd9c73ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 7 Aug 2018 09:28:01 +0200 Subject: [PATCH 186/868] Corrected On::MouseOver, insert default fonts into the VectorizedFontCache, added SvgBbox Note: SvgBbox doesn't work correctly right now because the text metrics calculation is broken. --- examples/debug.rs | 64 ++++++++++--------- src/app.rs | 15 ++++- src/app_state.rs | 2 +- src/lib.rs | 2 +- src/resources.rs | 5 ++ src/text_layout.rs | 28 ++++----- src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 149 +++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 208 insertions(+), 59 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7edd5fd86..9aba8892e 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -36,6 +36,7 @@ impl Layout for MyAppData { .with_zoom(map.zoom as f32) .dom(&info.window, &map.cache) .with_callback(On::Scroll, Callback(scroll_map_contents)) + .with_callback(On::MouseOver, Callback(check_hovered_font)) } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file @@ -50,7 +51,7 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); - let font_id = FontId::ExternalFont(String::from("Webly Sleeky UI")); + let font_id = FONT_ID; let curve = SampledBezierCurve::from_curve(&[ BezierControlPoint { x: 0.0, y: 0.0 }, BezierControlPoint { x: 40.0, y: 120.0 }, @@ -59,33 +60,33 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo ]); let font_size = FontSize::px(10.0); let font = resources.get_font(&font_id).unwrap().0; - let text_layout_1 = SvgTextLayout::from_str("On Curve!!!!", &font, &font_size); - let text_layout_2 = SvgTextLayout::from_str("Rotated", &font, &font_size); - let text_layout_3 = SvgTextLayout::from_str("Unmodified", &font, &font_size); - - layers.push(SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &text_layout_1, - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::OnCubicBezierCurve(curve), - }.to_svg_layer(vector_font_cache, resources)); - - layers.push(SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &text_layout_2, - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::Rotated(-30.0), - }.to_svg_layer(vector_font_cache, resources)); - - layers.push(SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &text_layout_3, - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::Unmodified, - }.to_svg_layer(vector_font_cache, resources)); + + let texts = [ + SvgText { + font_size: font_size, + font_id: &font_id, + text_layout: &SvgTextLayout::from_str("On Curve!!!!", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::OnCubicBezierCurve(curve), + }, + SvgText { + font_size: font_size, + font_id: &font_id, + text_layout: &SvgTextLayout::from_str("Rotated", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Rotated(-30.0), + }, + SvgText { + font_size: font_size, + font_id: &font_id, + text_layout: &SvgTextLayout::from_str("Unmodified\nCool", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Unmodified, + }, + ]; + + layers.extend(texts.iter().map(|t| t.to_svg_layer(vector_font_cache, resources))); + layers.extend(texts.iter().map(|t| t.get_bbox().draw_lines())); layers.push(curve.draw_lines()); layers.push(curve.draw_control_handles()); @@ -93,6 +94,11 @@ fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFo layers } +fn check_hovered_font(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + + UpdateScreen::DontRedraw +} + fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { app_state.data.modify(|data| { if let Some(map) = data.map.as_mut() { @@ -129,7 +135,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv // Pre-vectorize the glyphs of the font into vertex buffers let (font, _) = app_state.get_font(&font_id)?; - let mut vectorized_font_cache = VectorizedFontCache::new(); + let mut vectorized_font_cache = VectorizedFontCache::new(&app_state.resources); vectorized_font_cache.insert_if_not_exist(font_id, font); app_state.data.modify(|data| data.map = Some(Map { diff --git a/src/app.rs b/src/app.rs index 5ff0de9d3..98f394a0a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -598,6 +598,8 @@ fn do_hit_test_and_call_callbacks( .get(&item.tag.0) .and_then(|callback_list| Some((item, callback_list))) ) { + use dom::On; + // TODO: currently we don't have information about what DOM node was hit let window_event = WindowEvent { window: window_id.id, @@ -606,12 +608,21 @@ fn do_hit_test_and_call_callbacks( cursor_in_viewport: (item.point_in_viewport.x, item.point_in_viewport.y), }; - // Invoke callback if necessary - for callback_id in callbacks_filter_list.iter().filter_map(|on| callback_list.get(on)) { + let mut invoke_callback = |callback_id| { let Callback(callback_func) = ui_state_cache[window_id.id].callback_list[callback_id]; if (callback_func)(app_state, window_event) == UpdateScreen::Redraw { should_update_screen = UpdateScreen::Redraw; } + }; + + // Invoke On::MouseOver callback + if let Some(callback_id) = callback_list.get(&On::MouseOver) { + invoke_callback(callback_id); + } + + // Invoke callback if necessary + for callback_id in callbacks_filter_list.iter().filter_map(|on| callback_list.get(on)) { + invoke_callback(callback_id); } } diff --git a/src/app_state.rs b/src/app_state.rs index e38bba1d3..71063852a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -35,7 +35,7 @@ pub struct AppState<'a, T: Layout> { /// ``` pub windows: Vec, /// Fonts and images that are currently loaded into the app - pub(crate) resources: AppResources<'a>, + pub resources: AppResources<'a>, /// Currently running deamons (polling functions) pub(crate) deamons: FastHashMap UpdateScreen>, /// Currently running tasks (asynchronous functions running on a different thread) diff --git a/src/lib.rs b/src/lib.rs index 49620bca5..4ea91bd90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,6 @@ #[macro_use] pub extern crate glium; pub extern crate gleam; -pub extern crate image; #[macro_use] extern crate lazy_static; @@ -58,6 +57,7 @@ extern crate log; extern crate fern; #[cfg(feature = "logging")] extern crate backtrace; +extern crate image; #[cfg(not(target_os = "linux"))] extern crate nfd; diff --git a/src/resources.rs b/src/resources.rs index 2c53c0f10..08b33964d 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -90,6 +90,11 @@ fn load_system_fonts<'a>(fonts: &mut FastHashMap, impl<'a> AppResources<'a> { + /// Returns the IDs of all currently loaded fonts in `self.font_data` + pub fn get_loaded_fonts(&self) -> Vec { + self.font_data.keys().cloned().collect() + } + /// See `AppState::add_image()` pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> diff --git a/src/text_layout.rs b/src/text_layout.rs index b703ff056..e980ce9f4 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -15,7 +15,7 @@ use { pub use webrender::api::GlyphInstance; -pub(crate) const PX_TO_PT: f32 = 72.0 / 96.0; +pub const PX_TO_PT: f32 = 72.0 / 96.0; /// Words are a collection of glyph information, i.e. how much /// horizontal space each of the words in a text block and how much @@ -132,19 +132,19 @@ pub(crate) struct ScrollbarInfo { #[derive(Debug, Copy, Clone)] pub struct FontMetrics { /// Width of the space character - space_width: f32, + pub space_width: f32, /// Usually 4 * space_width - tab_width: f32, + pub tab_width: f32, /// font_size * line_height - vertical_advance: f32, + pub vertical_advance: f32, /// Offset of the font from the top of the bounding rectangle - offset_top: f32, + pub offset_top: f32, /// Font size (for rusttype) in **pt** (not px) /// Used for vertical layouting (since it includes the line height) - font_size_with_line_height: Scale, + pub font_size_with_line_height: Scale, /// Same as `font_size_with_line_height` but without the line height incorporated. /// Used for horizontal layouting - font_size_no_line_height: Scale, + pub font_size_no_line_height: Scale, } /// ## Inputs @@ -253,7 +253,7 @@ impl FontMetrics { fn calculate_font_metrics<'a>(font: &Font<'a>, font_size: &FontSize, line_height: Option) -> FontMetrics { let font_size_f32 = font_size.0.to_pixels() * PX_TO_PT; - let line_height = match line_height { Some(lh) => (lh.0).number, None => 1.0 }; + let line_height = line_height.and_then(|lh| Some(lh.0.number)).unwrap_or(1.0); let font_size_with_line_height = Scale::uniform(font_size_f32 * line_height); let font_size_no_line_height = Scale::uniform(font_size_f32); @@ -656,7 +656,6 @@ fn calculate_harfbuzz_adjustments<'a>(text: &str, font: &Font<'a>) /// If `max_horizontal_width` is `None`, it means that the text is allowed to overflow /// the rectangle horizontally -#[inline(always)] fn words_to_left_aligned_glyphs<'a>( words: &Words, font: &Font<'a>, @@ -666,7 +665,7 @@ fn words_to_left_aligned_glyphs<'a>( { let words = &words.0; - let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, offset_top, font_size_no_line_height, .. } = *font_metrics; // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, // left-aligned @@ -756,7 +755,7 @@ fn words_to_left_aligned_glyphs<'a>( } let min_enclosing_width = max_word_caret; - let min_enclosing_height = (current_line_num as f32 * vertical_advance) + offset_top; + let min_enclosing_height = (current_line_num as f32 * vertical_advance) + (font_size_no_line_height.y / PX_TO_PT) + (offset_top / PX_TO_PT); let line_break_offsets = line_break_offsets.into_iter().map(|(line, space_r)| { let space_r = match space_r { @@ -898,13 +897,14 @@ pub struct LayoutTextResult { pub min_width: f32, /// Minimal height of the layouted text pub min_height: f32, + pub font_metrics: FontMetrics, } /// Layout a string of text horizontally, given a font with its metrics. pub fn layout_text<'a>( text: &str, font: &Font<'a>, - font_metrics: &FontMetrics) + font_metrics: FontMetrics) -> LayoutTextResult { // NOTE: This function is different from the get_glyphs function that is @@ -914,10 +914,10 @@ pub fn layout_text<'a>( // This function does not calculate any overflow. let words = split_text_into_words(text, font, font_metrics.font_size_no_line_height); let (layouted_glyphs, line_breaks, min_width, min_height) = - words_to_left_aligned_glyphs(&words, font, None, font_metrics); + words_to_left_aligned_glyphs(&words, font, None, &font_metrics); LayoutTextResult { - words, layouted_glyphs, line_breaks, min_width, min_height + words, layouted_glyphs, line_breaks, min_width, min_height, font_metrics, } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 670ec94e0..0e1c1428f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -9,7 +9,7 @@ pub use self::svg::{ SvgCache, VectorizedFont, VectorizedFontCache, VerticesIndicesBuffer, SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, - SvgText, SvgTextPlacement, SvgTextLayout, + SvgText, SvgTextPlacement, SvgTextLayout, SvgBbox, BezierNormalVector, BezierCharacterRotation, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index b5a5b8eb2..7242d622d 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -37,7 +37,7 @@ use { window::ReadOnlyWindow, css_parser::{FontId, FontSize}, resources::AppResources, - text_layout::{FontMetrics, LayoutTextResult, layout_text}, + text_layout::{FontMetrics, LayoutTextResult, layout_text, PX_TO_PT}, }; pub use lyon::tessellation::VertexBuffers; @@ -564,6 +564,24 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio } } +pub fn quick_rects(rects: &[SvgRect], stroke_color: ColorU, stroke_options: Option) +-> SvgLayerResource +{ + let stroke_options = stroke_options.unwrap_or_default(); + let style = SvgStyle::stroked(stroke_color, stroke_options); + + let rects = rects.iter().map(|r| SvgLayerType::Rect(*r)).collect(); + let (_, stroke) = tesselate_layer_data(&LayerType::from_polygons(rects), 0.01, Some(stroke_options)); + + let stroke = stroke.unwrap(); + + SvgLayerResource::Direct { + style: style, + fill: None, + stroke: Some(VerticesIndicesBuffer { vertices: stroke.0, indices: stroke.1 }), + } +} + const BEZIER_SAMPLE_RATE: usize = 20; type ArcLength = f32; @@ -1117,9 +1135,8 @@ impl VectorizedFont { let stroke_options = SvgStrokeOptions::default(); // TODO: In a regular font (4000 characters), this is pretty slow! - // font.glyph_count() as u32 - // Pre-load the first 128 characters - for g in (0..128).filter_map(|i| { + // Pre-load the "A..Z | a..z" characters + for g in (65..122).filter_map(|i| { let g = font.glyph(GlyphId(i)); if g.id() == GlyphId(0) { None @@ -1148,8 +1165,8 @@ impl VectorizedFont { } Self { - glyph_polygon_map: Rc::new(RefCell::new(glyph_polygon_map)), - glyph_stroke_map: Rc::new(RefCell::new(glyph_stroke_map)), + glyph_polygon_map: Rc::new(RefCell::new(FastHashMap::default())), + glyph_stroke_map: Rc::new(RefCell::new(FastHashMap::default())), } } } @@ -1202,7 +1219,7 @@ fn glyph_to_svg_layer_type<'a>(glyph: Glyph<'a>) -> Option { .collect())) } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct VectorizedFontCache { /// Font -> Vectorized glyph map vectorized_fonts: FastHashMap, @@ -1210,8 +1227,16 @@ pub struct VectorizedFontCache { impl VectorizedFontCache { - pub fn new() -> Self { - Self::default() + pub fn new(app_resources: &AppResources) -> Self { + let mut fonts = FastHashMap::default(); + let loaded_font_keys = app_resources.get_loaded_fonts(); + println!("loaded fonts: {:?}", loaded_font_keys); + for font_id in loaded_font_keys { + fonts.entry(font_id.clone()).or_insert_with(|| VectorizedFont::from_font(app_resources.get_font(&font_id).unwrap().0)); + } + Self { + vectorized_fonts: fonts, + } } pub fn insert_if_not_exist(&mut self, id: FontId, font: &Font) { @@ -1232,7 +1257,6 @@ impl VectorizedFontCache { } impl SvgLayerType { - pub fn tesselate(&self, tolerance: f32, stroke: Option) -> (VertexBuffers, Option>) { @@ -1347,6 +1371,14 @@ pub struct SvgCircle { pub radius: f32, } +impl SvgCircle { + pub fn contains_point(&self, x: f32, y: f32) -> bool { + let x_diff = (x - self.center_x).abs(); + let y_diff = (y - self.center_y).abs(); + (x_diff * x_diff) + (y_diff * y_diff) < (self.radius * self.radius) + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub struct SvgRect { pub width: f32, @@ -1357,6 +1389,17 @@ pub struct SvgRect { pub ry: f32, } +impl SvgRect { + /// Note: does not incorporate rounded edges! + /// Origin of x and y is assumed to be the top left corner + pub fn contains_point(&self, x: f32, y: f32) -> bool { + x > self.x && + x < self.x + self.width && + y > self.y && + y < self.y + self.height + } +} + mod svg_to_lyon { use lyon::{ @@ -1692,10 +1735,90 @@ pub struct SvgText<'a> { #[derive(Debug, Clone)] pub struct SvgTextLayout(LayoutTextResult); +/// An axis-aligned bounding box (not rotated / skewed) +#[derive(Debug, Copy, Clone)] +pub struct SvgBbox(TypedRect); + +impl SvgBbox { + /// Simple function for drawing a single bounding box (in black). + pub fn draw_lines(&self) -> SvgLayerResource { + quick_rects(&[SvgRect { + width: self.0.size.width, + height: self.0.size.height, + x: self.0.origin.x, + y: self.0.origin.y, + rx: 0.0, + ry: 0.0, + }], ColorU { r: 0, b: 0, g: 0, a: 255 }, None) + } + + /// Checks if the bounding box contains a point + pub fn contains_point(&self, x: f32, y: f32) -> bool { + self.0.contains(&TypedPoint2D::new(x, y)) + } +} + +#[inline] +fn is_point_in_shape(point: (f32, f32), shape: &[(f32, f32)]) -> bool { + if shape.len() < 3 { + // Shape must at least have 3 points, i.e. be a triangle + return false; + } + + // We iterate over the shape in 2 points. + // + // If the mouse cursor (target point) is on the left side for all points, + // then cursor is inside of the shape. If it appears on the right side for + // only one point, we know that it isn't inside the target shape. + // all() is lazy and will quit on the first result where the target is not + // inside the shape. + shape.iter().zip(shape.iter().skip(1)).all(|(start, end)| { + !(side_of_point(point, *start, *end).is_sign_positive()) + }) +} + +/// Determine which side of a vector the point is on. +/// +/// Depending on if the result of this function is positive or negative, +/// the target point lies either right or left to the imaginary line from (start -> end) +#[inline] +fn side_of_point(target: (f32, f32), start: (f32, f32), end: (f32, f32)) -> f32 { + ((target.0 - start.0) * (end.1 - start.1)) - + ((target.1 - start.1) * (end.0 - start.0)) +} + impl SvgTextLayout { + /// Calculate the text layout from a font and a font size. + /// + /// Warning: may be slow on large texts. pub fn from_str(text: &str, font: &Font, font_size: &FontSize) -> Self { let font_metrics = FontMetrics::new(font, font_size, None); - SvgTextLayout(layout_text(text, font, &font_metrics)) + SvgTextLayout(layout_text(text, font, font_metrics)) + } + + /// Get the bounding box of a layouted text + pub fn get_bbox(&self, placement: &SvgTextPlacement) -> SvgBbox { + use self::SvgTextPlacement::*; + SvgBbox(match placement { + Unmodified => { + TypedRect::new( + TypedPoint2D::new(0.0, -self.0.font_metrics.vertical_advance / PX_TO_PT), + TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) + ) + }, + Rotated(_r) => { + TypedRect::new( + TypedPoint2D::new(0.0, 0.0), + TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) + ) + }, + OnCubicBezierCurve(_curve) => { + TypedRect::new( + TypedPoint2D::new(0.0, 0.0), + TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) + ) + } + }) } } @@ -1718,6 +1841,10 @@ impl<'a> SvgText<'a> { } } } + + pub fn get_bbox(&self) -> SvgBbox { + self.text_layout.get_bbox(&self.placement) + } } fn normal_text( From 695c858888f70d1aa0e20e87c1a2f26f2a334627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 7 Aug 2018 10:15:30 +0200 Subject: [PATCH 187/868] Implemented preliminary hit testing for SVG fonts --- examples/debug.rs | 137 ++++++++++++++++++++++++++++----------------- src/widgets/svg.rs | 12 ++-- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 9aba8892e..b1639022d 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -7,9 +7,18 @@ use azul::widgets::*; use azul::dialogs::*; use std::fs; +use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); -const FONT_BYTES: &[u8] = include_bytes!("../assets/fonts/weblysleekuil.ttf"); +static TEXT_ID: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct TextId(usize); + +fn new_text_id() -> TextId { + TextId(TEXT_ID.fetch_add(1, Ordering::SeqCst)) +} #[derive(Debug)] pub struct MyAppData { @@ -21,6 +30,8 @@ pub struct Map { pub cache: SvgCache, pub layers: Vec, pub font_cache: VectorizedFontCache, + pub texts: HashMap, + pub hovered_text: Option, pub zoom: f64, pub pan_horz: f64, pub pan_vert: f64, @@ -31,7 +42,7 @@ impl Layout for MyAppData { -> Dom { if let Some(map) = &self.map { - Svg::with_layers(build_layers(&map.layers, &map.font_cache, &info.resources)) + Svg::with_layers(build_layers(&map.layers, &map.texts, &map.hovered_text, &map.font_cache, &info.resources)) .with_pan(map.pan_horz as f32, map.pan_vert as f32) .with_zoom(map.zoom as f32) .dom(&info.window, &map.cache) @@ -46,57 +57,48 @@ impl Layout for MyAppData { } } -fn build_layers(existing_layers: &[SvgLayerId], vector_font_cache: &VectorizedFontCache, resources: &AppResources) +fn build_layers( + existing_layers: &[SvgLayerId], + texts: &HashMap, + hovered_text: &Option, + vector_font_cache: &VectorizedFontCache, + resources: &AppResources) -> Vec { let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); - let font_id = FONT_ID; - let curve = SampledBezierCurve::from_curve(&[ - BezierControlPoint { x: 0.0, y: 0.0 }, - BezierControlPoint { x: 40.0, y: 120.0 }, - BezierControlPoint { x: 80.0, y: 120.0 }, - BezierControlPoint { x: 120.0, y: 0.0 }, - ]); - let font_size = FontSize::px(10.0); - let font = resources.get_font(&font_id).unwrap().0; + layers.extend(texts.values().map(|text| text.to_svg_layer(vector_font_cache, resources))); - let texts = [ - SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &SvgTextLayout::from_str("On Curve!!!!", &font, &font_size), - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::OnCubicBezierCurve(curve), - }, - SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &SvgTextLayout::from_str("Rotated", &font, &font_size), - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::Rotated(-30.0), - }, - SvgText { - font_size: font_size, - font_id: &font_id, - text_layout: &SvgTextLayout::from_str("Unmodified\nCool", &font, &font_size), - style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), - placement: SvgTextPlacement::Unmodified, - }, - ]; - - layers.extend(texts.iter().map(|t| t.to_svg_layer(vector_font_cache, resources))); - layers.extend(texts.iter().map(|t| t.get_bbox().draw_lines())); + if let Some(active) = hovered_text { + layers.push(texts[active].get_bbox().draw_lines()); + } - layers.push(curve.draw_lines()); - layers.push(curve.draw_control_handles()); + // layers.push(curve.draw_lines()); + // layers.push(curve.draw_control_handles()); layers } +// Check what text was hovered over fn check_hovered_font(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + let (cursor_x, cursor_y) = event.cursor_relative_to_item; + + let mut should_redraw = UpdateScreen::DontRedraw; - UpdateScreen::DontRedraw + app_state.data.modify(|data| { + if let Some(map) = data.map.as_mut() { + for (k, v) in map.texts.iter() { + if v.get_bbox().contains_point(cursor_x, cursor_y) { + map.hovered_text = Some(*k); + should_redraw = UpdateScreen::Redraw; + break; + + } + } + } + }); + + should_redraw } fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { @@ -124,6 +126,47 @@ fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) } fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { + + let font_id = FONT_ID; + let font_size = FontSize::px(10.0); + let font = app_state.resources.get_font(&font_id).unwrap().0; + + // Texts only for testing + let texts = [ + SvgText { + font_size: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str("On Curve!!!!", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::OnCubicBezierCurve(SampledBezierCurve::from_curve(&[ + BezierControlPoint { x: 0.0, y: 0.0 }, + BezierControlPoint { x: 40.0, y: 120.0 }, + BezierControlPoint { x: 80.0, y: 120.0 }, + BezierControlPoint { x: 120.0, y: 0.0 }, + ])), + }, + SvgText { + font_size: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str("Rotated", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Rotated(-30.0), + }, + SvgText { + font_size: font_size, + font_id: font_id.clone(), + text_layout: SvgTextLayout::from_str("Unmodified\nCool", &font, &font_size), + style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), + placement: SvgTextPlacement::Unmodified, + }, + ]; + + let mut cached_texts = HashMap::::new(); + for t in texts.into_iter() { + let id = new_text_id(); + cached_texts.insert(id, t.clone()); + } + open_file_dialog(None, None) .and_then(|path| fs::read_to_string(path.clone()).ok()) .and_then(|contents| { @@ -131,16 +174,11 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv let mut svg_cache = SvgCache::empty(); let svg_layers = svg_cache.add_svg(&contents).ok()?; - let font_id = FontId::ExternalFont(String::from("Webly Sleeky UI")); - - // Pre-vectorize the glyphs of the font into vertex buffers - let (font, _) = app_state.get_font(&font_id)?; - let mut vectorized_font_cache = VectorizedFontCache::new(&app_state.resources); - vectorized_font_cache.insert_if_not_exist(font_id, font); - app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, - font_cache: vectorized_font_cache, + font_cache: VectorizedFontCache::new(&app_state.resources), + hovered_text: None, + texts: cached_texts, layers: svg_layers, zoom: 1.0, pan_horz: 0.0, @@ -156,7 +194,6 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv fn main() { let mut app = App::new(MyAppData { map: None }, AppConfig::default()); - app.add_font("Webly Sleeky UI", &mut FONT_BYTES.clone()).unwrap(); app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 7242d622d..150517861 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1229,9 +1229,7 @@ impl VectorizedFontCache { pub fn new(app_resources: &AppResources) -> Self { let mut fonts = FastHashMap::default(); - let loaded_font_keys = app_resources.get_loaded_fonts(); - println!("loaded fonts: {:?}", loaded_font_keys); - for font_id in loaded_font_keys { + for font_id in app_resources.get_loaded_fonts() { fonts.entry(font_id.clone()).or_insert_with(|| VectorizedFont::from_font(app_resources.get_font(&font_id).unwrap().0)); } Self { @@ -1724,10 +1722,10 @@ pub enum SvgTextPlacement { } #[derive(Debug, Clone)] -pub struct SvgText<'a> { +pub struct SvgText { pub font_size: FontSize, - pub font_id: &'a FontId, - pub text_layout: &'a SvgTextLayout, + pub font_id: FontId, + pub text_layout: SvgTextLayout, pub style: SvgStyle, pub placement: SvgTextPlacement, } @@ -1822,7 +1820,7 @@ impl SvgTextLayout { } } -impl<'a> SvgText<'a> { +impl SvgText { pub fn to_svg_layer(&self, vectorized_fonts_cache: &VectorizedFontCache, resources: &AppResources) -> SvgLayerResource { From c137865ba528057945d3d040f7bda237214f0b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 10 Aug 2018 08:37:43 +0200 Subject: [PATCH 188/868] Fixed task system, added async IO example --- examples/async.rs | 58 +++++++++++++++++++++++++++++++++++ src/app.rs | 31 +++++++++++++------ src/app_state.rs | 16 ++++++++-- src/styles/native_linux.css | 6 ++++ src/styles/native_macos.css | 6 ++++ src/styles/native_windows.css | 6 ++++ src/task.rs | 3 +- src/widgets/label.rs | 4 ++- 8 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 examples/async.rs diff --git a/examples/async.rs b/examples/async.rs new file mode 100644 index 000000000..33f518448 --- /dev/null +++ b/examples/async.rs @@ -0,0 +1,58 @@ +extern crate azul; + +use azul::{prelude::*, widgets::*}; +use std::{thread, time::Duration, sync::{Arc, Mutex}}; +use self::ConnectionStatus::*; + +#[derive(Debug, PartialEq)] +enum ConnectionStatus { + NotConnected, + Connected, + Error(String), + InProgress, +} + +struct MyDataModel { + connection_status: ConnectionStatus, +} + +impl Layout for MyDataModel { + fn layout(&self, info: WindowInfo) -> Dom { + + let status = match &self.connection_status { + NotConnected => format!("Not connected!"), + Connected => format!("You are connected!"), + InProgress => format!("Loading..."), + Error(e) => format!("There was an error: {}", e), + }; + + let status_p = Label::new(status).dom(); + + let mut dom = Dom::new(NodeType::Div).with_child(status_p); + + if self.connection_status == NotConnected { + dom.add_child(Button::with_label("Connect to database...").dom() + .with_callback(On::MouseUp, Callback(start_connection))); + } + + dom + } +} + +fn start_connection(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { + app_state.data.modify(|state| state.connection_status = ConnectionStatus::InProgress); + app_state.add_task(connect_to_db_async); + UpdateScreen::Redraw +} + +fn connect_to_db_async(app_data: Arc>, _: Arc<()>) { + thread::sleep(Duration::from_secs(4)); // simulate slow load + app_data.modify(|state| state.connection_status = ConnectionStatus::Connected); +} + +fn main() { + let model = MyDataModel { connection_status: ConnectionStatus::NotConnected }; + let mut app = App::new(model, AppConfig::default()); + app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); + app.run().unwrap(); +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 98f394a0a..f09d23de8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -189,6 +189,7 @@ impl<'a, T: Layout> App<'a, T> { let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; let mut force_redraw_cache = vec![1_usize; self.windows.len()]; + let mut awakened_task = false; while !self.windows.is_empty() { @@ -204,7 +205,7 @@ impl<'a, T: Layout> App<'a, T> { window.events_loop.poll_events(|e| events.push(e)); for event in &events { - if preprocess_event(event, &mut frame_event_info) == WindowCloseEvent::AboutToClose { + if preprocess_event(event, &mut frame_event_info, awakened_task) == WindowCloseEvent::AboutToClose { closed_windows.push(idx); continue 'window_loop; } @@ -266,6 +267,7 @@ impl<'a, T: Layout> App<'a, T> { Self::update_display(&window); // render the window (webrender will send an Awakened event when the frame is done) render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); + awakened_task = false; } } @@ -278,17 +280,23 @@ impl<'a, T: Layout> App<'a, T> { }); // Run deamons and remove them from the even queue if they are finished - self.app_state.run_all_deamons(); + let should_redraw_deamons = self.app_state.run_all_deamons(); // Clean up finished tasks, remove them if possible - self.app_state.clean_up_finished_tasks(); - - // Wait until 16ms have passed - let diff = time_start.elapsed(); - const FRAME_TIME: Duration = Duration::from_millis(16); - if diff < FRAME_TIME { - thread::sleep(FRAME_TIME - diff); + let should_redraw_tasks = self.app_state.clean_up_finished_tasks(); + + if [should_redraw_deamons, should_redraw_tasks].into_iter().any(|e| *e == UpdateScreen::Redraw) { + self.windows.iter().for_each(|w| w.events_loop.create_proxy().wakeup().unwrap_or(())); + awakened_task = true; + } else { + // Wait until 16ms have passed + let diff = time_start.elapsed(); + const FRAME_TIME: Duration = Duration::from_millis(16); + if diff < FRAME_TIME { + thread::sleep(FRAME_TIME - diff); + } } + } Ok(()) @@ -513,7 +521,7 @@ enum WindowCloseEvent { NoCloseEvent, } -fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> WindowCloseEvent { +fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo, awakened_task: bool) -> WindowCloseEvent { use glium::glutin::WindowEvent; match event { @@ -549,6 +557,9 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo) -> Win }, Event::Awakened => { frame_event_info.should_swap_window = true; + if awakened_task { + frame_event_info.should_redraw_window = true; + } }, _ => { }, } diff --git a/src/app_state.rs b/src/app_state.rs index 71063852a..f724e3dab 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -200,7 +200,10 @@ impl<'a, T: Layout> AppState<'a, T> { } /// Run all currently registered deamons - pub(crate) fn run_all_deamons(&self) -> UpdateScreen { + #[must_use] + pub(crate) fn run_all_deamons(&self) + -> UpdateScreen + { let mut should_update_screen = UpdateScreen::DontRedraw; let mut lock = self.data.lock().unwrap(); for deamon in self.deamons.values().cloned() { @@ -214,9 +217,18 @@ impl<'a, T: Layout> AppState<'a, T> { } /// Remove all tasks that have finished executing + #[must_use] pub(crate) fn clean_up_finished_tasks(&mut self) + -> UpdateScreen { - self.tasks.retain(|x| x.is_finished()); + let old_count = self.tasks.len(); + self.tasks.retain(|x| !x.is_finished()); + let new_count = self.tasks.len(); + if old_count != new_count { + UpdateScreen::Redraw + } else { + UpdateScreen::DontRedraw + } } pub fn add_text_uncached>(&mut self, text: S) diff --git a/src/styles/native_linux.css b/src/styles/native_linux.css index 42a3b7877..2c969feeb 100644 --- a/src/styles/native_linux.css +++ b/src/styles/native_linux.css @@ -11,6 +11,12 @@ justify-content: center; } +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + * { font-size: 10px; font-family: sans-serif; diff --git a/src/styles/native_macos.css b/src/styles/native_macos.css index 72140ee2f..5ee46de66 100644 --- a/src/styles/native_macos.css +++ b/src/styles/native_macos.css @@ -11,6 +11,12 @@ justify-content: center; } +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + * { font-size: 14px; font-family: sans-serif; diff --git a/src/styles/native_windows.css b/src/styles/native_windows.css index b59b8f29f..d7814671f 100644 --- a/src/styles/native_windows.css +++ b/src/styles/native_windows.css @@ -8,6 +8,12 @@ justify-content: center; } +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + * { font-size: 14.66px; font-family: sans-serif; diff --git a/src/task.rs b/src/task.rs index 8a09f624f..0c9adcb0c 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,4 +1,4 @@ -//! Preliminary async IO / Task system +//! Simplistic async IO / Task system use std::{ sync::{Arc, Mutex, Weak}, @@ -35,6 +35,7 @@ impl Task { } } + /// Returns true if the task has been finished, false otherwise pub fn is_finished(&self) -> bool { self.dropcheck.upgrade().is_none() } diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 158f81e79..dd4014db3 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -18,7 +18,9 @@ impl Label { pub fn dom(self) -> Dom where T: Layout { - Dom::new(NodeType::Label(self.text)) + Dom::new(NodeType::Div) + .with_child(Dom::new(NodeType::Label(self.text))) + .with_class("__azul-native-label") } } From 64e863500747e753e6a1e4c2013dc414b309b2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 11 Aug 2018 20:24:18 +0200 Subject: [PATCH 189/868] Preliminary hit testing for curved and rotated texts --- examples/debug.rs | 5 +-- src/widgets/svg.rs | 84 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index b1639022d..cebc5dcd2 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -68,11 +68,12 @@ fn build_layers( let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); layers.extend(texts.values().map(|text| text.to_svg_layer(vector_font_cache, resources))); - + layers.extend(texts.values().map(|text| text.get_bbox().draw_lines())); +/* if let Some(active) = hovered_text { layers.push(texts[active].get_bbox().draw_lines()); } - +*/ // layers.push(curve.draw_lines()); // layers.push(curve.draw_control_handles()); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 150517861..3a8d2e048 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -737,6 +737,43 @@ impl SampledBezierCurve { (glyph_offsets, glyph_rotations) } + pub fn get_bbox(&self) -> (SvgBbox, [(usize, usize);2]) { + + let mut lowest_x = self.sampled_bezier_points[0].x; + let mut highest_x = self.sampled_bezier_points[0].x; + let mut lowest_y = self.sampled_bezier_points[0].y; + let mut highest_y = self.sampled_bezier_points[0].y; + + let mut lowest_x_idx = 0; + let mut highest_x_idx = 0; + let mut lowest_y_idx = 0; + let mut highest_y_idx = 0; + + for (idx, BezierControlPoint { x, y }) in self.sampled_bezier_points.iter().enumerate().skip(1) { + if *x < lowest_x { + lowest_x = *x; + lowest_x_idx = idx; + } + if *x > highest_x { + highest_x = *x; + highest_x_idx = idx; + } + if *y < lowest_y { + lowest_y = *y; + lowest_y_idx = idx; + } + if *y > highest_y { + highest_y = *y; + highest_y_idx = idx; + } + } + + ( + SvgBbox(TypedRect::new(TypedPoint2D::new(lowest_x, lowest_y), TypedSize2D::new(highest_x - lowest_x, highest_y - lowest_y))), + [(lowest_x_idx, lowest_y_idx), (highest_x_idx, highest_y_idx)] + ) + } + /// Returns the geometry necessary for drawing the points from `self.sampled_bezier_points`. /// Usually only good for debugging pub fn draw_circles(&self) -> SvgLayerResource { @@ -1797,20 +1834,55 @@ impl SvgTextLayout { /// Get the bounding box of a layouted text pub fn get_bbox(&self, placement: &SvgTextPlacement) -> SvgBbox { use self::SvgTextPlacement::*; + + let normal_x = 0.0; + let normal_y = -self.0.font_metrics.vertical_advance / PX_TO_PT; + let normal_width = self.0.min_width * 2.0; + let normal_height = self.0.min_height; + SvgBbox(match placement { Unmodified => { TypedRect::new( - TypedPoint2D::new(0.0, -self.0.font_metrics.vertical_advance / PX_TO_PT), - TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) + TypedPoint2D::new(normal_x, normal_y), + TypedSize2D::new(normal_width, normal_height) ) }, - Rotated(_r) => { + Rotated(r) => { + + fn rotate_point((x, y): (f32, f32), sin: f32, cos: f32) -> (f32, f32) { + ((x * cos) - (y * sin), (x * sin) + (y * cos)) + } + + let rot_radians = r.to_radians(); + let sin = rot_radians.sin(); + let cos = rot_radians.cos(); + + let top_left = (normal_x, normal_y); + let top_right = (normal_x + normal_width, normal_y); + let bottom_right = (normal_x + normal_width, normal_y + normal_height); + let bottom_left = (normal_x, normal_y + normal_height); + + let (top_left_x, top_left_y) = rotate_point(top_left, sin, cos); + let (top_right_x, top_right_y) = rotate_point(top_right, sin, cos); + let (bottom_right_x, bottom_right_y) = rotate_point(bottom_right, sin, cos); + let (bottom_left_x, bottom_left_y) = rotate_point(bottom_left, sin, cos); + + let min_x = top_left_x.min(top_right_x).min(bottom_right_x).min(bottom_left_x); + let max_x = top_left_x.max(top_right_x).max(bottom_right_x).max(bottom_left_x); + let min_y = top_left_y.min(top_right_y).min(bottom_right_y).min(bottom_left_y); + let max_y = top_left_y.max(top_right_y).max(bottom_right_y).max(bottom_left_y); + TypedRect::new( - TypedPoint2D::new(0.0, 0.0), - TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) + TypedPoint2D::new(min_x, min_y), + TypedSize2D::new(max_x - min_x, max_y - min_y) ) }, - OnCubicBezierCurve(_curve) => { + OnCubicBezierCurve(curve) => { + let (bbox, bbox_indices) = curve.get_bbox(); + println!("bbox: {:?}, indices: {:?}", bbox, bbox_indices); + + // TODO: tomorrow! <---------------------------------------------------------------------------- + TypedRect::new( TypedPoint2D::new(0.0, 0.0), TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) From 122fd94467b5ac91024cdfaaab412a63498523fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 10 Aug 2018 14:15:05 +0200 Subject: [PATCH 190/868] Reworked deamon system, updated async example --- examples/async.rs | 68 +++++++++++++++++++++++++++++++++++------------ src/app.rs | 24 +++++++---------- src/app_state.rs | 34 ++++++++++++++---------- src/lib.rs | 3 ++- src/task.rs | 7 +++++ 5 files changed, 89 insertions(+), 47 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index 33f518448..909953030 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -1,15 +1,21 @@ extern crate azul; -use azul::{prelude::*, widgets::*}; -use std::{thread, time::Duration, sync::{Arc, Mutex}}; -use self::ConnectionStatus::*; +use azul::{ + prelude::*, + widgets::*, +}; +use std::{ + thread, + time::{Duration, Instant}, + sync::{Arc, Mutex}, +}; #[derive(Debug, PartialEq)] enum ConnectionStatus { NotConnected, Connected, Error(String), - InProgress, + InProgress(Instant, Duration), } struct MyDataModel { @@ -17,36 +23,64 @@ struct MyDataModel { } impl Layout for MyDataModel { - fn layout(&self, info: WindowInfo) -> Dom { + fn layout(&self, _info: WindowInfo) -> Dom { + + use self::ConnectionStatus::*; let status = match &self.connection_status { - NotConnected => format!("Not connected!"), - Connected => format!("You are connected!"), - InProgress => format!("Loading..."), - Error(e) => format!("There was an error: {}", e), + ConnectionStatus::NotConnected => format!("Not connected!"), + ConnectionStatus::Connected => format!("You are connected!"), + ConnectionStatus::InProgress(_, d) => format!("Loading... {}{:03}s", d.as_secs(), d.subsec_millis()), + ConnectionStatus::Error(e) => format!("There was an error: {}", e), }; - let status_p = Label::new(status).dom(); + let mut dom = Dom::new(NodeType::Div) + .with_child(Label::new(status.clone()).dom()); - let mut dom = Dom::new(NodeType::Div).with_child(status_p); + match &self.connection_status { + NotConnected => { + let button = Button::with_label("Connect to database...").dom() + .with_callback(On::MouseUp, Callback(start_connection)); - if self.connection_status == NotConnected { - dom.add_child(Button::with_label("Connect to database...").dom() - .with_callback(On::MouseUp, Callback(start_connection))); + dom.add_child(button); + }, + Error(_) | Connected => { + let button = Button::with_label(format!("{}\nRetry?", status)).dom() + .with_callback(On::MouseUp, Callback(reset_connection)); + dom.add_child(button); + } + InProgress(_, _) => { }, } dom } } -fn start_connection(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { - app_state.data.modify(|state| state.connection_status = ConnectionStatus::InProgress); +fn reset_connection(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { + app_state.data.modify(|state| state.connection_status = ConnectionStatus::NotConnected); + UpdateScreen::Redraw +} + +fn start_connection(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { + let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); + app_state.data.modify(|state| state.connection_status = status); app_state.add_task(connect_to_db_async); + app_state.add_deamon(timer_deamon); UpdateScreen::Redraw } +fn timer_deamon(state: &mut MyDataModel) -> (UpdateScreen, TerminateDeamon) { + println!("deamon running!"); + if let ConnectionStatus::InProgress(start, duration) = &mut state.connection_status { + *duration = Instant::now() - *start; + (UpdateScreen::Redraw, TerminateDeamon::Continue) + } else { + (UpdateScreen::DontRedraw, TerminateDeamon::Terminate) + } +} + fn connect_to_db_async(app_data: Arc>, _: Arc<()>) { - thread::sleep(Duration::from_secs(4)); // simulate slow load + thread::sleep(Duration::from_secs(10)); // simulate slow load app_data.modify(|state| state.connection_status = ConnectionStatus::Connected); } diff --git a/src/app.rs b/src/app.rs index f09d23de8..b89fe3930 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,7 @@ use { traits::Layout, ui_state::UiState, ui_description::UiDescription, + task::TerminateDeamon, }; /// Graphical application that maintains some kind of application state @@ -183,13 +184,14 @@ impl<'a, T: Layout> App<'a, T> { } fn run_inner(&mut self) -> Result<(), RuntimeError> { + use std::{thread, time::{Duration, Instant}}; use window::ReadOnlyWindow; let mut ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); let mut ui_description_cache = vec![UiDescription::default(); self.windows.len()]; let mut force_redraw_cache = vec![1_usize; self.windows.len()]; - let mut awakened_task = false; + let mut awakened_task = vec![false; self.windows.len()]; while !self.windows.is_empty() { @@ -205,7 +207,7 @@ impl<'a, T: Layout> App<'a, T> { window.events_loop.poll_events(|e| events.push(e)); for event in &events { - if preprocess_event(event, &mut frame_event_info, awakened_task) == WindowCloseEvent::AboutToClose { + if preprocess_event(event, &mut frame_event_info, awakened_task[idx]) == WindowCloseEvent::AboutToClose { closed_windows.push(idx); continue 'window_loop; } @@ -267,7 +269,7 @@ impl<'a, T: Layout> App<'a, T> { Self::update_display(&window); // render the window (webrender will send an Awakened event when the frame is done) render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); - awakened_task = false; + awakened_task[idx] = false; } } @@ -287,7 +289,7 @@ impl<'a, T: Layout> App<'a, T> { if [should_redraw_deamons, should_redraw_tasks].into_iter().any(|e| *e == UpdateScreen::Redraw) { self.windows.iter().for_each(|w| w.events_loop.create_proxy().wakeup().unwrap_or(())); - awakened_task = true; + awakened_task = vec![true; self.windows.len()]; } else { // Wait until 16ms have passed let diff = time_start.elapsed(); @@ -426,21 +428,13 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.delete_font(id) } - /// Create a deamon. Does nothing if a deamon with the same ID already exists. + /// Create a deamon. Does nothing if a deamon with the function pointer location already exists. /// /// If the deamon was inserted, returns true, otherwise false - pub fn add_deamon>(&mut self, id: S, deamon: fn(&mut T) -> UpdateScreen) - -> bool - { - self.app_state.add_deamon(id, deamon) - } - - /// Remove a currently running deamon from running. Does nothing if there is - /// already a deamon with the same ID - pub fn delete_deamon>(&mut self, id: S) + pub fn add_deamon(&mut self, deamon: fn(&mut T) -> (UpdateScreen, TerminateDeamon)) -> bool { - self.app_state.delete_deamon(id) + self.app_state.add_deamon(deamon) } pub fn add_text_uncached>(&mut self, text: S) diff --git a/src/app_state.rs b/src/app_state.rs index f724e3dab..3814a8b5c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -17,6 +17,7 @@ use { font::FontError, css_parser::{FontId, FontSize, PixelValue}, errors::ClipboardError, + task::TerminateDeamon, }; /// Wrapper for your application data. In order to be layout-able, @@ -37,7 +38,7 @@ pub struct AppState<'a, T: Layout> { /// Fonts and images that are currently loaded into the app pub resources: AppResources<'a>, /// Currently running deamons (polling functions) - pub(crate) deamons: FastHashMap UpdateScreen>, + pub(crate) deamons: FastHashMap (UpdateScreen, TerminateDeamon)>, /// Currently running tasks (asynchronous functions running on a different thread) pub(crate) tasks: Vec, } @@ -182,37 +183,42 @@ impl<'a, T: Layout> AppState<'a, T> { self.resources.delete_font(id) } - /// Create a deamon. Does nothing if a deamon with the same ID already exists. + /// Create a deamon. Does nothing if a deamon already exists. /// /// If the deamon was inserted, returns true, otherwise false - pub fn add_deamon>(&mut self, id: S, deamon: fn(&mut T) -> UpdateScreen) -> bool { - let id_string = id.into(); - match self.deamons.entry(id_string) { + pub fn add_deamon(&mut self, deamon: fn(&mut T) -> (UpdateScreen, TerminateDeamon)) -> bool { + match self.deamons.entry(deamon as usize) { Occupied(_) => false, Vacant(v) => { v.insert(deamon); true }, } } - /// Remove a currently running deamon from running. Does nothing if there is - /// already a deamon with the same ID - pub fn delete_deamon>(&mut self, id: S) -> bool { - self.deamons.remove(id.as_ref()).is_some() - } - /// Run all currently registered deamons #[must_use] - pub(crate) fn run_all_deamons(&self) + pub(crate) fn run_all_deamons(&mut self) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; let mut lock = self.data.lock().unwrap(); - for deamon in self.deamons.values().cloned() { - let should_update = (deamon)(&mut lock); + let mut deamons_to_terminate = vec![]; + + for (key, deamon) in self.deamons.iter() { + let (should_update, should_terminate) = (deamon.clone())(&mut lock); + if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { should_update_screen = UpdateScreen::Redraw; } + + if should_terminate == TerminateDeamon::Terminate { + deamons_to_terminate.push(key.clone()); + } + } + + for key in deamons_to_terminate { + self.deamons.remove(&key); } + should_update_screen } diff --git a/src/lib.rs b/src/lib.rs index 4ea91bd90..7b31d07fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,8 @@ pub mod prelude { pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; pub use rusttype::Font; pub use resources::AppResources; - + pub use task::TerminateDeamon; + #[cfg(feature = "logging")] pub use log::LevelFilter; } diff --git a/src/task.rs b/src/task.rs index 0c9adcb0c..14022fb95 100644 --- a/src/task.rs +++ b/src/task.rs @@ -8,6 +8,13 @@ use { traits::Layout, }; +/// Should a deamon terminate or not - used to remove active deamons +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TerminateDeamon { + Terminate, + Continue, +} + pub struct Task { // Task is in progress join_handle: Option>, From 6f2e5bf4945e5b389d565127134c800ee1f518f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 12 Aug 2018 11:49:06 +0200 Subject: [PATCH 191/868] Corrected typo: deamon => daemon --- examples/async.rs | 11 +++++------ src/app.rs | 16 ++++++++-------- src/app_state.rs | 36 ++++++++++++++++++------------------ src/lib.rs | 2 +- src/styles/native_linux.css | 2 +- src/task.rs | 4 ++-- 6 files changed, 35 insertions(+), 36 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index 909953030..51d0253af 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -30,7 +30,7 @@ impl Layout for MyDataModel { let status = match &self.connection_status { ConnectionStatus::NotConnected => format!("Not connected!"), ConnectionStatus::Connected => format!("You are connected!"), - ConnectionStatus::InProgress(_, d) => format!("Loading... {}{:03}s", d.as_secs(), d.subsec_millis()), + ConnectionStatus::InProgress(_, d) => format!("Loading... {}.{:02}s", d.as_secs(), d.subsec_millis()), ConnectionStatus::Error(e) => format!("There was an error: {}", e), }; @@ -65,17 +65,16 @@ fn start_connection(app_state: &mut AppState, _event: WindowEvent) let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); app_state.data.modify(|state| state.connection_status = status); app_state.add_task(connect_to_db_async); - app_state.add_deamon(timer_deamon); + app_state.add_daemon(timer_daemon); UpdateScreen::Redraw } -fn timer_deamon(state: &mut MyDataModel) -> (UpdateScreen, TerminateDeamon) { - println!("deamon running!"); +fn timer_daemon(state: &mut MyDataModel) -> (UpdateScreen, TerminateDaemon) { if let ConnectionStatus::InProgress(start, duration) = &mut state.connection_status { *duration = Instant::now() - *start; - (UpdateScreen::Redraw, TerminateDeamon::Continue) + (UpdateScreen::Redraw, TerminateDaemon::Continue) } else { - (UpdateScreen::DontRedraw, TerminateDeamon::Terminate) + (UpdateScreen::DontRedraw, TerminateDaemon::Terminate) } } diff --git a/src/app.rs b/src/app.rs index b89fe3930..1423b16a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,7 +25,7 @@ use { traits::Layout, ui_state::UiState, ui_description::UiDescription, - task::TerminateDeamon, + task::TerminateDaemon, }; /// Graphical application that maintains some kind of application state @@ -281,13 +281,13 @@ impl<'a, T: Layout> App<'a, T> { self.windows.remove(closed_window_id); }); - // Run deamons and remove them from the even queue if they are finished - let should_redraw_deamons = self.app_state.run_all_deamons(); + // Run daemons and remove them from the even queue if they are finished + let should_redraw_daemons = self.app_state.run_all_daemons(); // Clean up finished tasks, remove them if possible let should_redraw_tasks = self.app_state.clean_up_finished_tasks(); - if [should_redraw_deamons, should_redraw_tasks].into_iter().any(|e| *e == UpdateScreen::Redraw) { + if [should_redraw_daemons, should_redraw_tasks].into_iter().any(|e| *e == UpdateScreen::Redraw) { self.windows.iter().for_each(|w| w.events_loop.create_proxy().wakeup().unwrap_or(())); awakened_task = vec![true; self.windows.len()]; } else { @@ -428,13 +428,13 @@ impl<'a, T: Layout> App<'a, T> { self.app_state.delete_font(id) } - /// Create a deamon. Does nothing if a deamon with the function pointer location already exists. + /// Create a daemon. Does nothing if a daemon with the function pointer location already exists. /// - /// If the deamon was inserted, returns true, otherwise false - pub fn add_deamon(&mut self, deamon: fn(&mut T) -> (UpdateScreen, TerminateDeamon)) + /// If the daemon was inserted, returns true, otherwise false + pub fn add_daemon(&mut self, daemon: fn(&mut T) -> (UpdateScreen, TerminateDaemon)) -> bool { - self.app_state.add_deamon(deamon) + self.app_state.add_daemon(daemon) } pub fn add_text_uncached>(&mut self, text: S) diff --git a/src/app_state.rs b/src/app_state.rs index 3814a8b5c..23990ff82 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -17,7 +17,7 @@ use { font::FontError, css_parser::{FontId, FontSize, PixelValue}, errors::ClipboardError, - task::TerminateDeamon, + task::TerminateDaemon, }; /// Wrapper for your application data. In order to be layout-able, @@ -37,8 +37,8 @@ pub struct AppState<'a, T: Layout> { pub windows: Vec, /// Fonts and images that are currently loaded into the app pub resources: AppResources<'a>, - /// Currently running deamons (polling functions) - pub(crate) deamons: FastHashMap (UpdateScreen, TerminateDeamon)>, + /// Currently running daemons (polling functions) + pub(crate) daemons: FastHashMap (UpdateScreen, TerminateDaemon)>, /// Currently running tasks (asynchronous functions running on a different thread) pub(crate) tasks: Vec, } @@ -51,7 +51,7 @@ impl<'a, T: Layout> AppState<'a, T> { data: Arc::new(Mutex::new(initial_data)), windows: Vec::new(), resources: AppResources::default(), - deamons: FastHashMap::default(), + daemons: FastHashMap::default(), tasks: Vec::new(), } } @@ -183,40 +183,40 @@ impl<'a, T: Layout> AppState<'a, T> { self.resources.delete_font(id) } - /// Create a deamon. Does nothing if a deamon already exists. + /// Create a daemon. Does nothing if a daemon already exists. /// - /// If the deamon was inserted, returns true, otherwise false - pub fn add_deamon(&mut self, deamon: fn(&mut T) -> (UpdateScreen, TerminateDeamon)) -> bool { - match self.deamons.entry(deamon as usize) { + /// If the daemon was inserted, returns true, otherwise false + pub fn add_daemon(&mut self, daemon: fn(&mut T) -> (UpdateScreen, TerminateDaemon)) -> bool { + match self.daemons.entry(daemon as usize) { Occupied(_) => false, - Vacant(v) => { v.insert(deamon); true }, + Vacant(v) => { v.insert(daemon); true }, } } - /// Run all currently registered deamons + /// Run all currently registered daemons #[must_use] - pub(crate) fn run_all_deamons(&mut self) + pub(crate) fn run_all_daemons(&mut self) -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; let mut lock = self.data.lock().unwrap(); - let mut deamons_to_terminate = vec![]; + let mut daemons_to_terminate = vec![]; - for (key, deamon) in self.deamons.iter() { - let (should_update, should_terminate) = (deamon.clone())(&mut lock); + for (key, daemon) in self.daemons.iter() { + let (should_update, should_terminate) = (daemon.clone())(&mut lock); if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { should_update_screen = UpdateScreen::Redraw; } - if should_terminate == TerminateDeamon::Terminate { - deamons_to_terminate.push(key.clone()); + if should_terminate == TerminateDaemon::Terminate { + daemons_to_terminate.push(key.clone()); } } - for key in deamons_to_terminate { - self.deamons.remove(&key); + for key in daemons_to_terminate { + self.daemons.remove(&key); } should_update_screen diff --git a/src/lib.rs b/src/lib.rs index 7b31d07fb..56a67e6cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,7 @@ pub mod prelude { pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; pub use rusttype::Font; pub use resources::AppResources; - pub use task::TerminateDeamon; + pub use task::TerminateDaemon; #[cfg(feature = "logging")] pub use log::LevelFilter; diff --git a/src/styles/native_linux.css b/src/styles/native_linux.css index 2c969feeb..0d05e2393 100644 --- a/src/styles/native_linux.css +++ b/src/styles/native_linux.css @@ -18,7 +18,7 @@ } * { - font-size: 10px; + font-size: 40px; font-family: sans-serif; color: #4c4c4c; background-color: #e7e7e7; diff --git a/src/task.rs b/src/task.rs index 14022fb95..922d33cbc 100644 --- a/src/task.rs +++ b/src/task.rs @@ -8,9 +8,9 @@ use { traits::Layout, }; -/// Should a deamon terminate or not - used to remove active deamons +/// Should a daemon terminate or not - used to remove active daemons #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TerminateDeamon { +pub enum TerminateDaemon { Terminate, Continue, } From a03048b4437b363a177933a17fb085bcc0a53b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 13 Aug 2018 07:59:26 +0200 Subject: [PATCH 192/868] Adjusted bounding boxes for curved fonts, fixed DisplayBuilder --- src/app.rs | 6 ++---- src/display_list.rs | 4 ++-- src/widgets/svg.rs | 37 +++++++++++++++++++++++-------------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1423b16a0..476ff6151 100644 --- a/src/app.rs +++ b/src/app.rs @@ -668,10 +668,8 @@ fn render( has_window_size_changed, &window.state.size); - if let Some(new_builder) = builder { - // only finalize the list if we actually need to. Otherwise just redraw the last display list - window.internal.last_display_list_builder = new_builder.finalize().2; - } + // NOTE: Display list has to be rebuilt every frame, otherwise, the epochs get out of sync + window.internal.last_display_list_builder = builder.finalize().2; let mut txn = Transaction::new(); diff --git a/src/display_list.rs b/src/display_list.rs index 8708093e5..e23247321 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -251,7 +251,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { render_api: &RenderApi, mut has_window_size_changed: bool, window_size: &WindowSize) - -> Option + -> DisplayListBuilder { let mut changeset = None; @@ -329,7 +329,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { render_api.update_resources(resource_updates); - Some(builder) + builder } } diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 3a8d2e048..c7a96cb05 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -737,19 +737,23 @@ impl SampledBezierCurve { (glyph_offsets, glyph_rotations) } + /// Returns the bounding box of the 4 points making up the curve. + /// + /// Since a bezier curve is always contained within the 4 control points, + /// the returned Bbox can be used for hit-testing. pub fn get_bbox(&self) -> (SvgBbox, [(usize, usize);2]) { - let mut lowest_x = self.sampled_bezier_points[0].x; - let mut highest_x = self.sampled_bezier_points[0].x; - let mut lowest_y = self.sampled_bezier_points[0].y; - let mut highest_y = self.sampled_bezier_points[0].y; + let mut lowest_x = self.original_curve[0].x; + let mut highest_x = self.original_curve[0].x; + let mut lowest_y = self.original_curve[0].y; + let mut highest_y = self.original_curve[0].y; let mut lowest_x_idx = 0; let mut highest_x_idx = 0; let mut lowest_y_idx = 0; let mut highest_y_idx = 0; - for (idx, BezierControlPoint { x, y }) in self.sampled_bezier_points.iter().enumerate().skip(1) { + for (idx, BezierControlPoint { x, y }) in self.original_curve.iter().enumerate().skip(1) { if *x < lowest_x { lowest_x = *x; lowest_x_idx = idx; @@ -1878,15 +1882,20 @@ impl SvgTextLayout { ) }, OnCubicBezierCurve(curve) => { - let (bbox, bbox_indices) = curve.get_bbox(); - println!("bbox: {:?}, indices: {:?}", bbox, bbox_indices); - - // TODO: tomorrow! <---------------------------------------------------------------------------- - - TypedRect::new( - TypedPoint2D::new(0.0, 0.0), - TypedSize2D::new(self.0.min_width * 2.0, self.0.min_height) - ) + let (mut bbox, _bbox_indices) = curve.get_bbox(); + + // TODO: There should be a more sophisticated Bbox calculation here + // that takes the rotation of the text into account. Right now we simply + // add the font size to the BBox height, so that we can still select text + // even when the control points are aligned in a horizontal line. + // + // This is not so much about correctness as it is about simply making + // it work for now. + + let font_size = self.0.font_metrics.font_size_no_line_height.y; + bbox.0.origin.y -= font_size; + bbox.0.size.height += font_size; + bbox.0 } }) } From 1a3cb5b38536fccddfd51a0ee05ff2188f979986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 15 Aug 2018 23:12:47 +0200 Subject: [PATCH 193/868] Fixed bug SVG lines with only 2 points are valid and should be drawn Changed "> 2" to ">= 2" --- src/widgets/svg.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index c7a96cb05..f1c6a9c2b 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -539,7 +539,7 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio let style = SvgStyle::stroked(stroke_color, stroke_options); let polygons = lines.iter() - .filter(|line| line.len() > 2) + .filter(|line| line.len() >= 2) .map(|line| { let first_point = &line[0]; @@ -738,7 +738,7 @@ impl SampledBezierCurve { } /// Returns the bounding box of the 4 points making up the curve. - /// + /// /// Since a bezier curve is always contained within the 4 control points, /// the returned Bbox can be used for hit-testing. pub fn get_bbox(&self) -> (SvgBbox, [(usize, usize);2]) { @@ -1883,13 +1883,13 @@ impl SvgTextLayout { }, OnCubicBezierCurve(curve) => { let (mut bbox, _bbox_indices) = curve.get_bbox(); - + // TODO: There should be a more sophisticated Bbox calculation here // that takes the rotation of the text into account. Right now we simply // add the font size to the BBox height, so that we can still select text // even when the control points are aligned in a horizontal line. - // - // This is not so much about correctness as it is about simply making + // + // This is not so much about correctness as it is about simply making // it work for now. let font_size = self.0.font_metrics.font_size_no_line_height.y; From 9ebe477cfb8438e319103e454ba5652a335cac8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 16 Aug 2018 17:53:13 +0200 Subject: [PATCH 194/868] Added positioning for SVG text, fixed bbox calculation to include positioning --- examples/debug.rs | 3 ++ src/widgets/mod.rs | 2 +- src/widgets/svg.rs | 69 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index cebc5dcd2..7362b5bd5 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -145,6 +145,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv BezierControlPoint { x: 80.0, y: 120.0 }, BezierControlPoint { x: 120.0, y: 0.0 }, ])), + position: SvgPosition { x: 50.0, y: 50.0 }, }, SvgText { font_size: font_size, @@ -152,6 +153,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv text_layout: SvgTextLayout::from_str("Rotated", &font, &font_size), style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), placement: SvgTextPlacement::Rotated(-30.0), + position: SvgPosition { x: 50.0, y: 50.0 }, }, SvgText { font_size: font_size, @@ -159,6 +161,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv text_layout: SvgTextLayout::from_str("Unmodified\nCool", &font, &font_size), style: SvgStyle::filled(ColorU { r: 0, g: 0, b: 0, a: 255 }), placement: SvgTextPlacement::Unmodified, + position: SvgPosition { x: 50.0, y: 50.0 }, }, ]; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 0e1c1428f..362a843af 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -10,7 +10,7 @@ pub use self::svg::{ SvgStrokeOptions, VertexBuffers, SvgVert, GlyphId, SvgCircle, SvgRect, BezierControlPoint, SampledBezierCurve, SvgText, SvgTextPlacement, SvgTextLayout, SvgBbox, - BezierNormalVector, BezierCharacterRotation, + BezierNormalVector, BezierCharacterRotation, SvgPosition, join_vertex_buffers, get_fill_vertices, get_stroke_vertices, scale_vertex_buffer, transform_vertex_buffer, rotate_vertex_buffer, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index f1c6a9c2b..96f8a4cd7 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -25,7 +25,7 @@ use lyon::{ default::{Builder}, builder::{PathBuilder, FlatPathBuilder}, PathEvent, }, - geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D}, + geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D, TypedVector2D}, }; use resvg::usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF, GlyphInstance}; @@ -1764,19 +1764,26 @@ pub enum SvgTextPlacement { #[derive(Debug, Clone)] pub struct SvgText { + /// Font size of the text pub font_size: FontSize, + /// Font ID, such as "ExternalFont('Arial')" pub font_id: FontId, + /// What are the glyphs in this text pub text_layout: SvgTextLayout, + /// What is the font color & stroke (if any)? pub style: SvgStyle, + /// Is the text rotated or on a curve? pub placement: SvgTextPlacement, + /// X and Y displacement of the font in the drawing, measured from the top left + pub position: SvgPosition, } #[derive(Debug, Clone)] -pub struct SvgTextLayout(LayoutTextResult); +pub struct SvgTextLayout(pub LayoutTextResult); /// An axis-aligned bounding box (not rotated / skewed) #[derive(Debug, Copy, Clone)] -pub struct SvgBbox(TypedRect); +pub struct SvgBbox(pub TypedRect); impl SvgBbox { /// Simple function for drawing a single bounding box (in black). @@ -1795,6 +1802,11 @@ impl SvgBbox { pub fn contains_point(&self, x: f32, y: f32) -> bool { self.0.contains(&TypedPoint2D::new(x, y)) } + + /// Translate the SvgBbox by x / y + pub fn translate(&mut self, x: f32, y: f32) { + self.0 = self.0.translate(&TypedVector2D::new(x, y)); + } } #[inline] @@ -1910,36 +1922,39 @@ impl SvgText { match self.placement { SvgTextPlacement::Unmodified => { - normal_text(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size) + normal_text(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size) }, SvgTextPlacement::Rotated(degrees) => { - rotated_text(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size, degrees) + rotated_text(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size, degrees) }, SvgTextPlacement::OnCubicBezierCurve(curve) => { - text_on_curve(&self.text_layout.0, self.style, &font, vectorized_font, &self.font_size, &curve) + text_on_curve(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size, &curve) } } } pub fn get_bbox(&self) -> SvgBbox { - self.text_layout.get_bbox(&self.placement) + let mut bbox = self.text_layout.get_bbox(&self.placement); + bbox.translate(self.position.x, self.position.y); + bbox } } fn normal_text( layout: &LayoutTextResult, + position: &SvgPosition, text_style: SvgStyle, font: &Font, - vector_font: &VectorizedFont, + vectorized_font: &VectorizedFont, font_size: &FontSize) -> SvgLayerResource { let fill_vertices = text_style.fill.and_then(|_| { - Some(normal_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, get_fill_vertices)) + Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(normal_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, get_stroke_vertices)) + Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -1949,8 +1964,21 @@ fn normal_text( } } +#[derive(Debug, Copy, Clone)] +pub struct SvgPosition { + pub x: f32, + pub y: f32, +} + +impl Default for SvgPosition { + fn default() -> Self { + SvgPosition { x: 0.0, y: 0.0 } + } +} + fn normal_text_to_vertices( font_size: &FontSize, + position: &SvgPosition, glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, @@ -1961,7 +1989,7 @@ fn normal_text_to_vertices( .filter_map(|gid| transform_func(vectorized_font, original_font, &GlyphId(gid.index)).and_then(|vbuf| Some((vbuf, gid)))) .map(|(mut vertex_buf, gid)| { scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0 + position.x, gid.point.y + position.y); vertex_buf }) .collect::>(); @@ -1971,19 +1999,20 @@ fn normal_text_to_vertices( fn rotated_text( layout: &LayoutTextResult, + position: &SvgPosition, text_style: SvgStyle, font: &Font, - vector_font: &VectorizedFont, + vectorized_font: &VectorizedFont, font_size: &FontSize, rotation_degrees: f32) -> SvgLayerResource { let fill_vertices = text_style.fill.and_then(|_| { - Some(rotated_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, rotation_degrees, get_fill_vertices)) + Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(rotated_text_to_vertices(&font_size, &layout.layouted_glyphs, vector_font, font, rotation_degrees, get_stroke_vertices)) + Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -1995,6 +2024,7 @@ fn rotated_text( fn rotated_text_to_vertices( font_size: &FontSize, + position: &SvgPosition, glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, @@ -2010,6 +2040,7 @@ fn rotated_text_to_vertices( scale_vertex_buffer(&mut vertex_buf.vertices, font_size); transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + transform_vertex_buffer(&mut vertex_buf.vertices, position.x, position.y); vertex_buf }) .collect::>(); @@ -2019,9 +2050,10 @@ fn rotated_text_to_vertices( fn text_on_curve( layout: &LayoutTextResult, + position: &SvgPosition, text_style: SvgStyle, font: &Font, - vector_font: &VectorizedFont, + vectorized_font: &VectorizedFont, font_size: &FontSize, curve: &SampledBezierCurve) -> SvgLayerResource @@ -2029,11 +2061,11 @@ fn text_on_curve( let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs, 0.0); let fill_vertices = text_style.fill.and_then(|_| { - Some(curved_vector_text_to_vertices(font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_fill_vertices)) + Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(curved_vector_text_to_vertices(font_size, &layout.layouted_glyphs, vector_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) + Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -2046,6 +2078,7 @@ fn text_on_curve( // Calculates the layout for one word block fn curved_vector_text_to_vertices( font_size: &FontSize, + position: &SvgPosition, glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, @@ -2075,7 +2108,7 @@ fn curved_vector_text_to_vertices( rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x, *char_offset_y); + transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x + position.x, *char_offset_y + position.y); vertex_buf }) From b8730cc7095406eab52985704046f24ae98a97a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 17 Aug 2018 14:46:48 +0200 Subject: [PATCH 195/868] Added On::KeyDown and On::KeyUp for keyboard input --- src/app.rs | 8 ++++ src/dom.rs | 12 ++++++ src/lib.rs | 7 +++- src/window_state.rs | 94 +++++++++++++++++++++++++++++---------------- 4 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/app.rs b/src/app.rs index 476ff6151..f30b5b07d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -515,6 +515,11 @@ enum WindowCloseEvent { NoCloseEvent, } +/// Pre-filters any events that are not handled by the framework yet, since it would be wasteful +/// to process them. Modifies the `frame_event_info` +/// +/// `awakened_task` is a special field that should be set to true if the `Task` +/// system fired a `WindowEvent::Awakened`. fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo, awakened_task: bool) -> WindowCloseEvent { use glium::glutin::WindowEvent; @@ -546,6 +551,9 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo, awaken WindowEvent::CloseRequested => { return WindowCloseEvent::AboutToClose; }, + WindowEvent::KeyboardInput { .. } => { + frame_event_info.should_hittest = true; + } _ => { }, } }, diff --git a/src/dom.rs b/src/dom.rs index 7c07b22b4..0f73c5ac2 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -188,6 +188,18 @@ pub enum On { MouseLeave, /// Mousewheel / touchpad scrolling Scroll, + /// A key was pressed. Check `window.get_keyboard_state().current_chars` for + /// getting the actual key / virtual key / scancode. + /// + /// Warning: key repeat is on. When a key is held down, this event fires + /// multiple times, the delay between events depends on the operating system. + KeyDown, + /// A key was released. Check `window.get_keyboard_state().current_chars` for + /// getting the actual key / virtual key / scancode + /// + /// Warning: key repeat is on. When a key is held down, this event fires + /// multiple times, the delay between events depends on the operating system. + KeyUp, } pub struct NodeData { diff --git a/src/lib.rs b/src/lib.rs index 56a67e6cd..777f27a5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,11 +149,14 @@ pub mod prelude { Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, ExtendMode, PixelValue, PercentageValue, }; - pub use glium::glutin::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; + pub use glium::glutin::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + VirtualKeyCode, ScanCode, + }; pub use rusttype::Font; pub use resources::AppResources; pub use task::TerminateDaemon; - + #[cfg(feature = "logging")] pub use log::LevelFilter; } diff --git a/src/window_state.rs b/src/window_state.rs index 74a8c4be6..2bb487647 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -2,7 +2,7 @@ //! click was a mouseover, mouseout, and so on and calling the correct callbacks) use glium::glutin::{ - Window, Event, WindowEvent, KeyboardInput, ElementState, + Window, Event, WindowEvent, KeyboardInput, ScanCode, ElementState, MouseCursor, VirtualKeyCode, MouseScrollDelta, ModifiersState, dpi::{LogicalPosition, LogicalSize}, }; @@ -33,8 +33,21 @@ pub struct KeyboardState pub alt_down: bool, /// `Super / Windows / Command` key pub super_down: bool, - /// Currently pressed keys + /// Currently pressed keys, already converted to characters pub current_keys: HashSet, + /// Currently pressed virtual keycodes - this is essentially an "extension" + /// of `current_keys` - `current_keys` stores the characters, but what if the + /// pressed key is not a character (such as `ArrowRight` or `PgUp`)? + /// + /// Note that this can have an overlap, so pressing "a" on the keyboard will insert + /// both a `VirtualKeyCode::A` into `current_virtual_keycodes` and an `"a"` as a char into `current_keys`. + pub current_virtual_keycodes: HashSet, + /// Same as `current_virtual_keycodes`, but the scancode identifies the physical key pressed. + /// + /// This should not change if the user adjusts the host's keyboard map. + /// Use when the physical location of the key is more important than the key's host GUI semantics, + /// such as for movement controls in a first-person game (German keyboard: Z key, UK keyboard: Y key, etc.) + pub current_scancodes: HashSet, } impl KeyboardState { @@ -45,10 +58,6 @@ impl KeyboardState { self.alt_down = state.alt; self.super_down = state.logo; } - - pub(crate) fn clear_keys(&mut self) { - self.current_keys.clear(); - } } /// Mouse position on the screen @@ -173,88 +182,95 @@ impl WindowState // so that we are ready for the next frame pub(crate) fn determine_callbacks(&mut self, event: &Event) -> Vec { + use std::collections::HashSet; use glium::glutin::{ - Event::WindowEvent, - WindowEvent::*, + Event, WindowEvent, KeyboardInput, MouseButton::*, dpi::LogicalPosition, }; - let event = if let WindowEvent { event, .. } = event { event } else { return Vec::new(); }; + let event = if let Event::WindowEvent { event, .. } = event { event } else { return Vec::new(); }; // store the current window state so we can set it in this.previous_window_state later on let mut previous_state = Box::new(self.clone()); previous_state.previous_window_state = None; - let mut events_vec = Vec::::new(); + let mut events_vec = HashSet::::new(); match event { - MouseInput { state: ElementState::Pressed, button, .. } => { + WindowEvent::MouseInput { state: ElementState::Pressed, button, .. } => { match button { Left => { if !self.mouse_state.left_down { - events_vec.push(On::MouseDown); - events_vec.push(On::LeftMouseDown); + events_vec.insert(On::MouseDown); + events_vec.insert(On::LeftMouseDown); } self.mouse_state.left_down = true; }, Right => { if !self.mouse_state.right_down { - events_vec.push(On::MouseDown); - events_vec.push(On::RightMouseDown); + events_vec.insert(On::MouseDown); + events_vec.insert(On::RightMouseDown); } self.mouse_state.right_down = true; }, Middle => { if !self.mouse_state.middle_down { - events_vec.push(On::MouseDown); - events_vec.push(On::MiddleMouseDown); + events_vec.insert(On::MouseDown); + events_vec.insert(On::MiddleMouseDown); } self.mouse_state.middle_down = true; }, _ => { } } }, - MouseInput { state: ElementState::Released, button, .. } => { + WindowEvent::MouseInput { state: ElementState::Released, button, .. } => { match button { Left => { if self.mouse_state.left_down { - events_vec.push(On::MouseUp); - events_vec.push(On::LeftMouseUp); + events_vec.insert(On::MouseUp); + events_vec.insert(On::LeftMouseUp); } self.mouse_state.left_down = false; }, Right => { if self.mouse_state.right_down { - events_vec.push(On::MouseUp); - events_vec.push(On::RightMouseUp); + events_vec.insert(On::MouseUp); + events_vec.insert(On::RightMouseUp); } self.mouse_state.right_down = false; }, Middle => { if self.mouse_state.middle_down { - events_vec.push(On::MouseUp); - events_vec.push(On::MiddleMouseUp); + events_vec.insert(On::MouseUp); + events_vec.insert(On::MiddleMouseUp); } self.mouse_state.middle_down = false; }, _ => { } } }, - MouseWheel { delta, .. } => { + WindowEvent::MouseWheel { delta, .. } => { let (scroll_x_px, scroll_y_px) = match delta { MouseScrollDelta::PixelDelta(LogicalPosition { x, y }) => (*x, *y), MouseScrollDelta::LineDelta(x, y) => (*x as f64 * 100.0, *y as f64 * 100.0), }; self.mouse_state.scroll_x = -scroll_x_px; self.mouse_state.scroll_y = -scroll_y_px; // TODO: "natural scrolling"? - events_vec.push(On::Scroll); + events_vec.insert(On::Scroll); + }, + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, virtual_keycode: Some(_), .. }, .. } => { + events_vec.insert(On::KeyDown); + }, + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Released, virtual_keycode: Some(_), .. }, .. } => { + events_vec.insert(On::KeyUp); }, _ => { } } self.previous_window_state = Some(previous_state); - events_vec + + events_vec.into_iter().collect() } pub(crate) fn update_keyboard_modifiers(&mut self, event: &Event) { @@ -308,18 +324,28 @@ impl WindowState match event { Event::WindowEvent { event, .. } => { match event { - WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, virtual_keycode: Some(vk), .. }, .. } => { - if let Some(ch) = virtual_key_code_to_char(*vk) { - self.keyboard_state.current_keys.insert(ch); + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, virtual_keycode, scancode, .. }, .. } => { + if let Some(vk) = virtual_keycode { + if let Some(ch) = virtual_key_code_to_char(*vk) { + self.keyboard_state.current_keys.insert(ch); + } + self.keyboard_state.current_virtual_keycodes.insert(*vk); } + self.keyboard_state.current_scancodes.insert(*scancode); }, - WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Released, virtual_keycode: Some(vk), .. }, .. } => { - if let Some(ch) = virtual_key_code_to_char(*vk) { - self.keyboard_state.current_keys.remove(&ch); + WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Released, virtual_keycode, scancode, .. }, .. } => { + if let Some(vk) = virtual_keycode { + if let Some(ch) = virtual_key_code_to_char(*vk) { + self.keyboard_state.current_keys.remove(&ch); + } + self.keyboard_state.current_virtual_keycodes.remove(vk); } + self.keyboard_state.current_scancodes.remove(scancode); }, WindowEvent::Focused(false) => { - self.keyboard_state.clear_keys(); + self.keyboard_state.current_keys.clear(); + self.keyboard_state.current_virtual_keycodes.clear(); + self.keyboard_state.current_scancodes.clear(); }, _ => { }, } From 10356b1cfc57db0f4e2b2449f73bcb36612601bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 17 Aug 2018 15:08:16 +0200 Subject: [PATCH 196/868] Added background color to SVG - doesn't work yet --- src/widgets/svg.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 96f8a4cd7..b7fb89684 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1599,6 +1599,8 @@ pub struct Svg { pub zoom: f32, /// Whether an FXAA shader should be applied to the resulting OpenGL texture pub enable_fxaa: bool, + /// Background color (default: transparent) + pub background_color: ColorU, } impl Default for Svg { @@ -1608,6 +1610,7 @@ impl Default for Svg { pan: (0.0, 0.0), zoom: 1.0, enable_fxaa: false, + background_color: ColorU { r: 0, b: 0, g: 0, a: 0 }, } } } @@ -2142,6 +2145,14 @@ impl Svg { self } + #[inline] + pub fn with_background_color(mut self, color: ColorU) + -> Self + { + self.background_color = color; + self + } + #[inline] pub fn with_fxaa(mut self, enable_fxaa: bool) -> Self @@ -2154,11 +2165,18 @@ impl Svg { pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom where T: Layout { - const DEFAULT_COLOR: ColorU = ColorU { r: 0, b: 0, g: 0, a: 255 }; - let (window_width, window_height) = window.get_physical_size(); let tex = window.create_texture(window_width as u32, window_height as u32); - tex.as_surface().clear_color(1.0, 1.0, 1.0, 1.0); + + // TODO: This currently doesn't work - only the first draw call is drawn + // This is probably because either webrender or glium messes with the texture + // in some way. Need to investigate. + let background_color: ColorF = self.background_color.into(); + tex.as_surface().clear_color( + background_color.r, + background_color.g, + background_color.b, + background_color.a); let z_index: f32 = 0.5; let bbox: TypedRect = TypedRect { From a087652aa858a58a0e4c2c392046b531edd18361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 17 Aug 2018 15:31:41 +0200 Subject: [PATCH 197/868] Added CssColor for parsing and serializing CSS colors --- src/css_parser.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index 49f2f36f6..f9ef1facd 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1796,14 +1796,80 @@ multi_type_parser!(parse_layout_text_align, TextAlignmentHorz, ["left", Left], ["right", Right]); +/// CssColor is simply a wrapper around the internal CSS color parsing methods. +/// +/// Sometimes you'd want to load and parse a CSS color, but you don't want to +/// write your own parser for that. Since azul already has a parser for CSS colors, +/// this API exposes +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct CssColor { + internal: ColorU, +} + +impl CssColor { + /// Can parse a CSS color with or without prefixed hash or capitalization, i.e. `#aabbcc` + pub fn from_str<'a>(input: &'a str) -> Result> { + let color = parse_css_color(input)?; + Ok(Self { + internal: color, + }) + } + + /// Returns the internal parsed color, but in a `0.0 - 1.0` range instead of `0 - 255` + pub fn to_color_f(&self) -> ColorF { + self.internal.into() + } + + /// Returns the internal parsed color + pub fn to_color_u(&self) -> ColorU { + self.internal + } + + /// If `prefix_hash` is set to false, you only get the string, without a hash, in lowercase + /// + /// If `self.alpha` is `FF`, it wil be omitted from the final result (since `FF` is the default for CSS colors) + pub fn to_string(&self, prefix_hash: bool) -> String { + let prefix = if prefix_hash { "#" } else { "" }; + let alpha = if self.internal.a == 255 { String::new() } else { format!("{:02x}", self.internal.a) }; + format!("{}{:02x}{:02x}{:02x}{}", prefix, self.internal.r, self.internal.g, self.internal.b, alpha) + } +} + +impl Into for CssColor { + fn into(self) -> ColorF { + self.to_color_f() + } +} + +impl Into for CssColor { + fn into(self) -> ColorU { + self.to_color_u() + } +} + +impl Into for CssColor { + fn into(self) -> String { + self.to_string(false) + } +} + #[cfg(test)] mod css_tests { use super::*; + #[test] fn test_parse_box_shadow_1() { assert_eq!(parse_css_box_shadow("none"), Ok(None)); } + #[test] + fn test_css_color_convert() { + let color = "#FFD700"; + let parsed = CssColor::from_str(color).unwrap(); + assert_eq!(parsed.to_string(true).to_uppercase(), color.to_string()); + assert_eq!(parsed.to_color_u(), ColorU { r: 255, g: 215, b: 0, a: 255 }); + } + #[test] fn test_parse_box_shadow_2() { assert_eq!(parse_css_box_shadow("5px 10px"), Ok(Some(BoxShadowPreDisplayItem { diff --git a/src/lib.rs b/src/lib.rs index 777f27a5a..7a0ca3116 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,7 +143,7 @@ pub mod prelude { LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, LayoutMaxHeight, LayoutWrap, LayoutDirection, LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, - LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, FontId, + LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, FontId, CssColor, LayoutPixel, TypedSize2D, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, Gradient, SideOffsets2D, RadialGradient, LayoutPoint, LayoutSize, From c4500fddfed04ea7e8be75827c90ffc8a2f1c3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 17 Aug 2018 22:53:19 +0200 Subject: [PATCH 198/868] Impl Display for CssColorParseError --- src/css_parser.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index f9ef1facd..4d2f0be21 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1,6 +1,6 @@ //! Contains utilities to convert strings (CSS strings) to servo types -use std::num::{ParseIntError, ParseFloatError}; +use std::{fmt, num::{ParseIntError, ParseFloatError}}; pub use { euclid::{TypedSize2D, SideOffsets2D}, webrender::api::{ @@ -330,6 +330,17 @@ pub enum CssColorParseError<'a> { ValueParseErr(ParseIntError), } +impl<'a> fmt::Display for CssColorParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CssColorParseError::*; + match self { + InvalidColor(i) => write!(f, "Invalid CSS color: \"{}\"", i), + InvalidColorComponent(i) => write!(f, "Invalid color component when parsing CSS color: \"{}\"", i), + ValueParseErr(e) => write!(f, "Css color component: Value not in range between 00 - FF: \"{}\"", e), + } + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub enum CssImageParseError<'a> { UnclosedQuotes(&'a str), From 6ab14b50340ada5faa21ecd9028d666b2738c179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 17 Aug 2018 22:56:59 +0200 Subject: [PATCH 199/868] Use all log macros, due to API-breaking update in log! --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7a0ca3116..b387704bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,7 +51,7 @@ extern crate harfbuzz_rs; extern crate tinyfiledialogs; extern crate clipboard2; extern crate font_loader; -#[macro_use(error, log)] +#[macro_use] extern crate log; #[cfg(feature = "logging")] extern crate fern; From a11ed00c18a509c54410934fb3d1e78318fa2c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 18 Aug 2018 01:04:32 +0200 Subject: [PATCH 200/868] Made quick_rects more configurable - added option for fill color --- examples/debug.rs | 2 +- src/widgets/svg.rs | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7362b5bd5..996891151 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -68,7 +68,7 @@ fn build_layers( let mut layers: Vec = existing_layers.iter().map(|e| SvgLayerResource::Reference(*e)).collect(); layers.extend(texts.values().map(|text| text.to_svg_layer(vector_font_cache, resources))); - layers.extend(texts.values().map(|text| text.get_bbox().draw_lines())); + layers.extend(texts.values().map(|text| text.get_bbox().draw_lines(ColorU { r: 0, g: 0, b: 0, a: 255 }))); /* if let Some(active) = hovered_text { layers.push(texts[active].get_bbox().draw_lines()); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index b7fb89684..313aafb9a 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -564,21 +564,21 @@ pub fn quick_lines(lines: &[Vec<(f32, f32)>], stroke_color: ColorU, stroke_optio } } -pub fn quick_rects(rects: &[SvgRect], stroke_color: ColorU, stroke_options: Option) +pub fn quick_rects(rects: &[SvgRect], stroke_color: Option, fill_color: Option, stroke_options: Option) -> SvgLayerResource { - let stroke_options = stroke_options.unwrap_or_default(); - let style = SvgStyle::stroked(stroke_color, stroke_options); + let style = SvgStyle { + stroke: stroke_color.and_then(|col| Some((col, stroke_options.unwrap_or_default()))), + fill: fill_color, + }; let rects = rects.iter().map(|r| SvgLayerType::Rect(*r)).collect(); - let (_, stroke) = tesselate_layer_data(&LayerType::from_polygons(rects), 0.01, Some(stroke_options)); - - let stroke = stroke.unwrap(); + let (fill, stroke) = tesselate_layer_data(&LayerType::from_polygons(rects), 0.01, style.stroke.and_then(|(_, options)| Some(options))); SvgLayerResource::Direct { style: style, - fill: None, - stroke: Some(VerticesIndicesBuffer { vertices: stroke.0, indices: stroke.1 }), + fill: fill_color.and_then(|_| Some(VerticesIndicesBuffer { vertices: fill.0, indices: fill.1 })), + stroke: stroke.and_then(|stroke_vertices| Some(VerticesIndicesBuffer { vertices: stroke_vertices.0, indices: stroke_vertices.1 })), } } @@ -780,29 +780,29 @@ impl SampledBezierCurve { /// Returns the geometry necessary for drawing the points from `self.sampled_bezier_points`. /// Usually only good for debugging - pub fn draw_circles(&self) -> SvgLayerResource { + pub fn draw_circles(&self, color: ColorU) -> SvgLayerResource { quick_circles( &self.sampled_bezier_points .iter() .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 1.0 }) .collect::>(), - ColorU { r: 0, b: 0, g: 0, a: 255 }) + color) } /// Returns the geometry necessary to draw the control handles of this curve - pub fn draw_control_handles(&self) -> SvgLayerResource { + pub fn draw_control_handles(&self, color: ColorU) -> SvgLayerResource { quick_circles( &self.original_curve .iter() .map(|c| SvgCircle { center_x: c.x, center_y: c.y, radius: 3.0 }) .collect::>(), - ColorU { r: 255, b: 0, g: 0, a: 255 }) + color) } /// Returns the geometry necessary to draw the bezier curve (the actual line) - pub fn draw_lines(&self) -> SvgLayerResource { + pub fn draw_lines(&self, stroke_color: ColorU) -> SvgLayerResource { let line = [self.sampled_bezier_points.iter().map(|b| (b.x, b.y)).collect()]; - quick_lines(&line, ColorU { r: 0, b: 0, g: 0, a: 255 }, None) + quick_lines(&line, stroke_color, None) } } @@ -1789,8 +1789,8 @@ pub struct SvgTextLayout(pub LayoutTextResult); pub struct SvgBbox(pub TypedRect); impl SvgBbox { - /// Simple function for drawing a single bounding box (in black). - pub fn draw_lines(&self) -> SvgLayerResource { + /// Simple function for drawing a single bounding box + pub fn draw_lines(&self, color: ColorU) -> SvgLayerResource { quick_rects(&[SvgRect { width: self.0.size.width, height: self.0.size.height, @@ -1798,7 +1798,7 @@ impl SvgBbox { y: self.0.origin.y, rx: 0.0, ry: 0.0, - }], ColorU { r: 0, b: 0, g: 0, a: 255 }, None) + }], Some(color), None, None) } /// Checks if the bounding box contains a point From 204db2ded5b5c95b095a49f943121b7fdbb07111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 18 Aug 2018 01:57:10 +0200 Subject: [PATCH 201/868] Fix VirtualKeyCode::O to return 'o', not 'a' as a character --- src/window_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window_state.rs b/src/window_state.rs index 2bb487647..77ebc6049 100644 --- a/src/window_state.rs +++ b/src/window_state.rs @@ -389,7 +389,7 @@ fn virtual_key_code_to_char(code: VirtualKeyCode) -> Option { L => Some('l'), M => Some('m'), N => Some('n'), - O => Some('a'), + O => Some('o'), P => Some('p'), Q => Some('q'), R => Some('r'), From 91d524ed2def502b4a34f3662b396ff99e81d31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 18 Aug 2018 21:55:35 +0200 Subject: [PATCH 202/868] Use stb_truetype instead of custom rusttype fork --- Cargo.toml | 3 ++- src/lib.rs | 1 + src/widgets/svg.rs | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 86b0849b6..453ba682e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,8 @@ glium = "0.22.0" gleam = "0.6" euclid = "0.19" image = "0.19.0" -rusttype = { git = "https://github.com/fschutt/rusttype" } +rusttype = "0.6.4" +stb_truetype = "0.2.2" app_units = "0.7" unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" diff --git a/src/lib.rs b/src/lib.rs index b387704bf..8277718f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ extern crate app_units; extern crate unicode_normalization; extern crate harfbuzz_rs; extern crate tinyfiledialogs; +extern crate stb_truetype; extern crate clipboard2; extern crate font_loader; #[macro_use] diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 313aafb9a..bc6afa7dc 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1453,7 +1453,7 @@ mod svg_to_lyon { use traits::Layout; use webrender::api::ColorU; use FastHashMap; - use rusttype::Vertex; + use stb_truetype::Vertex; pub fn parse_from, T: Layout>(svg_source: S, view_boxes: &mut FastHashMap) -> Result<(Vec>, FastHashMap), SvgParseError> { @@ -1575,7 +1575,7 @@ mod svg_to_lyon { // in order to turn a glyph into a polygon pub fn rusttype_glyph_to_path_events(vertex: &Vertex) -> PathEvent - { use rusttype::VertexType; + { use stb_truetype::VertexType; // Rusttypes vertex type needs to be inverted in the Y axis // in order to work with lyon correctly match vertex.vertex_type() { From 84880046dd5dd631e4ddc08bb2956e335762f70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 15:39:55 +0200 Subject: [PATCH 203/868] Lazy-load system fonts, huge drop in memory (80MB -> 38MB) This slightly breaks webrenders fonts, so this isn't a final solution, but it brings down the memory consumption significantly by loading fonts only when absolutely necessary. This also uses Font<'static> so we can get rid of possible lifetime problems. Currently, because of mutability problems, font_data uses a RefCell and Rc. This currently breaks the font loading system for reasons I don't yet understand, so the examples currently don't display any fonts, but displaying the SVG still works. --- examples/debug.rs | 3 +- src/app.rs | 10 ++--- src/app_state.rs | 19 +++++---- src/css_parser.rs | 14 ++---- src/display_list.rs | 29 +++++++++---- src/font.rs | 2 +- src/resources.rs | 102 +++++++++++++++++++++++--------------------- src/text_layout.rs | 10 ++++- src/widgets/svg.rs | 2 +- src/window.rs | 2 +- 10 files changed, 105 insertions(+), 88 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 996891151..9601c532c 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -10,7 +10,6 @@ use std::fs; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; -const FONT_ID: FontId = FontId::BuiltinFont("sans-serif"); static TEXT_ID: AtomicUsize = AtomicUsize::new(0); #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -128,7 +127,7 @@ fn scroll_map_contents(app_state: &mut AppState, event: WindowEvent) fn my_button_click_handler(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { - let font_id = FONT_ID; + let font_id = FontId::BuiltinFont(String::from("sans-serif")); let font_size = FontSize::px(10.0); let font = app_state.resources.get_font(&font_id).unwrap().0; diff --git a/src/app.rs b/src/app.rs index f30b5b07d..aa92bbd36 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,11 +29,11 @@ use { }; /// Graphical application that maintains some kind of application state -pub struct App<'a, T: Layout> { +pub struct App { /// The graphical windows, indexed by ID windows: Vec>, /// The global application state - pub app_state: AppState<'a, T>, + pub app_state: AppState, } /// Error returned by the `.run()` function @@ -120,7 +120,7 @@ impl Default for AppConfig { } } -impl<'a, T: Layout> App<'a, T> { +impl App { #[allow(unused_variables)] /// Create a new, empty application. This does not open any windows. @@ -318,7 +318,7 @@ impl<'a, T: Layout> App<'a, T> { window.internal.api.send_transaction(window.internal.document_id, txn); } - fn initialize_ui_state(windows: &[Window], app_state: &AppState<'a, T>) + fn initialize_ui_state(windows: &[Window], app_state: &AppState) -> Vec> { use window::ReadOnlyWindow; @@ -501,7 +501,7 @@ impl<'a, T: Layout> App<'a, T> { } } -impl<'a, T: Layout + Send + 'static> App<'a, T> { +impl App { /// Tasks, once started, cannot be stopped, which is why there is no `.delete()` function pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) { diff --git a/src/app_state.rs b/src/app_state.rs index 23990ff82..e8ab52269 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2,6 +2,7 @@ use std::{ io::Read, collections::hash_map::Entry::*, sync::{Arc, Mutex}, + rc::Rc, }; use image::ImageError; use rusttype::Font; @@ -23,7 +24,7 @@ use { /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application /// should be laid out) -pub struct AppState<'a, T: Layout> { +pub struct AppState { /// Your data (the global struct which all callbacks will have access to) pub data: Arc>, /// Note: this isn't the real window state. This is a "mock" window state which @@ -36,14 +37,14 @@ pub struct AppState<'a, T: Layout> { /// ``` pub windows: Vec, /// Fonts and images that are currently loaded into the app - pub resources: AppResources<'a>, + pub resources: AppResources, /// Currently running daemons (polling functions) pub(crate) daemons: FastHashMap (UpdateScreen, TerminateDaemon)>, /// Currently running tasks (asynchronous functions running on a different thread) pub(crate) tasks: Vec, } -impl<'a, T: Layout> AppState<'a, T> { +impl AppState { /// Creates a new `AppState` pub fn new(initial_data: T) -> Self { @@ -153,7 +154,7 @@ impl<'a, T: Layout> AppState<'a, T> { self.resources.has_font(id) } - pub fn get_font<'b>(&'b self, id: &FontId) -> Option<(&'b Font<'a>, &'b Vec)> { + pub fn get_font(&self, id: &FontId) -> Option<(Rc>, Rc>)> { self.resources.get_font(id) } @@ -195,8 +196,8 @@ impl<'a, T: Layout> AppState<'a, T> { /// Run all currently registered daemons #[must_use] - pub(crate) fn run_all_daemons(&mut self) - -> UpdateScreen + pub(crate) fn run_all_daemons(&mut self) + -> UpdateScreen { let mut should_update_screen = UpdateScreen::DontRedraw; let mut lock = self.data.lock().unwrap(); @@ -204,7 +205,7 @@ impl<'a, T: Layout> AppState<'a, T> { for (key, daemon) in self.daemons.iter() { let (should_update, should_terminate) = (daemon.clone())(&mut lock); - + if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { should_update_screen = UpdateScreen::Redraw; @@ -218,7 +219,7 @@ impl<'a, T: Layout> AppState<'a, T> { for key in daemons_to_terminate { self.daemons.remove(&key); } - + should_update_screen } @@ -273,7 +274,7 @@ impl<'a, T: Layout> AppState<'a, T> { } } -impl<'a, T: Layout + Send + 'static> AppState<'a, T> { +impl AppState { /// Tasks, once started, cannot be stopped pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) { diff --git a/src/css_parser.rs b/src/css_parser.rs index 4d2f0be21..ff89ec0d4 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1709,14 +1709,13 @@ pub struct FontFamily { #[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum FontId { - BuiltinFont(&'static str), + BuiltinFont(String), ExternalFont(String), } #[derive(Debug, PartialEq, Copy, Clone)] pub enum CssFontFamilyParseError<'a> { InvalidFontFamily(&'a str), - UnrecognizedBuiltinFont(&'a str), UnclosedQuotes(&'a str), } @@ -1747,14 +1746,7 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result fonts.push(FontId::BuiltinFont("serif")), - "sans-serif" => fonts.push(FontId::BuiltinFont("sans-serif")), - "monospace" => fonts.push(FontId::BuiltinFont("monospace")), - "fantasy" => fonts.push(FontId::BuiltinFont("fantasy")), - "cursive" => fonts.push(FontId::BuiltinFont("cursive")), - _ => return Err(CssFontFamilyParseError::UnrecognizedBuiltinFont(font)), - } + fonts.push(FontId::BuiltinFont(font.into())); } } @@ -2238,7 +2230,7 @@ mod css_tests { assert_eq!(parse_css_font_family("\"Webly Sleeky UI\", monospace"), Ok(FontFamily { fonts: vec![ FontId::ExternalFont("Webly Sleeky UI".into()), - FontId::BuiltinFont("monospace"), + FontId::BuiltinFont("monospace".into()), ] })); } diff --git a/src/display_list.rs b/src/display_list.rs index e23247321..45d22d27a 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -25,7 +25,6 @@ use { }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); -const DEFAULT_BUILTIN_FONT_SANS_SERIF: FontId = FontId::BuiltinFont("sans-serif"); pub(crate) struct DisplayList<'a, T: Layout + 'a> { pub(crate) ui_descr: &'a UiDescription, @@ -64,6 +63,7 @@ pub(crate) struct SolvedLayout { /// /// TODO: It should be possible to switch this over to a `&'a str`, but currently /// this leads to unsolvable borrowing issues. +#[derive(Debug)] pub(crate) enum TextInfo { Cached(TextId), Uncached(String), @@ -204,8 +204,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let mut updated_fonts = Vec::<(FontId, Vec)>::new(); let mut to_delete_fonts = Vec::<(FontId, Option<(FontKey, Vec)>)>::new(); - for (key, value) in app_resources.font_data.iter() { - match value.2 { + for (key, value) in app_resources.font_data.borrow().iter() { + match &*value.2 { FontState::ReadyForUpload(ref bytes) => { updated_fonts.push((key.clone(), bytes.clone())); }, @@ -230,14 +230,16 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { resource_updates.push(ResourceUpdate::DeleteFont(font_key)); app_resources.fonts.remove(&font_key); } - app_resources.font_data.remove(&resource_key); + app_resources.font_data.borrow_mut().remove(&resource_key); } + use std::rc::Rc; + // Upload all remaining fonts to the GPU only if the haven't been uploaded yet for (resource_key, data) in updated_fonts.into_iter() { let key = api.generate_font_key(); resource_updates.push(ResourceUpdate::AddFont(AddFont::Raw(key, data, 0))); // TODO: use the index better? - app_resources.font_data.get_mut(&resource_key).unwrap().2 = FontState::Uploaded(key); + app_resources.font_data.borrow_mut().get_mut(&resource_key).unwrap().2 = Rc::new(FontState::Uploaded(key)); } } @@ -548,8 +550,11 @@ fn push_text( let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX as i32); - let font_id = font_family.fonts.get(0).unwrap_or(&DEFAULT_BUILTIN_FONT_SANS_SERIF); + println!("text: {:?}, font_family.fonts: {:?}", text, font_family.fonts); + let font_id = match font_family.fonts.get(0) { Some(s) => s, None => { error!("div @ {:?} has no font assigned!", bounds); return; }}; + println!("font_id: {:?}", font_id); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); + println!("font result: {:?}", font_result); let font_instance_key = match font_result { Some(f) => f, @@ -886,16 +891,22 @@ fn push_font( { use font::FontState; + println!("in function push_font!, font_size_app_units: {:?}, MIN_AU: {:?}", font_size_app_units, MIN_AU); + if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { error!("warning: too big or too small font size"); return None; } - let &(ref font, _, ref font_state) = match app_resources.font_data.get(font_id) { - Some(f) => f, - None => return None, + let font_state = match app_resources.font_data.borrow().get(font_id) { + Some(f) => f.2.clone(), + None => { + println!("returning none!"); return None; }, }; + // let font_state = app_resources.get_font_state(font_id)?; + println!("font_id: {:?}, font_state: {:?}", font_id, font_state); + match *font_state { FontState::Uploaded(font_key) => { let font_sizes_hashmap = app_resources.fonts.entry(font_key) diff --git a/src/font.rs b/src/font.rs index 9c5024ec6..f6ab159af 100644 --- a/src/font.rs +++ b/src/font.rs @@ -35,7 +35,7 @@ impl From for FontError { } /// Read font data to get font information, v_metrics, glyph info etc. -pub(crate) fn rusttype_load_font<'a>(data: Vec, index: Option) -> Result<(Font<'a>, Vec), FontError> { +pub(crate) fn rusttype_load_font(data: Vec, index: Option) -> Result<(Font<'static>, Vec), FontError> { let collection = FontCollection::from_bytes(data.clone())?; let font = collection.clone().into_font().unwrap_or(collection.font_at(index.and_then(|i| Some(i as usize)).unwrap_or(0))?); Ok((font, data)) diff --git a/src/resources.rs b/src/resources.rs index 08b33964d..0edd97d8c 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -14,6 +14,8 @@ use css_parser::{ FontSize, FontId::{self, ExternalFont} }; +use std::rc::Rc; +use std::cell::RefCell; /// Font and image keys /// @@ -25,7 +27,7 @@ use css_parser::{ /// /// Images and fonts can be references across window contexts /// (not yet tested, but should work). -pub struct AppResources<'a> { +pub struct AppResources { /// When looking up images, there are two sources: Either the indirect way via using a /// CssId (which is a String) or a direct ImageId. The indirect way requires one extra /// lookup (to map from the stringified ID to the actual image ID). This is what this @@ -39,7 +41,7 @@ pub struct AppResources<'a> { // but we also need access to the font metrics. So we first parse the font // to make sure that nothing is going wrong. In the next draw call, we // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: FastHashMap, Vec, FontState)>, + pub(crate) font_data: RefCell>, Rc>, Rc)>>, // After we've looked up the FontKey in the font_data map, we can then access // the font instance key (if there is any). If there is no font instance key, // we first need to create one. @@ -50,15 +52,12 @@ pub struct AppResources<'a> { clipboard: SystemClipboard, } -impl<'a> Default for AppResources<'a> { +impl Default for AppResources { fn default() -> Self { - let mut default_font_data = FastHashMap::default(); - load_system_fonts(&mut default_font_data); - Self { css_ids_to_image_ids: FastHashMap::default(), fonts: FastHashMap::default(), - font_data: default_font_data, + font_data: RefCell::new(FastHashMap::default()), images: FastHashMap::default(), text_cache: TextCache::default(), clipboard: SystemClipboard::new().unwrap(), @@ -66,33 +65,11 @@ impl<'a> Default for AppResources<'a> { } } -fn load_system_fonts<'a>(fonts: &mut FastHashMap, Vec, FontState)>) { - - use font_loader::system_fonts::{self, FontPropertyBuilder}; - use css_parser::FontId::BuiltinFont; - use font::rusttype_load_font; - - fn insert_font<'b>(fonts: &mut FastHashMap, Vec, FontState)>, target: &'static str) { - if let Some((font_bytes, idx)) = system_fonts::get(&FontPropertyBuilder::new().family(target).build()) { - match rusttype_load_font(font_bytes.clone(), Some(idx)) { - Ok((f, b)) => { fonts.insert(BuiltinFont(target), (f, b, FontState::ReadyForUpload(font_bytes))); }, - Err(e) => error!("Error loading {} font: {:?}", target, e), - } - } - } - - insert_font(fonts, "serif"); - insert_font(fonts, "sans-serif"); - insert_font(fonts, "monospace"); - insert_font(fonts, "cursive"); - insert_font(fonts, "fantasy"); -} - -impl<'a> AppResources<'a> { +impl AppResources { /// Returns the IDs of all currently loaded fonts in `self.font_data` pub fn get_loaded_fonts(&self) -> Vec { - self.font_data.keys().cloned().collect() + self.font_data.borrow().keys().cloned().collect() } /// See `AppState::add_image()` @@ -163,47 +140,76 @@ impl<'a> AppResources<'a> { { use font; - match self.font_data.entry(ExternalFont(id.into())) { + match self.font_data.borrow_mut().entry(ExternalFont(id.into())) { Occupied(_) => Ok(None), Vacant(v) => { let mut font_data = Vec::::new(); data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; let (parsed_font, fd) = font::rusttype_load_font(font_data.clone(), None)?; - v.insert((parsed_font, fd, FontState::ReadyForUpload(font_data))); + v.insert((Rc::new(parsed_font), Rc::new(fd), Rc::new(FontState::ReadyForUpload(font_data)))); Ok(Some(())) }, } } - pub fn get_font<'b>(&'b self, id: &FontId) -> Option<(&'b Font<'a>, &'b Vec)> { - self.font_data.get(id).and_then(|(font, bytes, _)| Some((font, bytes))) + pub fn get_font(&self, id: &FontId) -> Option<(Rc>, Rc>)> { + match id { + FontId::BuiltinFont(b) => { + if self.font_data.borrow().get(id).is_none() { + let (font, font_bytes, font_state) = Self::get_builtin_font(b.clone())?; + self.font_data.borrow_mut().insert(id.clone(), (Rc::new(font), Rc::new(font_bytes), Rc::new(font_state))); + } + self.font_data.borrow().get(id).and_then(|(font, bytes, _)| Some((font.clone(), bytes.clone()))) + }, + FontId::ExternalFont(_) => { + // For external fonts, we assume that the application programmer has + // already loaded them, so we don't try to fallback to system fonts. + self.font_data.borrow().get(id).and_then(|(font, bytes, _)| Some((font.clone(), bytes.clone()))) + }, + } + } + + /// Search for a builtin font on the computer and and insert it + fn get_builtin_font(id: String) -> Option<(::rusttype::Font<'static>, Vec, FontState)> + { + use font_loader::system_fonts::{self, FontPropertyBuilder}; + use font::rusttype_load_font; + + let (font_bytes, idx) = system_fonts::get(&FontPropertyBuilder::new().family(&id).build())?; + let (f, b) = rusttype_load_font(font_bytes.clone(), Some(idx)).ok()?; + Some((f, b, FontState::ReadyForUpload(font_bytes))) + } + + pub(crate) fn get_font_state(&self, id: &FontId) -> Option> { + self.font_data.borrow().get(id).and_then(|(_, _, font_state)| Some(font_state.clone())) } /// Checks if a font is currently registered and ready-to-use pub(crate) fn has_font>(&mut self, id: S) -> bool { - self.font_data.get(&ExternalFont(id.into())).is_some() + self.font_data.borrow().get(&ExternalFont(id.into())).is_some() } /// See `AppState::delete_font()` pub(crate) fn delete_font>(&mut self, id: S) -> Option<()> { + let id = ExternalFont(id.into()); + // TODO: can fonts that haven't been uploaded yet be deleted? - match self.font_data.get_mut(&ExternalFont(id.into())) { - None => None, - Some(v) => { - let to_delete_font_key = match v.2 { - FontState::Uploaded(ref font_key) => { - Some(font_key.clone()) - }, - _ => None, - }; - v.2 = FontState::AboutToBeDeleted(to_delete_font_key); - Some(()) + let mut to_delete_font_key = None; + + match self.font_data.borrow().get(&id) { + None => return None, + Some(v) => match &*v.2 { + FontState::Uploaded(font_key) => { to_delete_font_key = Some(font_key.clone()); }, + _ => { }, } } + + self.font_data.borrow_mut().get_mut(&id).unwrap().2 = Rc::new(FontState::AboutToBeDeleted(to_delete_font_key)); + Some(()) } pub(crate) fn add_text_uncached>(&mut self, text: S) @@ -233,7 +239,7 @@ impl<'a> AppResources<'a> { // Otherwise, how would the TextId be valid? let text = self.text_cache.string_cache.get(&id).expect("Invalid text Id"); let font_size_no_line_height = Scale::uniform(size.0.to_pixels() * PX_TO_PT); - let rusttype_font = self.font_data.get(&font).expect("Invalid font ID"); + let rusttype_font = self.get_font(&font).expect("Invalid font ID"); let words = split_text_into_words(text.as_ref(), &rusttype_font.0, font_size_no_line_height); self.text_cache.cached_strings diff --git a/src/text_layout.rs b/src/text_layout.rs index e980ce9f4..fb62a6206 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -183,7 +183,15 @@ pub(crate) fn get_glyphs( scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) { - let target_font = app_resources.font_data.get(target_font_id).expect("Drawing with invalid font!"); + println!("target_font_id: {:?}", target_font_id); + println!("loaded fonts (before .get_font() call): {:?}", app_resources.get_loaded_fonts()); + + let target_font = match app_resources.get_font(target_font_id) { + Some(s) => s, + None => panic!("Drawing with invalid font!: {:?}", target_font_id), + }; + + println!("loaded fonts (after .get_font() call): {:?}", app_resources.get_loaded_fonts()); let font_metrics = calculate_font_metrics(&target_font.0, target_font_size, line_height); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index bc6afa7dc..dbc87e640 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1271,7 +1271,7 @@ impl VectorizedFontCache { pub fn new(app_resources: &AppResources) -> Self { let mut fonts = FastHashMap::default(); for font_id in app_resources.get_loaded_fonts() { - fonts.entry(font_id.clone()).or_insert_with(|| VectorizedFont::from_font(app_resources.get_font(&font_id).unwrap().0)); + fonts.entry(font_id.clone()).or_insert_with(|| VectorizedFont::from_font(&*app_resources.get_font(&font_id).unwrap().0)); } Self { vectorized_fonts: fonts, diff --git a/src/window.rs b/src/window.rs index 68eba1042..f09031519 100644 --- a/src/window.rs +++ b/src/window.rs @@ -147,7 +147,7 @@ impl Drop for ReadOnlyWindow { pub struct WindowInfo<'a> { pub window_id: WindowId, pub window: ReadOnlyWindow, - pub resources: &'a AppResources<'a>, + pub resources: &'a AppResources, } impl fmt::Debug for FakeWindow { From 3ce8ee15e7627c012a4230dccec93c93735916bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 17:33:53 +0200 Subject: [PATCH 204/868] Use usvg instead of resvg to minimize dependencies --- Cargo.toml | 6 +++--- src/lib.rs | 2 +- src/widgets/svg.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 453ba682e..1bbce0c19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,9 @@ backtrace = { version = "0.3.9", optional = true } [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" -[dependencies.resvg] -git = "https://github.com/RazrFalcon/resvg.git" -rev = "0eaf6bdc6811e469ee655246cc39ed0b37329fd3" +[dependencies.usvg] +git = "https://github.com/RazrFalcon/usvg" +rev = "803c4f7bfa1775e7f3824173ac05793a8a4adc5c" [dependencies.webrender] git = "https://github.com/servo/webrender" diff --git a/src/lib.rs b/src/lib.rs index 8277718f8..2848ded1a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ extern crate lazy_static; extern crate palette; extern crate euclid; extern crate lyon; -extern crate resvg; +extern crate usvg; extern crate webrender; extern crate cassowary; extern crate twox_hash; diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index dbc87e640..9b48bc41a 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -27,7 +27,7 @@ use lyon::{ }, geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D, TypedVector2D}, }; -use resvg::usvg::{Error as SvgError, ViewBox, Transform}; +use usvg::{Error as SvgError, ViewBox, Transform}; use webrender::api::{ColorU, ColorF, GlyphInstance}; use rusttype::{Font, Glyph}; use { @@ -1445,7 +1445,7 @@ mod svg_to_lyon { math::Point, path::PathEvent, }; - use resvg::usvg::{ViewBox, Transform, Tree, PathSegment, + use usvg::{ViewBox, Transform, Tree, PathSegment, Color, Options, Paint, Stroke, LineCap, LineJoin, NodeKind}; use widgets::svg::{SvgLayer, SvgStrokeOptions, SvgLineCap, SvgLineJoin, SvgLayerType, SvgStyle, SvgCallbacks, SvgParseError, SvgTransformId, From d8b2247ef924eee9e99f0088a625e422d732676d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 18:07:21 +0200 Subject: [PATCH 205/868] Removed palette crate because we only used one function from it --- Cargo.toml | 1 - src/lib.rs | 1 - src/widgets/svg.rs | 54 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bbce0c19..1a75728fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ unicode-normalization = "0.1.5" harfbuzz_rs = "0.1.0" lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" -palette = "0.4.0" tinyfiledialogs = "3.3.5" clipboard2 = "0.1.0" font-loader = "0.7.0" diff --git a/src/lib.rs b/src/lib.rs index 2848ded1a..4a5e5ea23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,6 @@ pub extern crate gleam; #[macro_use] extern crate lazy_static; -extern crate palette; extern crate euclid; extern crate lyon; extern crate usvg; diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 9b48bc41a..e61927943 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -2268,19 +2268,17 @@ fn draw_vertex_buffer_to_surface( pan: (f32, f32), zoom: f32) { - use palette::Srgba; - - let color = Srgba::new(color.r, color.g, color.b, color.a).into_linear(); + let color = srgba_to_linear(color); let uniforms = uniform! { bbox_origin: (bbox.origin.x, bbox.origin.y), bbox_size: (bbox.size.width / 2.0, bbox.size.height / 2.0), z_index: z_index, color: ( - color.color.red as f32, - color.color.green as f32, - color.color.blue as f32, - color.alpha as f32 + color.r as f32, + color.g as f32, + color.b as f32, + color.a as f32 ), offset: (pan.0, pan.1), zoom: zoom, @@ -2289,6 +2287,48 @@ fn draw_vertex_buffer_to_surface( surface.draw(vertices, indices, shader, &uniforms, draw_options).unwrap(); } +/// Taken from the `palette` crate - I wouldn't want to +/// import the entire crate just for one function (due to added compile time) +/// +/// The MIT License (MIT) +/// +/// Copyright (c) 2015 Erik Hedvall +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. +fn srgba_to_linear(color: ColorF) -> ColorF { + + fn into_linear(x: f32) -> f32 { + if x <= 0.04045 { + x / 12.92 + } else { + ((x + 0.055) / 1.055).powf(2.4) + } + } + + ColorF { + r: into_linear(color.r), + g: into_linear(color.g), + b: into_linear(color.b), + a: color.a, + } +} + #[test] fn __codecov_test_widget_svg_file() { From 4cbdff0e177cfb58681157091976755b3ba1cf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 18:36:48 +0200 Subject: [PATCH 206/868] Removed unnecessary crates, added svg feature-flag - The harfbuzz_rs crate is currently not used, so it's temporarily removed until the font layout & rendering code is more mature. - Added "svg" feature-flag, since usvg and lyon (the two crates necessary for SVG rendering) have a huge amount of dependencies In total, right now we have roughly 113 dependencies that I don't think can be reduced any further, a lot of these crates are essential to the library working properly. A further measure could be to break down the svg feature even more because if you aren't parsing SVG, then the usvg crate is unnecessary, but you can still draw SVG. Also updated travis.yml and appveyor.yml to only check, not build --- .travis.yml | 9 ++++----- Cargo.toml | 17 +++++++++++++---- appveyor.yml | 7 +++++-- src/lib.rs | 5 +++-- src/widgets/mod.rs | 9 ++++++++- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index e8247aa90..448f05280 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,13 +19,12 @@ notifications: # because glium does a check first if it has a OGL 3.2 context script: - cargo clean - - cargo build --verbose --examples - - cargo build --no-default-features + - cargo check --verbose --examples + - cargo check --no-default-features + - cargo check --no-default-features --features="svg" + - cargo check --no-default-features --features="logging" - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" -# before_install: -# - sudo apt-get update - install: - PATH=$PATH:/home/travis/.cargo/bin diff --git a/Cargo.toml b/Cargo.toml index 1a75728fc..895857e18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,6 @@ rusttype = "0.6.4" stb_truetype = "0.2.2" app_units = "0.7" unicode-normalization = "0.1.5" -harfbuzz_rs = "0.1.0" -lyon = { version = "0.10.0", features = ["extra"] } lazy_static = "1.0.1" tinyfiledialogs = "3.3.5" clipboard2 = "0.1.0" @@ -37,6 +35,8 @@ font-loader = "0.7.0" log = "0.4.1" fern = { version = "0.5.5", optional = true } backtrace = { version = "0.3.9", optional = true } +lyon = { version = "0.10.0", features = ["extra"], optional = true } +# harfbuzz_rs = "0.1.0" [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" @@ -44,13 +44,14 @@ nfd = "0.0.4" [dependencies.usvg] git = "https://github.com/RazrFalcon/usvg" rev = "803c4f7bfa1775e7f3824173ac05793a8a4adc5c" +optional = true [dependencies.webrender] git = "https://github.com/servo/webrender" rev = "c939a61b83bcc9dc10742977704793e9a85b3858" [features] -default = ["logging"] +default = ["logging", "svg"] # The reason we do this is because doctests don't get cfg(test) # See: https://github.com/rust-lang/cargo/issues/4669 @@ -62,4 +63,12 @@ doc-test = [] # To actually test the library, run cargo --test --features=doc-test no-opengl-tests = [] -logging = ["fern", "backtrace"] \ No newline at end of file +# Enable this feature to enable crash logging & reporting. +# Azul will insert custom panic handlers to pop up a message and log +# crashes to an "error.log" file, see AppConfig for more details +logging = ["fern", "backtrace"] + +# The SVG parsing & rendering module is pretty huge - if you don't use +# SVG rendering in your app, you can turn this off to increase compilation +# speed and decrease your binary size +svg = ["lyon", "usvg"] \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 32b7bcfc2..89ed29ece 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,5 +41,8 @@ build: false # Equivalent to Travis' `script` phase test_script: - - cargo build --verbose --examples - - cargo test --verbose --features "doc-test no-opengl-tests" \ No newline at end of file + - cargo check --verbose --examples + - cargo check --no-default-features + - cargo check --no-default-features --features="svg" + - cargo check --no-default-features --features="logging" + - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" diff --git a/src/lib.rs b/src/lib.rs index 4a5e5ea23..5399bafde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,9 @@ pub extern crate gleam; #[macro_use] extern crate lazy_static; extern crate euclid; +#[cfg(feature = "svg")] extern crate lyon; +#[cfg(feature = "svg")] extern crate usvg; extern crate webrender; extern crate cassowary; @@ -46,7 +48,6 @@ extern crate simplecss; extern crate rusttype; extern crate app_units; extern crate unicode_normalization; -extern crate harfbuzz_rs; extern crate tinyfiledialogs; extern crate stb_truetype; extern crate clipboard2; @@ -179,5 +180,5 @@ pub mod errors { pub use clipboard2::ClipboardError; pub use window::WindowCreateError; - pub use widgets::svg::SvgParseError; + pub use widgets::errors::*; } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 362a843af..0d8b1feb3 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,8 +1,10 @@ +#[cfg(feature = "svg")] pub mod svg; pub mod button; pub mod label; // Re-export widgets +#[cfg(feature = "svg")] pub use self::svg::{ Svg, SvgLayerId, SvgLayer, LayerType, SvgStyle, SvgLayerType, SvgWorldPixel, SvgLayerResource, @@ -18,4 +20,9 @@ pub use self::svg::{ quadratic_interpolate_bezier, }; pub use self::button::{Button, ButtonContent}; -pub use self::label::Label; \ No newline at end of file +pub use self::label::Label; + +pub mod errors { + #[cfg(feature = "svg")] + pub use self::svg::SvgParseError; +} \ No newline at end of file From 44aae530fa700afa2343c04872a6356da45b00c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 19:01:25 +0200 Subject: [PATCH 207/868] Fixed compilation mistake with feature=svg flag --- src/widgets/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 0d8b1feb3..2408c7043 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -24,5 +24,5 @@ pub use self::label::Label; pub mod errors { #[cfg(feature = "svg")] - pub use self::svg::SvgParseError; + pub use super::svg::SvgParseError; } \ No newline at end of file From 523b3d429e90f1f17f73dd847ed4204b3cfca0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 19 Aug 2018 19:53:03 +0200 Subject: [PATCH 208/868] Added icon loading feature + fixed fullscreen on startup --- Cargo.toml | 8 +++++++- src/lib.rs | 2 +- src/window.rs | 41 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 895857e18..1be882014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,4 +71,10 @@ logging = ["fern", "backtrace"] # The SVG parsing & rendering module is pretty huge - if you don't use # SVG rendering in your app, you can turn this off to increase compilation # speed and decrease your binary size -svg = ["lyon", "usvg"] \ No newline at end of file +svg = ["lyon", "usvg"] + +# If you want an application icon, you can either load it via the raw +# RGBA bytes or use the icon_loading feature to decode it from a PNG / JPG / +# whatever image format on startup. Note that this will import the image +# dependency and use a bit of extra runtime. +icon_loading = ["glium/icon_loading"] \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 5399bafde..aebf639a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -152,7 +152,7 @@ pub mod prelude { }; pub use glium::glutin::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - VirtualKeyCode, ScanCode, + VirtualKeyCode, ScanCode, Icon, }; pub use rusttype::Font; pub use resources::AppResources; diff --git a/src/window.rs b/src/window.rs index f09031519..037d9b19f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -14,7 +14,7 @@ use glium::{ IncompatibleOpenGl, Display, debug::DebugCallbackBehavior, glutin::{self, EventsLoop, AvailableMonitorsIter, GlProfile, GlContext, GlWindow, CreationError, - MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder}, + MonitorId, EventsLoopProxy, ContextError, ContextBuilder, WindowBuilder, Icon}, backend::{Context, Facade, glutin::DisplayCreationError}, }; use gleam::gl::{self, Gl}; @@ -214,6 +214,14 @@ pub struct WindowCreateOptions { pub update_behaviour: UpdateBehaviour, /// Renderer type: Hardware-with-software-fallback, pure software or pure hardware renderer? pub renderer_type: RendererType, + /// Win32 menu callbacks + pub menu_callbacks: HashMap>, + /// Sets the window icon (Windows and Linux only). Usually 16x16 px or 32x32px + pub window_icon: Option, + /// Windows only: Sets the 256x256 taskbar icon during startup + pub taskbar_icon: Option, + /// Windows only: Sets `WS_EX_NOREDIRECTIONBITMAP` on the window + pub no_redirection_bitmap: bool, } impl Default for WindowCreateOptions { @@ -228,6 +236,10 @@ impl Default for WindowCreateOptions { mouse_mode: MouseMode::default(), update_behaviour: UpdateBehaviour::default(), renderer_type: RendererType::default(), + menu_callbacks: HashMap::new(), + window_icon: None, + taskbar_icon: None, + no_redirection_bitmap: false, } } } @@ -530,12 +542,11 @@ impl Window { options.state.size.hidpi_factor = hidpi_factor; let mut window = WindowBuilder::new() - .with_dimensions(options.state.size.dimensions) .with_title(options.state.title.clone()) + .with_maximized(options.state.is_maximized) .with_decorations(options.state.has_decorations) .with_visibility(options.state.is_visible) .with_transparency(options.state.is_transparent) - .with_maximized(options.state.is_maximized) .with_multitouch(); // TODO: Update winit to have: @@ -546,6 +557,24 @@ impl Window { // TODO: Add all the extensions for X11 / Mac / Windows, // like setting the taskbar icon, setting the titlebar icon, etc. + if let Some(icon) = options.window_icon { + window = window.with_window_icon(Some(icon)); + } + + #[cfg(target_os = "windows")] { + if let Some(icon) = options.taskbar_icon { + use glium::glutin::os::windows::WindowBuilderExt; + window = window.with_taskbar_icon(Some(icon)); + } + } + + #[cfg(target_os = "windows")] { + if options.no_redirection_bitmap { + use glium::glutin::os::windows::WindowBuilderExt; + window = window.with_no_redirection_bitmap(true); + } + } + if options.state.is_fullscreen { window = window.with_fullscreen(Some(monitor)); } @@ -594,6 +623,12 @@ impl Window { gl_window.window().set_position(pos); } + if options.state.is_maximized && !options.state.is_fullscreen { + gl_window.window().set_maximized(true); + } else if !options.state.is_fullscreen { + gl_window.window().set_inner_size(options.state.size.dimensions); + } + /*#[cfg(debug_assertions)] let display = Display::with_debug(gl_window, DebugCallbackBehavior::DebugMessageOnError)?; #[cfg(not(debug_assertions))]*/ From 489639343f5a60e71acaf3bcc261db6f28262ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 01:19:03 +0200 Subject: [PATCH 209/868] Fix compilation errors - T:Layout disappeared mysteriously For some reasom (probably due to a merge / rebase), all the T: Layout trait bounds on the WindowCreateOptions disappeared. --- src/app.rs | 2 +- src/window.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index aa92bbd36..f2127d0e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -143,7 +143,7 @@ impl App { /// Spawn a new window on the screen. If an application has no windows, /// the [`run`](#method.run) function will exit immediately. - pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { + pub fn create_window(&mut self, options: WindowCreateOptions, css: Css) -> Result<(), WindowCreateError> { let window = Window::new(options, css)?; self.app_state.windows.push(FakeWindow { state: window.state.clone(), diff --git a/src/window.rs b/src/window.rs index 037d9b19f..0a847ac98 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3,7 +3,8 @@ use std::{ time::Duration, fmt, - rc::Rc + rc::Rc, + collections::HashMap, }; use webrender::{ api::*, @@ -23,9 +24,8 @@ use cassowary::{ Variable, Solver, strength::*, }; - use { - dom::Texture, + dom::{Texture, Callback}, css::{Css, FakeCss}, window_state::{WindowState, MouseState, KeyboardState}, display_list::SolvedLayout, @@ -193,7 +193,7 @@ impl WindowEvent { /// Options on how to initially create the window #[derive(Debug, Clone)] -pub struct WindowCreateOptions { +pub struct WindowCreateOptions { /// State of the window, set the initial title / width / height here. pub state: WindowState, /// OpenGL clear color @@ -224,7 +224,7 @@ pub struct WindowCreateOptions { pub no_redirection_bitmap: bool, } -impl Default for WindowCreateOptions { +impl Default for WindowCreateOptions { fn default() -> Self { Self { state: WindowState::default(), @@ -529,7 +529,7 @@ pub(crate) struct WindowInternal { impl Window { /// Creates a new window - pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { + pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { let events_loop = EventsLoop::new(); From d4f0697a3aa63932986382e11aed3d499b02c4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 01:33:06 +0200 Subject: [PATCH 210/868] Fix appveyor CI: RUST_BACKTRACE=1 is not valid on Windows --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 89ed29ece..f315514ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,4 +45,4 @@ test_script: - cargo check --no-default-features - cargo check --no-default-features --features="svg" - cargo check --no-default-features --features="logging" - - RUST_BACKTRACE=1 cargo test --verbose --features "doc-test no-opengl-tests" + - cargo test --verbose --features "doc-test no-opengl-tests" From a6557bba8d8400f371e70032c489795d71dd202d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 02:21:58 +0200 Subject: [PATCH 211/868] Fixed lazy-font-loading The issue was not loading the font in .get_font_state() - this should maybe be solved a bit more cleanly. --- src/display_list.rs | 31 +++++++++++-------------------- src/resources.rs | 45 ++++++++++++++++++++++++++------------------- src/text_layout.rs | 5 ----- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 45d22d27a..f41e77203 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -205,7 +205,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let mut to_delete_fonts = Vec::<(FontId, Option<(FontKey, Vec)>)>::new(); for (key, value) in app_resources.font_data.borrow().iter() { - match &*value.2 { + match &*(*value.2).borrow() { FontState::ReadyForUpload(ref bytes) => { updated_fonts.push((key.clone(), bytes.clone())); }, @@ -233,13 +233,12 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { app_resources.font_data.borrow_mut().remove(&resource_key); } - use std::rc::Rc; - // Upload all remaining fonts to the GPU only if the haven't been uploaded yet for (resource_key, data) in updated_fonts.into_iter() { let key = api.generate_font_key(); resource_updates.push(ResourceUpdate::AddFont(AddFont::Raw(key, data, 0))); // TODO: use the index better? - app_resources.font_data.borrow_mut().get_mut(&resource_key).unwrap().2 = Rc::new(FontState::Uploaded(key)); + let mut borrow_mut = app_resources.font_data.borrow_mut(); + *borrow_mut.get_mut(&resource_key).unwrap().2.borrow_mut() = FontState::Uploaded(key); } } @@ -550,11 +549,8 @@ fn push_text( let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX as i32); - println!("text: {:?}, font_family.fonts: {:?}", text, font_family.fonts); let font_id = match font_family.fonts.get(0) { Some(s) => s, None => { error!("div @ {:?} has no font assigned!", bounds); return; }}; - println!("font_id: {:?}", font_id); let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); - println!("font result: {:?}", font_result); let font_instance_key = match font_result { Some(f) => f, @@ -891,25 +887,18 @@ fn push_font( { use font::FontState; - println!("in function push_font!, font_size_app_units: {:?}, MIN_AU: {:?}", font_size_app_units, MIN_AU); - if font_size_app_units < MIN_AU || font_size_app_units > MAX_AU { error!("warning: too big or too small font size"); return None; } - let font_state = match app_resources.font_data.borrow().get(font_id) { - Some(f) => f.2.clone(), - None => { - println!("returning none!"); return None; }, - }; + let font_state = app_resources.get_font_state(font_id)?; - // let font_state = app_resources.get_font_state(font_id)?; - println!("font_id: {:?}, font_state: {:?}", font_id, font_state); + let borrow = font_state.borrow(); - match *font_state { + match &*borrow { FontState::Uploaded(font_key) => { - let font_sizes_hashmap = app_resources.fonts.entry(font_key) + let font_sizes_hashmap = app_resources.fonts.entry(*font_key) .or_insert(FastHashMap::default()); let font_instance_key = font_sizes_hashmap.entry(font_size_app_units) .or_insert_with(|| { @@ -917,7 +906,7 @@ fn push_font( resource_updates.push(ResourceUpdate::AddFontInstance( AddFontInstance { key: f_instance_key, - font_key: font_key, + font_key: *font_key, glyph_size: font_size_app_units, options: None, platform_options: None, @@ -931,7 +920,9 @@ fn push_font( Some(*font_instance_key) }, _ => { - error!("warning: trying to use font {:?} that isn't available", font_id); + // This can happen when the font is loaded for the first time in `.get_font_state` + // TODO: Make a pre-pass that queries and uploads all non-available fonts + // error!("warning: trying to use font {:?} that isn't yet available", font_id); None }, } diff --git a/src/resources.rs b/src/resources.rs index 0edd97d8c..1d9d1c0c5 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -41,7 +41,7 @@ pub struct AppResources { // but we also need access to the font metrics. So we first parse the font // to make sure that nothing is going wrong. In the next draw call, we // upload the font and replace the FontState with the newly created font key - pub(crate) font_data: RefCell>, Rc>, Rc)>>, + pub(crate) font_data: RefCell>, Rc>, Rc>)>>, // After we've looked up the FontKey in the font_data map, we can then access // the font instance key (if there is any). If there is no font instance key, // we first need to create one. @@ -146,42 +146,48 @@ impl AppResources { let mut font_data = Vec::::new(); data.read_to_end(&mut font_data).map_err(|e| FontError::IoError(e))?; let (parsed_font, fd) = font::rusttype_load_font(font_data.clone(), None)?; - v.insert((Rc::new(parsed_font), Rc::new(fd), Rc::new(FontState::ReadyForUpload(font_data)))); + v.insert((Rc::new(parsed_font), Rc::new(fd), Rc::new(RefCell::new(FontState::ReadyForUpload(font_data))))); Ok(Some(())) }, } } - pub fn get_font(&self, id: &FontId) -> Option<(Rc>, Rc>)> { + /// Search for a builtin font on the users computer, validate and return it + fn get_builtin_font(id: String) -> Option<(::rusttype::Font<'static>, Vec, FontState)> + { + use font_loader::system_fonts::{self, FontPropertyBuilder}; + use font::rusttype_load_font; + + let (font_bytes, idx) = system_fonts::get(&FontPropertyBuilder::new().family(&id).build())?; + let (f, b) = rusttype_load_font(font_bytes.clone(), Some(idx)).ok()?; + Some((f, b, FontState::ReadyForUpload(font_bytes))) + } + + /// Internal API - we want the user to get the first two fields of the + fn get_font_internal(&self, id: &FontId) -> Option<(Rc>, Rc>, Rc>)> { match id { FontId::BuiltinFont(b) => { if self.font_data.borrow().get(id).is_none() { let (font, font_bytes, font_state) = Self::get_builtin_font(b.clone())?; - self.font_data.borrow_mut().insert(id.clone(), (Rc::new(font), Rc::new(font_bytes), Rc::new(font_state))); + self.font_data.borrow_mut().insert(id.clone(), (Rc::new(font), Rc::new(font_bytes), Rc::new(RefCell::new(font_state)))); } - self.font_data.borrow().get(id).and_then(|(font, bytes, _)| Some((font.clone(), bytes.clone()))) + self.font_data.borrow().get(id).and_then(|(font, bytes, state)| Some((font.clone(), bytes.clone(), state.clone()))) }, FontId::ExternalFont(_) => { // For external fonts, we assume that the application programmer has // already loaded them, so we don't try to fallback to system fonts. - self.font_data.borrow().get(id).and_then(|(font, bytes, _)| Some((font.clone(), bytes.clone()))) + self.font_data.borrow().get(id).and_then(|(font, bytes, state)| Some((font.clone(), bytes.clone(), state.clone()))) }, } } - /// Search for a builtin font on the computer and and insert it - fn get_builtin_font(id: String) -> Option<(::rusttype::Font<'static>, Vec, FontState)> - { - use font_loader::system_fonts::{self, FontPropertyBuilder}; - use font::rusttype_load_font; - - let (font_bytes, idx) = system_fonts::get(&FontPropertyBuilder::new().family(&id).build())?; - let (f, b) = rusttype_load_font(font_bytes.clone(), Some(idx)).ok()?; - Some((f, b, FontState::ReadyForUpload(font_bytes))) + pub fn get_font(&self, id: &FontId) -> Option<(Rc>, Rc>)> { + self.get_font_internal(id).and_then(|(font, bytes, _)| Some((font, bytes))) } - pub(crate) fn get_font_state(&self, id: &FontId) -> Option> { - self.font_data.borrow().get(id).and_then(|(_, _, font_state)| Some(font_state.clone())) + /// Note the pub(crate) here: We don't want to expose the FontState in the public API + pub(crate) fn get_font_state(&self, id: &FontId) -> Option>> { + self.get_font_internal(id).and_then(|(_, _, state)| Some(state)) } /// Checks if a font is currently registered and ready-to-use @@ -202,13 +208,14 @@ impl AppResources { match self.font_data.borrow().get(&id) { None => return None, - Some(v) => match &*v.2 { + Some(v) => match *(*v.2).borrow() { FontState::Uploaded(font_key) => { to_delete_font_key = Some(font_key.clone()); }, _ => { }, } } - self.font_data.borrow_mut().get_mut(&id).unwrap().2 = Rc::new(FontState::AboutToBeDeleted(to_delete_font_key)); + let mut borrow_mut = self.font_data.borrow_mut(); + *borrow_mut.get_mut(&id).unwrap().2.borrow_mut() = FontState::AboutToBeDeleted(to_delete_font_key); Some(()) } diff --git a/src/text_layout.rs b/src/text_layout.rs index fb62a6206..69368f99d 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -183,16 +183,11 @@ pub(crate) fn get_glyphs( scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) { - println!("target_font_id: {:?}", target_font_id); - println!("loaded fonts (before .get_font() call): {:?}", app_resources.get_loaded_fonts()); - let target_font = match app_resources.get_font(target_font_id) { Some(s) => s, None => panic!("Drawing with invalid font!: {:?}", target_font_id), }; - println!("loaded fonts (after .get_font() call): {:?}", app_resources.get_loaded_fonts()); - let font_metrics = calculate_font_metrics(&target_font.0, target_font_size, line_height); // (1) Split the text into semantic items (word, tab or newline) OR get the cached From 41a60850be955f07b5ea65147a658fcebd632ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 02:43:56 +0200 Subject: [PATCH 212/868] Updated VectorizedFont to also use lazy-loading of fonts --- src/widgets/svg.rs | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index e61927943..a4031bf7a 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1263,35 +1263,36 @@ fn glyph_to_svg_layer_type<'a>(glyph: Glyph<'a>) -> Option { #[derive(Debug)] pub struct VectorizedFontCache { /// Font -> Vectorized glyph map - vectorized_fonts: FastHashMap, + /// + /// Needs to be wrapped in a RefCell / Rc since we want to lazy-load the + /// fonts to keep the memory usage down + vectorized_fonts: RefCell>>, } impl VectorizedFontCache { - pub fn new(app_resources: &AppResources) -> Self { - let mut fonts = FastHashMap::default(); - for font_id in app_resources.get_loaded_fonts() { - fonts.entry(font_id.clone()).or_insert_with(|| VectorizedFont::from_font(&*app_resources.get_font(&font_id).unwrap().0)); - } + pub fn new() -> Self { Self { - vectorized_fonts: fonts, + vectorized_fonts: RefCell::new(FastHashMap::default()), } } pub fn insert_if_not_exist(&mut self, id: FontId, font: &Font) { - self.vectorized_fonts.entry(id).or_insert_with(|| VectorizedFont::from_font(font)); + self.vectorized_fonts.borrow_mut().entry(id).or_insert_with(|| Rc::new(VectorizedFont::from_font(font))); } pub fn insert(&mut self, id: FontId, font: VectorizedFont) { - self.vectorized_fonts.insert(id, font); + self.vectorized_fonts.borrow_mut().insert(id, Rc::new(font)); } - pub fn get_font(&self, id: &FontId) -> Option<&VectorizedFont> { - self.vectorized_fonts.get(id) + pub fn get_font(&self, id: &FontId, app_resources: &AppResources) -> Option> { + self.vectorized_fonts.borrow_mut().entry(id.clone()) + .or_insert_with(|| Rc::new(VectorizedFont::from_font(&*app_resources.get_font(&id).unwrap().0))); + self.vectorized_fonts.borrow().get(&id).and_then(|font| Some(font.clone())) } pub fn remove_font(&mut self, id: &FontId) { - self.vectorized_fonts.remove(id); + self.vectorized_fonts.borrow_mut().remove(id); } } @@ -1921,17 +1922,17 @@ impl SvgText { -> SvgLayerResource { let font = resources.get_font(&self.font_id).unwrap().0; - let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id).unwrap(); + let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id, resources).unwrap(); match self.placement { SvgTextPlacement::Unmodified => { - normal_text(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size) + normal_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size) }, SvgTextPlacement::Rotated(degrees) => { - rotated_text(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size, degrees) + rotated_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, degrees) }, SvgTextPlacement::OnCubicBezierCurve(curve) => { - text_on_curve(&self.text_layout.0, &self.position, self.style, &font, vectorized_font, &self.font_size, &curve) + text_on_curve(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, &curve) } } } @@ -2291,19 +2292,19 @@ fn draw_vertex_buffer_to_surface( /// import the entire crate just for one function (due to added compile time) /// /// The MIT License (MIT) -/// +/// /// Copyright (c) 2015 Erik Hedvall -/// +/// /// Permission is hereby granted, free of charge, to any person obtaining a copy /// of this software and associated documentation files (the "Software"), to deal /// in the Software without restriction, including without limitation the rights /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell /// copies of the Software, and to permit persons to whom the Software is /// furnished to do so, subject to the following conditions: -/// +/// /// The above copyright notice and this permission notice shall be included in all /// copies or substantial portions of the Software. -/// +/// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE From 526eff80e398345aaa9191b3735fb3b7499c27e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 03:03:43 +0200 Subject: [PATCH 213/868] Fixed examples to use new VectorizedFontCache API --- examples/debug.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/debug.rs b/examples/debug.rs index 9601c532c..5def79890 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -179,7 +179,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv app_state.data.modify(|data| data.map = Some(Map { cache: svg_cache, - font_cache: VectorizedFontCache::new(&app_state.resources), + font_cache: VectorizedFontCache::new(), hovered_text: None, texts: cached_texts, layers: svg_layers, From 054ff69e57d70c3e80f1bfa4043adf79f21ad5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 20 Aug 2018 18:44:18 +0200 Subject: [PATCH 214/868] Added optional serde serialization for CssColor --- Cargo.toml | 6 +++++- src/css_parser.rs | 24 ++++++++++++++++++++++++ src/lib.rs | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1be882014..0b834964e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ fern = { version = "0.5.5", optional = true } backtrace = { version = "0.3.9", optional = true } lyon = { version = "0.10.0", features = ["extra"], optional = true } # harfbuzz_rs = "0.1.0" +serde = { version = "1.0.*", optional = true } [target.'cfg(not(target_os = "linux"))'.dependencies] nfd = "0.0.4" @@ -77,4 +78,7 @@ svg = ["lyon", "usvg"] # RGBA bytes or use the icon_loading feature to decode it from a PNG / JPG / # whatever image format on startup. Note that this will import the image # dependency and use a bit of extra runtime. -icon_loading = ["glium/icon_loading"] \ No newline at end of file +icon_loading = ["glium/icon_loading"] + +# For serializing / deserializing CSS colors using serde +serde_serialization = ["serde"] \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index ff89ec0d4..ef82e92ba 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1856,6 +1856,30 @@ impl Into for CssColor { } } +#[cfg(feature = "serde_serialization")] +use serde::{de, Serialize, Deserialize, Serializer, Deserializer}; + +#[cfg(feature = "serde_serialization")] +impl Serialize for CssColor { + fn serialize(&self, serializer: S) -> Result + where S: Serializer, + { + let prefix_css_color_with_hash = true; + serializer.serialize_str(&self.to_string(prefix_css_color_with_hash)) + } +} + +#[cfg(feature = "serde_serialization")] +impl<'de> Deserialize<'de> for CssColor { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + CssColor::from_str(&s).map_err(de::Error::custom) + } +} + + #[cfg(test)] mod css_tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index aebf639a0..60d991c85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,8 @@ extern crate fern; #[cfg(feature = "logging")] extern crate backtrace; extern crate image; +#[cfg(feature = "serde_serialization")] +extern crate serde; #[cfg(not(target_os = "linux"))] extern crate nfd; From 3ebffadac2c41ba3ad9c1ac5d7f322a714b8be7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 21 Aug 2018 22:52:20 +0200 Subject: [PATCH 215/868] Make VectorizedFont Send, so you can add fonts from non-main threads --- src/app.rs | 12 ++- src/cache.rs | 1 - src/dialogs.rs | 15 ++-- src/display_list.rs | 1 - src/font.rs | 3 +- src/images.rs | 14 ++-- src/resources.rs | 28 ++++--- src/task.rs | 4 +- src/widgets/svg.rs | 186 +++++++++++++++++++++++--------------------- 9 files changed, 140 insertions(+), 124 deletions(-) diff --git a/src/app.rs b/src/app.rs index f2127d0e7..ebbc50b4f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,10 +3,14 @@ use std::{ io::Read, sync::{Arc, Mutex, PoisonError}, }; -use glium::{SwapBuffersError, glutin::Event}; -use glium::glutin::dpi::{LogicalPosition, LogicalSize}; -use webrender::api::{HitTestFlags, DevicePixel}; -use webrender::PipelineInfo; +use glium::{ + SwapBuffersError, + glutin::{ + Event, + dpi::{LogicalPosition, LogicalSize} + }, +}; +use webrender::{PipelineInfo, api::{HitTestFlags, DevicePixel}}; use image::ImageError; use euclid::TypedSize2D; #[cfg(feature = "logging")] diff --git a/src/cache.rs b/src/cache.rs index a46b1cb78..0fbd7fc80 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -40,7 +40,6 @@ use std::{ collections::BTreeMap, }; use cassowary::Solver; - use { constraints::DisplayRect, id_tree::{NodeId, Arena}, diff --git a/src/dialogs.rs b/src/dialogs.rs index 7823a33b1..5b258b1ea 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -1,5 +1,6 @@ -pub use tinyfiledialogs::MessageBoxIcon; -pub use tinyfiledialogs::DefaultColorValue; +//! Dialogs (open folder, open file), message boxes and native color pickers + +pub use tinyfiledialogs::{MessageBoxIcon, DefaultColorValue}; /// Ok or cancel result, returned from the `msg_box_ok_cancel` function #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] @@ -136,9 +137,9 @@ pub fn color_picker_dialog(title: &str, default_value: Option) // The difference between tinyfiledialogs and nfd is that nfd links // to a specific dialog at compile time, while tinyfiledialogs selects -// the dialog at runtime from a set of specific dialogs (i.e. Mate, KDE, +// the dialog at runtime from a set of specific dialogs (i.e. Mate, KDE, // dolphin, whatever). This (a) doesn't force the library user to choose -// a specific dialog, (b) won't look non-native (GTK3 on a KDE env can +// a specific dialog, (b) won't look non-native (GTK3 on a KDE env can // look jarring) and (c) doesn't require the user to install extra libraries // // The only reason why we don't use tinyfiledialogs everywhere is because @@ -173,7 +174,7 @@ pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]> /// "doc" and "docx" files #[cfg(target_os = "linux")] pub fn open_file_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) --> Option +-> Option { let filter_list = filter_list.map(|f| (f, "")); let path = default_path.unwrap_or(""); @@ -196,7 +197,7 @@ pub fn open_directory_dialog(default_path: Option<&str>) /// Open a directory, returns `None` if the user canceled the dialog #[cfg(target_os = "linux")] pub fn open_directory_dialog(default_path: Option<&str>) --> Option +-> Option { ::tinyfiledialogs::select_folder_dialog("Open Filder", default_path.unwrap_or("")) } @@ -229,7 +230,7 @@ pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Optio /// "doc" and "docx" files #[cfg(target_os = "linux")] pub fn open_multiple_files_dialog(default_path: Option<&str>, filter_list: Option<&[&str]>) --> Option> +-> Option> { let filter_list = filter_list.map(|f| (f, "")); let path = default_path.unwrap_or(""); diff --git a/src/display_list.rs b/src/display_list.rs index f41e77203..3874f01f5 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -5,7 +5,6 @@ use webrender::api::*; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; use cassowary::Constraint; - use { FastHashMap, resources::AppResources, diff --git a/src/font.rs b/src/font.rs index f6ab159af..068480736 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,7 +1,6 @@ //! Module for loading and handling fonts use webrender::api::FontKey; -use rusttype::{Font, FontCollection}; -use rusttype::Error as RusttypeError; +use rusttype::{Error as RusttypeError, Font, FontCollection}; #[derive(Debug, Clone)] pub(crate) enum FontState { diff --git a/src/images.rs b/src/images.rs index 697832f52..fb1e63a66 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,10 +1,14 @@ //! Module for loading and handling images use std::sync::atomic::{AtomicUsize, Ordering}; -use webrender::api::ImageFormat as WebrenderImageFormat; -use image::{ImageResult, ImageFormat, guess_format}; -use image::{self, ImageError, DynamicImage, GenericImage}; -use webrender::api::{ImageData, ImageDescriptor, ImageKey}; +use webrender::api::{ + ImageFormat as WebrenderImageFormat, + ImageData, ImageDescriptor, ImageKey +}; +use image::{ + self, ImageResult, ImageFormat, + ImageError, DynamicImage, GenericImage, +}; static IMAGE_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -68,7 +72,7 @@ impl ImageType { Tiff => Ok(ImageFormat::TIFF), WebP => Ok(ImageFormat::WEBP), GuessImageFormat => { - guess_format(data) + image::guess_format(data) } } } diff --git a/src/resources.rs b/src/resources.rs index 1d9d1c0c5..5826573fc 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,21 +1,25 @@ -use std::io::Read; -use std::collections::hash_map::Entry::*; -use text_layout::{PX_TO_PT, split_text_into_words}; -use text_cache::{TextId, TextCache}; +use std::{ + io::Read, + rc::Rc, + cell::RefCell, + collections::hash_map::Entry::*, +}; use webrender::api::{FontKey, FontInstanceKey}; -use FastHashMap; -use font::{FontState, FontError}; use image::{self, ImageError}; -use images::{ImageId, ImageState, ImageType}; +use FastHashMap; use app_units::Au; use clipboard2::{Clipboard, ClipboardError, SystemClipboard}; use rusttype::Font; -use css_parser::{ - FontSize, - FontId::{self, ExternalFont} +use { + text_layout::{PX_TO_PT, split_text_into_words}, + text_cache::{TextId, TextCache}, + font::{FontState, FontError}, + images::{ImageId, ImageState, ImageType}, + css_parser::{ + FontSize, + FontId::{self, ExternalFont} + }, }; -use std::rc::Rc; -use std::cell::RefCell; /// Font and image keys /// diff --git a/src/task.rs b/src/task.rs index 922d33cbc..42e56983b 100644 --- a/src/task.rs +++ b/src/task.rs @@ -4,9 +4,7 @@ use std::{ sync::{Arc, Mutex, Weak}, thread::{spawn, JoinHandle}, }; -use { - traits::Layout, -}; +use traits::Layout; /// Should a daemon terminate or not - used to remove active daemons #[derive(Debug, Copy, Clone, PartialEq, Eq)] diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index a4031bf7a..d2965b287 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -2,7 +2,7 @@ use std::{ fmt, rc::Rc, io::{Error as IoError}, - sync::{Mutex, atomic::{Ordering, AtomicUsize}}, + sync::{Arc, Mutex, atomic::{Ordering, AtomicUsize}}, cell::{RefCell, RefMut}, hash::{Hash, Hasher}, collections::hash_map::Entry::*, @@ -1162,9 +1162,9 @@ pub struct SvgWorldPixel; #[derive(Debug, Clone)] pub struct VectorizedFont { /// Glyph -> Polygon map - glyph_polygon_map: Rc>>>, + glyph_polygon_map: Arc>>>, /// Glyph -> Stroke map - glyph_stroke_map: Rc>>>, + glyph_stroke_map: Arc>>>, } impl VectorizedFont { @@ -1206,47 +1206,63 @@ impl VectorizedFont { } Self { - glyph_polygon_map: Rc::new(RefCell::new(FastHashMap::default())), - glyph_stroke_map: Rc::new(RefCell::new(FastHashMap::default())), + glyph_polygon_map: Arc::new(Mutex::new(FastHashMap::default())), + glyph_stroke_map: Arc::new(Mutex::new(FastHashMap::default())), } } } -pub fn get_fill_vertices(vectorized_font: &VectorizedFont, original_font: &Font, id: &GlyphId) --> Option> +/// Note: Since `VectorizedFont` has to lock access on this, you'll want to get the +/// fill vertices for all the characters at once +pub fn get_fill_vertices(vectorized_font: &VectorizedFont, original_font: &Font, ids: &[GlyphInstance]) +-> Vec> { let svg_stroke_opts = Some(SvgStrokeOptions::default()); - match vectorized_font.glyph_polygon_map.borrow_mut().entry(*id) { - Occupied(o) => Some(o.get().clone()), - Vacant(v) => { - let g = original_font.glyph(*id); - let poly = glyph_to_svg_layer_type(g)?; - let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); - v.insert(polygon_verts.clone()); - vectorized_font.glyph_stroke_map.borrow_mut().insert(*id, stroke_verts.unwrap()); - Some(polygon_verts) + let mut glyph_stroke_lock = vectorized_font.glyph_stroke_map.lock().unwrap(); + let mut glyph_polygon_lock = vectorized_font.glyph_polygon_map.lock().unwrap(); + + ids.iter().filter_map(|id| { + let id = GlyphId(id.index); + match glyph_polygon_lock.entry(id) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let g = original_font.glyph(id); + let poly = glyph_to_svg_layer_type(g)?; + let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); + v.insert(polygon_verts.clone()); + glyph_stroke_lock.insert(id, stroke_verts.unwrap()); + Some(polygon_verts) + } } - } + }).collect() } -pub fn get_stroke_vertices(vectorized_font: &VectorizedFont, original_font: &Font, id: &GlyphId) --> Option> +/// Note: Since `VectorizedFont` has to lock access on this, you'll want to get the +/// stroke vertices for all the characters at once +pub fn get_stroke_vertices(vectorized_font: &VectorizedFont, original_font: &Font, ids: &[GlyphInstance]) +-> Vec> { let svg_stroke_opts = Some(SvgStrokeOptions::default()); - match vectorized_font.glyph_stroke_map.borrow_mut().entry(*id) { - Occupied(o) => Some(o.get().clone()), - Vacant(v) => { - let g = original_font.glyph(*id); - let poly = glyph_to_svg_layer_type(g)?; - let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); - let stroke_verts = stroke_verts.unwrap(); - v.insert(stroke_verts.clone()); - vectorized_font.glyph_polygon_map.borrow_mut().insert(*id, polygon_verts); - Some(stroke_verts) + let mut glyph_stroke_lock = vectorized_font.glyph_stroke_map.lock().unwrap(); + let mut glyph_polygon_lock = vectorized_font.glyph_polygon_map.lock().unwrap(); + + ids.iter().filter_map(|id| { + let id = GlyphId(id.index); + match glyph_stroke_lock.entry(id) { + Occupied(o) => Some(o.get().clone()), + Vacant(v) => { + let g = original_font.glyph(id); + let poly = glyph_to_svg_layer_type(g)?; + let (polygon_verts, stroke_verts) = poly.tesselate(DEFAULT_GLYPH_TOLERANCE, svg_stroke_opts); + let stroke_verts = stroke_verts.unwrap(); + v.insert(stroke_verts.clone()); + glyph_polygon_lock.insert(id, polygon_verts); + Some(stroke_verts) + } } - } + }).collect() } /// Converts a glyph to a `SvgLayerType::Polygon` @@ -1266,33 +1282,39 @@ pub struct VectorizedFontCache { /// /// Needs to be wrapped in a RefCell / Rc since we want to lazy-load the /// fonts to keep the memory usage down - vectorized_fonts: RefCell>>, + vectorized_fonts: Mutex>>, +} + +#[test] +fn test_vectorized_font_cache_is_send() { + fn is_send() {} + is_send::(); } impl VectorizedFontCache { pub fn new() -> Self { Self { - vectorized_fonts: RefCell::new(FastHashMap::default()), + vectorized_fonts: Mutex::new(FastHashMap::default()), } } pub fn insert_if_not_exist(&mut self, id: FontId, font: &Font) { - self.vectorized_fonts.borrow_mut().entry(id).or_insert_with(|| Rc::new(VectorizedFont::from_font(font))); + self.vectorized_fonts.lock().unwrap().entry(id).or_insert_with(|| Arc::new(VectorizedFont::from_font(font))); } pub fn insert(&mut self, id: FontId, font: VectorizedFont) { - self.vectorized_fonts.borrow_mut().insert(id, Rc::new(font)); + self.vectorized_fonts.lock().unwrap().insert(id, Arc::new(font)); } - pub fn get_font(&self, id: &FontId, app_resources: &AppResources) -> Option> { - self.vectorized_fonts.borrow_mut().entry(id.clone()) - .or_insert_with(|| Rc::new(VectorizedFont::from_font(&*app_resources.get_font(&id).unwrap().0))); - self.vectorized_fonts.borrow().get(&id).and_then(|font| Some(font.clone())) + pub fn get_font(&self, id: &FontId, app_resources: &AppResources) -> Option> { + self.vectorized_fonts.lock().unwrap().entry(id.clone()) + .or_insert_with(|| Arc::new(VectorizedFont::from_font(&*app_resources.get_font(&id).unwrap().0))); + self.vectorized_fonts.lock().unwrap().get(&id).and_then(|font| Some(font.clone())) } pub fn remove_font(&mut self, id: &FontId) { - self.vectorized_fonts.borrow_mut().remove(id); + self.vectorized_fonts.lock().unwrap().remove(id); } } @@ -1986,19 +2008,17 @@ fn normal_text_to_vertices( glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, - transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> + transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { - let fill_buf = glyph_ids.iter() - .filter_map(|gid| transform_func(vectorized_font, original_font, &GlyphId(gid.index)).and_then(|vbuf| Some((vbuf, gid)))) - .map(|(mut vertex_buf, gid)| { - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0 + position.x, gid.point.y + position.y); - vertex_buf - }) - .collect::>(); + let mut vertex_buffers = transform_func(vectorized_font, original_font, glyph_ids); - join_vertex_buffers(&fill_buf) + vertex_buffers.iter_mut().zip(glyph_ids).for_each(|(vertex_buf, gid)| { + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0 + position.x, gid.point.y + position.y); + }); + + join_vertex_buffers(&vertex_buffers) } fn rotated_text( @@ -2033,23 +2053,22 @@ fn rotated_text_to_vertices( vectorized_font: &VectorizedFont, original_font: &Font, rotation_degrees: f32, - transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> + transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { let rotation_rad = rotation_degrees.to_radians(); let (char_sin, char_cos) = (rotation_rad.sin(), rotation_rad.cos()); - let fill_buf = glyph_ids.iter() - .filter_map(|gid| transform_func(vectorized_font, original_font, &GlyphId(gid.index)).and_then(|vbuf| Some((vbuf, gid)))) - .map(|(mut vertex_buf, gid)| { - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); - rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); - transform_vertex_buffer(&mut vertex_buf.vertices, position.x, position.y); - vertex_buf - }) - .collect::>(); - join_vertex_buffers(&fill_buf) + let mut vertex_buffers = transform_func(vectorized_font, original_font, glyph_ids); + + vertex_buffers.iter_mut().zip(glyph_ids).for_each(|(vertex_buf, gid)| { + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + transform_vertex_buffer(&mut vertex_buf.vertices, position.x, position.y); + }); + + join_vertex_buffers(&vertex_buffers) } fn text_on_curve( @@ -2088,37 +2107,26 @@ fn curved_vector_text_to_vertices( original_font: &Font, char_offsets: &[(f32, f32)], char_rotations: &[BezierCharacterRotation], - transform_func: fn(&VectorizedFont, &Font, &GlyphId) -> Option> + transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { - let fill_buf = glyph_ids.iter() - .filter_map(|gid| { - // 1. Transform glyph to vertex buffer && filter out all glyphs - // that don't have a vertex buffer - transform_func(vectorized_font, original_font, &GlyphId(gid.index)) - }) - .zip(char_rotations.into_iter()) - .zip(char_offsets.iter()) - .map(|((mut vertex_buf, char_rot), char_offset)| { - - let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue - - // 2. Scale characters to the final size - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - - // 3. Rotate individual characters inside of the word - let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); - - rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); - - // 4. Transform characters to their respective positions - transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x + position.x, *char_offset_y + position.y); - - vertex_buf - }) - .collect::>(); + let mut vertex_buffers = transform_func(vectorized_font, original_font, glyph_ids); + + vertex_buffers.iter_mut() + .zip(char_rotations.into_iter()) + .zip(char_offsets.iter()) + .for_each(|((vertex_buf, char_rot), char_offset)| { + let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue + // 2. Scale characters to the final size + scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + // 3. Rotate individual characters inside of the word + let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); + rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); + // 4. Transform characters to their respective positions + transform_vertex_buffer(&mut vertex_buf.vertices, *char_offset_x + position.x, *char_offset_y + position.y); + }); - join_vertex_buffers(&fill_buf) + join_vertex_buffers(&vertex_buffers) } impl Svg { From d58f25a0ed41066a417dfd98817b5b74e9992d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 21 Aug 2018 23:43:09 +0200 Subject: [PATCH 216/868] Added custom_task for !Send data models Also added the ability to start daemons after task ends, so that you can chain multiple tasks together. --- examples/async.rs | 8 +++---- src/app.rs | 23 ++++++++++++++---- src/app_state.rs | 60 ++++++++++++++++++++++++++++++++++++++++------- src/task.rs | 28 +++++++++++++++------- 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index 51d0253af..f9ed438e8 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -1,12 +1,12 @@ extern crate azul; use azul::{ - prelude::*, + prelude::*, widgets::*, }; use std::{ - thread, - time::{Duration, Instant}, + thread, + time::{Duration, Instant}, sync::{Arc, Mutex}, }; @@ -64,7 +64,7 @@ fn reset_connection(app_state: &mut AppState, _event: WindowEvent) fn start_connection(app_state: &mut AppState, _event: WindowEvent) -> UpdateScreen { let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); app_state.data.modify(|state| state.connection_status = status); - app_state.add_task(connect_to_db_async); + app_state.add_task(connect_to_db_async, &[]); app_state.add_daemon(timer_daemon); UpdateScreen::Redraw } diff --git a/src/app.rs b/src/app.rs index ebbc50b4f..dc35372e8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,7 @@ use { traits::Layout, ui_state::UiState, ui_description::UiDescription, - task::TerminateDaemon, + app_state::Daemon, }; /// Graphical application that maintains some kind of application state @@ -435,7 +435,7 @@ impl App { /// Create a daemon. Does nothing if a daemon with the function pointer location already exists. /// /// If the daemon was inserted, returns true, otherwise false - pub fn add_daemon(&mut self, daemon: fn(&mut T) -> (UpdateScreen, TerminateDaemon)) + pub fn add_daemon(&mut self, daemon: Daemon) -> bool { self.app_state.add_daemon(daemon) @@ -503,13 +503,26 @@ impl App { // let ui_state_cache = Self::initialize_ui_state(&self.windows, &self.app_state); // Self::do_first_redraw(&mut self.windows, &mut self.app_state, &ui_state_cache); } + + /// See `AppState::add_custom_task`. + pub fn add_custom_task( + &mut self, + data: &Arc>, + callback: fn(Arc>, Arc<()>), + after_completion_deamons: &[Daemon]) + { + self.app_state.add_custom_task(data, callback, after_completion_deamons); + } } impl App { - /// Tasks, once started, cannot be stopped, which is why there is no `.delete()` function - pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) + /// See `AppState::add_ask`. + pub fn add_task( + &mut self, + callback: fn(Arc>, Arc<()>), + after_completion_callbacks: &[Daemon]) { - self.app_state.add_task(callback); + self.app_state.add_task(callback, after_completion_callbacks); } } diff --git a/src/app_state.rs b/src/app_state.rs index e8ab52269..265c08440 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -21,6 +21,8 @@ use { task::TerminateDaemon, }; +pub type Daemon = fn(&mut T) -> (UpdateScreen, TerminateDaemon); + /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application /// should be laid out) @@ -41,7 +43,7 @@ pub struct AppState { /// Currently running daemons (polling functions) pub(crate) daemons: FastHashMap (UpdateScreen, TerminateDaemon)>, /// Currently running tasks (asynchronous functions running on a different thread) - pub(crate) tasks: Vec, + pub(crate) tasks: Vec>, } impl AppState { @@ -187,7 +189,7 @@ impl AppState { /// Create a daemon. Does nothing if a daemon already exists. /// /// If the daemon was inserted, returns true, otherwise false - pub fn add_daemon(&mut self, daemon: fn(&mut T) -> (UpdateScreen, TerminateDaemon)) -> bool { + pub fn add_daemon(&mut self, daemon: Daemon) -> bool { match self.daemons.entry(daemon as usize) { Occupied(_) => false, Vacant(v) => { v.insert(daemon); true }, @@ -229,12 +231,28 @@ impl AppState { -> UpdateScreen { let old_count = self.tasks.len(); - self.tasks.retain(|x| !x.is_finished()); + let mut daemons_to_add = Vec::new(); + self.tasks.retain(|task| { + if !task.is_finished() { + true + } else { + daemons_to_add.extend(task.after_completion_daemons.iter().cloned()); + false + } + }); + + let daemons_is_empty = daemons_to_add.is_empty(); let new_count = self.tasks.len(); - if old_count != new_count { - UpdateScreen::Redraw - } else { + + // Start all the daemons that should run after the completion of the task + for daemon in daemons_to_add { + self.add_daemon(daemon); + } + + if old_count == new_count && daemons_is_empty { UpdateScreen::DontRedraw + } else { + UpdateScreen::Redraw } } @@ -272,13 +290,37 @@ impl AppState { { self.resources.set_clipboard_string(contents) } + + /// Custom tasks can be used when the `AppState` isn't `Send`. For example + /// `SvgCache` isn't thread-safe, since it has to interact with OpenGL, so + /// it can't be sent to other threads safely. + /// + /// What you can do instead, is take a part of your application data, wrap + /// that in an `Arc>` and push a task that takes it onto the queue. + /// This way you can modify a part of the application state on a different + /// thread, while not requiring that everything is thread-safe. + /// + /// While you can't modify the `SvgCache` from a different thread, you can + /// modify other things in the `AppState` and leave the SVG cache alone. + pub fn add_custom_task( + &mut self, + data: &Arc>, + callback: fn(Arc>, Arc<()>), + after_completion_deamons: &[Daemon]) + { + let task = Task::new(data, callback).then(after_completion_deamons); + self.tasks.push(task); + } } impl AppState { - /// Tasks, once started, cannot be stopped - pub fn add_task(&mut self, callback: fn(Arc>, Arc<()>)) + /// Add a task that has access to the entire `AppState`. + pub fn add_task( + &mut self, + callback: fn(Arc>, Arc<()>), + after_completion_deamons: &[Daemon]) { - let task = Task::new(&self.data, callback); + let task = Task::new(&self.data, callback).then(after_completion_deamons); self.tasks.push(task); } } diff --git a/src/task.rs b/src/task.rs index 42e56983b..1ff95270d 100644 --- a/src/task.rs +++ b/src/task.rs @@ -4,7 +4,10 @@ use std::{ sync::{Arc, Mutex, Weak}, thread::{spawn, JoinHandle}, }; -use traits::Layout; +use { + app_state::Daemon, + traits::Layout, +}; /// Should a daemon terminate or not - used to remove active daemons #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -13,18 +16,20 @@ pub enum TerminateDaemon { Continue, } -pub struct Task { +pub struct Task { // Task is in progress join_handle: Option>, dropcheck: Weak<()>, + /// Deamons that run directly after completion of this task + pub(crate) after_completion_daemons: Vec> } -impl Task { - pub fn new( - app_state: &Arc>, - callback: fn(Arc>, Arc<()>)) +impl Task { + pub fn new( + app_state: &Arc>, + callback: fn(Arc>, Arc<()>)) -> Self - where T: Layout + Send + 'static + where U: Send + 'static { let thread_check = Arc::new(()); let thread_weak = Arc::downgrade(&thread_check); @@ -37,6 +42,7 @@ impl Task { Self { join_handle: Some(thread_handle), dropcheck: thread_weak, + after_completion_daemons: Vec::new(), } } @@ -44,9 +50,15 @@ impl Task { pub fn is_finished(&self) -> bool { self.dropcheck.upgrade().is_none() } + + #[inline] + pub fn then(mut self, deamons: &[Daemon]) -> Self { + self.after_completion_daemons.extend(deamons.iter().cloned()); + self + } } -impl Drop for Task { +impl Drop for Task { fn drop(&mut self) { if let Some(thread_handle) = self.join_handle.take() { let _ = thread_handle.join().unwrap(); From dbb661c35eb7f716ce5591bfffabc7deac6a8994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 22 Aug 2018 00:19:05 +0200 Subject: [PATCH 217/868] Relaxed trait bounds on Modify trait --- src/lib.rs | 2 +- src/traits.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 60d991c85..1022f90ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub mod prelude { pub use app_state::AppState; pub use css::{Css, FakeCss}; pub use dom::{Dom, NodeType, NodeData, Callback, On, UpdateScreen}; - pub use traits::{Layout, ModifyAppState}; + pub use traits::{Layout, Modify}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, WindowMonitorTarget, RendererType, WindowEvent, WindowInfo, ReadOnlyWindow}; diff --git a/src/traits.rs b/src/traits.rs index 8ff3ce233..ff4ec1943 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -61,7 +61,7 @@ pub trait IntoParsedCssProperty<'a> { } /// Convenience trait that allows the `app_state.modify()` - only implemented for -/// `Arc` - shortly locks the app state mutex, modifies it and unlocks +/// `Arc` - shortly locks the app state mutex, modifies it and unlocks /// it again. /// /// Note: Usually when doing asynchronous programming you don't want to block the main @@ -70,13 +70,13 @@ pub trait IntoParsedCssProperty<'a> { /// to the apps data. In order to make your app performant, don't do heavy computations /// inside the closure, only use it to write or copy data in and out of the application /// state. -pub trait ModifyAppState { +pub trait Modify { /// Modifies the app state and then returns if the modification was successful /// Takes a FnMut that modifies the state fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T); } -impl ModifyAppState for Arc> { +impl Modify for Arc> { fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T) { match self.lock().as_mut() { Ok(lock) => { closure(&mut *lock); true }, From 9fb3e0ed78d11221f24fcf960ce87a75559a3517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 22 Aug 2018 00:30:41 +0200 Subject: [PATCH 218/868] Implement Modify for all Arc> --- src/traits.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traits.rs b/src/traits.rs index ff4ec1943..02b44c2b5 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -76,7 +76,7 @@ pub trait Modify { fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T); } -impl Modify for Arc> { +impl Modify for Arc> { fn modify(&self, closure: F) -> bool where F: FnOnce(&mut T) { match self.lock().as_mut() { Ok(lock) => { closure(&mut *lock); true }, From 759e308ddd52af42da2d4391e6c66858f81238ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 22 Aug 2018 07:24:44 +0200 Subject: [PATCH 219/868] Implemented Debug for SvgCallbacks --- src/widgets/svg.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index d2965b287..f0d504e6b 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -894,6 +894,17 @@ impl SvgLayer { } } +impl fmt::Debug for SvgLayer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SvgLayer {{ data: {:?}, callbacks: {:?}, style: {:?}, transform_id: {:?}, view_box_id: {:?} }}", + self.data, + self.callbacks, + self.style, + self.transform_id, + self.view_box_id) + } +} + #[derive(Debug, Clone)] pub enum LayerType { KnownSize([SvgLayerType; 1]), @@ -940,6 +951,23 @@ pub enum SvgCallbacks { Some(Vec<(usize, Callback)>), } +impl fmt::Debug for SvgCallbacks { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::SvgCallbacks::*; + match self { + None => write!(f, "SvgCallbacks::None"), + Any(a) => write!(f, "SvgCallbacks::Any({:?})", a), + Some(v) => { + let mut s = String::new(); + for i in v.iter() { + s += &format!("{:?}", i); + } + write!(f, "SvgCallbacks::Some({})", s) + }, + } + } +} + impl Clone for SvgCallbacks { fn clone(&self) -> Self { use self::SvgCallbacks::*; From 788700977ed3c61cd804323eb49745459b9bb769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 22 Aug 2018 07:51:56 +0200 Subject: [PATCH 220/868] Publicly re-export lyon::PathEvent and lyon::Point --- src/widgets/svg.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index f0d504e6b..3c135acff 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -23,7 +23,7 @@ use lyon::{ }, path::{ default::{Builder}, - builder::{PathBuilder, FlatPathBuilder}, PathEvent, + builder::{PathBuilder, FlatPathBuilder}, }, geom::euclid::{TypedRect, TypedPoint2D, TypedSize2D, TypedVector2D}, }; @@ -42,6 +42,8 @@ use { pub use lyon::tessellation::VertexBuffers; pub use rusttype::GlyphId; +pub use lyon::path::PathEvent; +pub use lyon::geom::math::Point; static SVG_LAYER_ID: AtomicUsize = AtomicUsize::new(0); static SVG_TRANSFORM_ID: AtomicUsize = AtomicUsize::new(0); From 6b7c6def2e6c3d73e1e05a583a00bc0ff33cdb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 23 Aug 2018 04:21:55 +0200 Subject: [PATCH 221/868] Added optional serde serialization for BezierControlPoint --- src/lib.rs | 1 + src/widgets/svg.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1022f90ab..d60c96ef3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,6 +60,7 @@ extern crate fern; extern crate backtrace; extern crate image; #[cfg(feature = "serde_serialization")] +#[cfg_attr(feature = "serde_serialization", macro_use)] extern crate serde; #[cfg(not(target_os = "linux"))] diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 3c135acff..6198c800e 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1684,6 +1684,7 @@ pub struct VerticesIndicesBuffer { pub indices: Vec, } +#[cfg_attr(feature = "serde_serialization", derive(Serialize, Deserialize))] #[derive(Debug, Copy, Clone)] pub struct BezierControlPoint { pub x: f32, From 468a924044be1ec8104f3301a454b1f7451ee070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 25 Aug 2018 12:25:46 +0200 Subject: [PATCH 222/868] Initial architecture for CSS to layout constraints This represents the change from hard-coded constraints to transitioning to the cassowary constraint solver. Right now it should be possible to transform the CSS layout parameters into layout constraints - however, the initial test doesn't work yet. DisplayRect has been renamed to RectConstraintVariables (since it contains the variable IDs for the rectangle), to not be confused with DisplayRectangle. Right now if the DOM changes, the memory for the constraints is "leaked", i.e. the constraints aren't removed. This will be fixed in future versions. --- src/app.rs | 14 ++--- src/cache.rs | 6 +- src/constraints.rs | 20 +++---- src/display_list.rs | 136 ++++++++++++++++++-------------------------- src/id_tree.rs | 4 ++ src/lib.rs | 2 + src/ui_solver.rs | 133 +++++++++++++++++++++++++++++++++++++++++++ src/window.rs | 84 ++++----------------------- 8 files changed, 223 insertions(+), 176 deletions(-) create mode 100644 src/ui_solver.rs diff --git a/src/app.rs b/src/app.rs index dc35372e8..1c0dd1eb3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -35,7 +35,7 @@ use { /// Graphical application that maintains some kind of application state pub struct App { /// The graphical windows, indexed by ID - windows: Vec>, + windows: Vec, /// The global application state pub app_state: AppState, } @@ -308,7 +308,7 @@ impl App { Ok(()) } - fn update_display(window: &Window) + fn update_display(window: &Window) { use webrender::api::{Transaction, DeviceUintRect, DeviceUintPoint}; use euclid::TypedSize2D; @@ -322,7 +322,7 @@ impl App { window.internal.api.send_transaction(window.internal.document_id, txn); } - fn initialize_ui_state(windows: &[Window], app_state: &AppState) + fn initialize_ui_state(windows: &[Window], app_state: &AppState) -> Vec> { use window::ReadOnlyWindow; @@ -588,7 +588,7 @@ fn preprocess_event(event: &Event, frame_event_info: &mut FrameEventInfo, awaken fn do_hit_test_and_call_callbacks( event: &Event, - window: &mut Window, + window: &mut Window, window_id: WindowId, info: &mut FrameEventInfo, ui_state_cache: &[UiState], @@ -671,7 +671,7 @@ fn do_hit_test_and_call_callbacks( } fn render( - window: &mut Window, + window: &mut Window, _window_id: &WindowId, ui_description: &UiDescription, app_resources: &mut AppResources, @@ -686,7 +686,7 @@ fn render( let builder = display_list.into_display_list_builder( window.internal.pipeline_id, window.internal.epoch, - &mut window.solver, + &mut window.ui_solver, &mut window.css, app_resources, &window.internal.api, @@ -760,7 +760,7 @@ fn clean_up_unused_opengl_textures(pipeline_info: PipelineInfo) { // See: https://github.com/servo/webrender/pull/2880 // webrender doesn't reset the active shader back to what it was, but rather sets it // to zero, which glium doesn't know about, so on the next frame it tries to draw with shader 0 -fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D) { +fn render_inner(window: &mut Window, framebuffer_size: TypedSize2D) { use gleam::gl; use window::get_gl_context; diff --git a/src/cache.rs b/src/cache.rs index 0fbd7fc80..f28d104e1 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -41,7 +41,7 @@ use std::{ }; use cassowary::Solver; use { - constraints::DisplayRect, + constraints::RectConstraintVariables, id_tree::{NodeId, Arena}, traits::Layout, dom::NodeData, @@ -233,7 +233,7 @@ pub(crate) struct DomNodeHash { #[derive(Debug)] pub(crate) struct EditVariableCache { - pub(crate) map: BTreeMap + pub(crate) map: BTreeMap } impl EditVariableCache { @@ -254,7 +254,7 @@ impl EditVariableCache { e.into_mut().0 = true; }, Vacant(e) => { - let rect = DisplayRect::default(); + let rect = RectConstraintVariables::default(); rect.add_to_solver(solver); e.insert((true, rect)); } diff --git a/src/constraints.rs b/src/constraints.rs index 3c8e3f85e..f27dc39e2 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -13,7 +13,7 @@ pub type Point = Point2D; /// A set of cassowary `Variable`s representing the /// bounding rectangle of a layout. #[derive(Debug, Copy, Clone)] -pub(crate) struct DisplayRect { +pub(crate) struct RectConstraintVariables { pub left: Variable, pub top: Variable, pub right: Variable, @@ -22,7 +22,7 @@ pub(crate) struct DisplayRect { pub height: Variable, } -impl Default for DisplayRect { +impl Default for RectConstraintVariables { fn default() -> Self { Self { left: Variable::new(), @@ -35,7 +35,7 @@ impl Default for DisplayRect { } } -impl DisplayRect { +impl RectConstraintVariables { pub fn add_to_solver(&self, solver: &mut Solver) { solver.add_edit_variable(self.left, WEAK).unwrap_or_else(|_e| { }); @@ -65,8 +65,8 @@ pub struct Padding(pub f32); #[derive(Debug, Copy, Clone)] pub(crate) enum CssConstraint { - Size((SizeConstraint, Strength)), - Padding((PaddingConstraint, Strength, Padding)) + Size(SizeConstraint, Strength), + Padding(PaddingConstraint, Strength, Padding) } #[derive(Debug, Copy, Clone)] @@ -82,7 +82,7 @@ pub(crate) enum SizeConstraint { ShrinkHorizontal, ShrinkVertical, TopLeft(Point), - Center(DisplayRect), + Center(RectConstraintVariables), CenterHorizontal(Variable, Variable), CenterVertical(Variable, Variable), } @@ -105,14 +105,14 @@ pub(crate) enum PaddingConstraint { BoundTop(Variable), BoundRight(Variable), BoundBottom(Variable), - BoundBy(DisplayRect), - MatchLayout(DisplayRect), + BoundBy(RectConstraintVariables), + MatchLayout(RectConstraintVariables), MatchWidth(Variable), MatchHeight(Variable), } impl SizeConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64) -> Vec { + pub(crate) fn build(&self, rect: &RectConstraintVariables, strength: f64) -> Vec { use self::SizeConstraint::*; match *self { @@ -178,7 +178,7 @@ impl SizeConstraint { } impl PaddingConstraint { - pub(crate) fn build(&self, rect: &DisplayRect, strength: f64, padding: f32) -> Vec { + pub(crate) fn build(&self, rect: &RectConstraintVariables, strength: f64, padding: f32) -> Vec { use self::PaddingConstraint::*; match *self { AlignTop(top) => { diff --git a/src/display_list.rs b/src/display_list.rs index 3874f01f5..3eb167109 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -4,14 +4,13 @@ use webrender::api::*; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; -use cassowary::Constraint; use { FastHashMap, resources::AppResources, traits::Layout, - constraints::{DisplayRect, CssConstraint}, + constraints::CssConstraint, ui_description::{UiDescription, StyledNode}, - window::UiSolver, + ui_solver::UiSolver, window_state::WindowSize, id_tree::{Arena, NodeId}, css_parser::*, @@ -45,16 +44,6 @@ pub(crate) struct DisplayRectangle<'a> { pub(crate) layout: RectLayout, } -/// It is not very efficient to re-create constraints on every call, the difference -/// in performance can be huge. Without re-creating constraints, solving can take 0.3 ms, -/// with re-creation it can take up to 9 ms. So the goal is to not re-create constraints -/// if their contents haven't changed. -#[derive(Default)] -pub(crate) struct SolvedLayout { - // List of previously solved constraints - pub(crate) solved_constraints: FastHashMap>, -} - /// This is used for caching large strings (in the `push_text` function) /// In the cached version, you can lookup the text as well as the dimensions of /// the words in the `AppResources`. For the `Uncached` version, you'll have to re- @@ -90,14 +79,6 @@ impl TextInfo { } } -impl SolvedLayout { - pub fn empty() -> Self { - Self { - solved_constraints: FastHashMap::default(), - } - } -} - impl<'a> DisplayRectangle<'a> { #[inline] pub fn new(tag: Option, styled_node: &'a StyledNode) -> Self { @@ -245,7 +226,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { &self, pipeline_id: PipelineId, current_epoch: Epoch, - ui_solver: &mut UiSolver, + ui_solver: &mut UiSolver, css: &mut Css, app_resources: &mut AppResources, render_api: &RenderApi, @@ -253,50 +234,42 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { window_size: &WindowSize) -> DisplayListBuilder { - let mut changeset = None; + use glium::glutin::dpi::LogicalSize; - if let Some(root) = self.ui_descr.ui_descr_root { - let local_changeset = ui_solver.dom_tree_cache.update(root, &*(self.ui_descr.ui_descr_arena.borrow())); - ui_solver.edit_variable_cache.initialize_new_rectangles(&mut ui_solver.solver, &local_changeset); - ui_solver.edit_variable_cache.remove_unused_variables(&mut ui_solver.solver); - changeset = Some(local_changeset); - } + let changeset = self.ui_descr.ui_descr_root.as_ref().and_then(|root| { + let changeset = ui_solver.update_dom(root, &*(self.ui_descr.ui_descr_arena.borrow())); + if changeset.is_empty() { None } else { Some(changeset) } + }); if css.needs_relayout { - // constraints were added or removed during the last frame + // Constraints were added or removed during the last frame for rect_idx in self.rectangles.linear_iter() { - let rect = &self.rectangles[rect_idx].data; - let arena = &*self.ui_descr.ui_descr_arena.borrow(); - let dom_hash = &ui_solver.dom_tree_cache.previous_layout.arena[rect_idx]; - let display_rect = ui_solver.edit_variable_cache.map[&dom_hash.data]; - let layout_contraints = create_layout_constraints(rect, rect_idx, &self.rectangles, window_size); - let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, &layout_contraints); - ui_solver.solver.add_constraints(&cassowary_constraints).unwrap(); + + let layout_contraints = create_layout_constraints( + rect_idx, + &self.rectangles, + &*self.ui_descr.ui_descr_arena.borrow(), + &ui_solver, + ); + + ui_solver.insert_css_constraints_for_rect(rect_idx, &layout_contraints); } - // if we push or pop constraints that means we also need to re-layout the window + // If we push or pop constraints that means we also need to re-layout the window has_window_size_changed = true; } - let changeset_is_useless = match changeset { - None => true, - Some(c) => c.is_empty() - }; + // TODO: early return based on changeset? - // recalculate the actual layout + // Recalculate the actual layout if css.needs_relayout || has_window_size_changed { - /* - for change in solver.fetch_changes() { - println!("change: - {:?}", change); - } - */ + ui_solver.update_window_size(&window_size.dimensions); + ui_solver.update_layout_cache(); } css.needs_relayout = false; - use glium::glutin::dpi::LogicalSize; - let LogicalSize { width, height } = window_size.dimensions; let mut builder = DisplayListBuilder::with_capacity(pipeline_id, TypedSize2D::new(width as f32, height as f32), self.rectangles.nodes_len()); let mut resource_updates = Vec::::new(); @@ -310,8 +283,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let arena = self.ui_descr.ui_descr_arena.borrow(); let node_type = &arena[rect_idx].data.node_type; - // ask the solver what the bounds of the current rectangle is - // let bounds = ui_solver.query_bounds_of_rect(*rect_idx); + // Ask the solver what the bounds of the current rectangle is + let bounds = ui_solver.query_bounds_of_rect(rect_idx); // temporary: fill the whole window with each rectangle displaylist_handle_rect( @@ -320,7 +293,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { rect_idx, &self.rectangles, node_type, - full_screen_rect, /* replace this with the real bounds */ + bounds, full_screen_rect, app_resources, render_api, @@ -835,6 +808,7 @@ fn push_background( } } +#[inline] fn push_image( info: &PrimitiveInfo, builder: &mut DisplayListBuilder, @@ -992,42 +966,40 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash } // Returns the constraints for one rectangle -fn create_layout_constraints<'a>( - rect: &DisplayRectangle, - rect_id: NodeId, - arena: &Arena>, - window_size: &WindowSize) +fn create_layout_constraints<'a, T: Layout>( + node_id: NodeId, + display_rectangles: &Arena>, + dom: &Arena>, + ui_solver: &UiSolver) -> Vec { use cassowary::strength::*; - use constraints::{SizeConstraint, Strength}; + use constraints::{SizeConstraint, PaddingConstraint, Strength, Padding}; - let mut layout_constraints = Vec::::new(); - /* - let max_width = arena.get_wh_for_rectangle(rect_id, WidthOrHeight::Width) - .unwrap_or(window_size.width as f32); - */ - layout_constraints.push(CssConstraint::Size((SizeConstraint::Width(200.0), Strength(STRONG)))); - layout_constraints.push(CssConstraint::Size((SizeConstraint::Height(200.0), Strength(STRONG)))); + let rect = &display_rectangles[node_id].data; + let dom_node = &dom[node_id]; - layout_constraints -} + let mut layout_constraints = Vec::new(); -fn css_constraints_to_cassowary_constraints(rect: &DisplayRect, css: &Vec) --> Vec -{ - use self::CssConstraint::*; + // Insert the max height and width constraints + if let Some(max_width) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Width) { + layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(max_width), Strength(MEDIUM))); + } - css.iter().flat_map(|constraint| - match *constraint { - Size((constraint, strength)) => { - constraint.build(&rect, strength.0) - } - Padding((constraint, strength, padding)) => { - constraint.build(&rect, strength.0, padding.0) - } - } - ).collect() + if let Some(max_height) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Height) { + layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(max_height), Strength(MEDIUM))); + } + + // Testing only - each rectangle should be below its previous sibling DOM element + if let Some(previous_sibling) = dom_node.previous_sibling { + // The variable must have been initialized before `create_layout_constraints` + // was called, so this `unwrap()` should never panic + let previous_rect_var = ui_solver.get_rect_constraints(previous_sibling).unwrap(); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.bottom), Strength(STRONG), Padding(0.0))); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.top), Strength(STRONG), Padding(0.0))); + } + + layout_constraints } // Layout / tracing-related functions diff --git a/src/id_tree.rs b/src/id_tree.rs index a52198b29..ed9a5fe95 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -202,6 +202,10 @@ impl Arena { pub(crate) fn append(&mut self, other: &mut Arena) { self.nodes.append(&mut other.nodes); } + + pub fn get(&self, node: &NodeId) -> Option<&Node> { + self.nodes.get(node.index()) + } } impl Arena { diff --git a/src/lib.rs b/src/lib.rs index d60c96ef3..4f1da7738 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,6 +121,8 @@ mod compositor; /// Default logger, can be turned off with `feature = "logging"` #[cfg(feature = "logging")] mod logging; +/// Cassowary-based UI solver +mod ui_solver; /// Faster implementation of a HashMap type FastHashMap = ::std::collections::HashMap>; diff --git a/src/ui_solver.rs b/src/ui_solver.rs new file mode 100644 index 000000000..1bf30bd79 --- /dev/null +++ b/src/ui_solver.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; +use cassowary::{ + Variable, Solver, Constraint, + strength::*, +}; +use glium::glutin::dpi::LogicalSize; +use webrender::api::LayoutPixel; +use euclid::{TypedRect, TypedPoint2D, TypedSize2D}; +use { + id_tree::{NodeId, Arena}, + dom::NodeData, + cache::{EditVariableCache, DomTreeCache, DomChangeSet}, + constraints::{CssConstraint, RectConstraintVariables}, + traits::Layout, +}; + +/// Stores the variables of the root width and height (but not the values themselves) +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) struct WindowSizeConstraints { + pub(crate) width_var: Variable, + pub(crate) height_var: Variable, +} + +impl WindowSizeConstraints { + pub fn new() -> Self { + Self { + width_var: Variable::new(), + height_var: Variable::new(), + } + } +} + +/// Solver for solving the UI of the current window +pub struct UiSolver { + /// The actual cassowary solver + solver: Solver, + /// The size constraints of the root window + window_constraints: WindowSizeConstraints, + /// The list of variables that has been added to the solver + edit_variable_cache: EditVariableCache, + /// + solved_values: BTreeMap, + /// The cache of the previous frames DOM tree + dom_tree_cache: DomTreeCache, +} + +impl UiSolver { + + pub(crate) fn new(window_size: &LogicalSize) -> Self { + + let mut solver = Solver::new(); + let window_constraints = WindowSizeConstraints::new(); + + solver.add_edit_variable(window_constraints.width_var, STRONG).unwrap(); + solver.add_edit_variable(window_constraints.height_var, STRONG).unwrap(); + solver.suggest_value(window_constraints.width_var, window_size.width as f64).unwrap(); + solver.suggest_value(window_constraints.height_var, window_size.height as f64).unwrap(); + + Self { + solver: solver, + solved_values: BTreeMap::new(), + window_constraints: window_constraints, + edit_variable_cache: EditVariableCache::empty(), + dom_tree_cache: DomTreeCache::empty(), + } + } + + pub(crate) fn update_dom(&mut self, root: &NodeId, arena: &Arena>) -> DomChangeSet { + let changeset = self.dom_tree_cache.update(*root, arena); + self.edit_variable_cache.initialize_new_rectangles(&mut self.solver, &changeset); + self.edit_variable_cache.remove_unused_variables(&mut self.solver); + changeset + } + + pub(crate) fn insert_css_constraints_for_rect(&mut self, rect_idx: NodeId, constraints: &[CssConstraint]) { + let dom_hash = &self.dom_tree_cache.previous_layout.arena[rect_idx]; + let display_rect = self.edit_variable_cache.map[&dom_hash.data]; + let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, constraints); + self.solver.add_constraints(&cassowary_constraints).unwrap(); + } + + /// Notifies the solver that the window size has changed + pub(crate) fn update_window_size(&mut self, window_size: &LogicalSize) { + self.solver.suggest_value(self.window_constraints.width_var, window_size.width).unwrap(); + self.solver.suggest_value(self.window_constraints.height_var, window_size.height).unwrap(); + } + + pub(crate) fn update_layout_cache(&mut self) { + for (variable, solved_value) in self.solver.fetch_changes() { + self.solved_values.insert(*variable, *solved_value); + } + } + + pub(crate) fn query_bounds_of_rect(&self, rect_id: NodeId) -> TypedRect { + + let display_rect = self.get_rect_constraints(rect_id).unwrap(); + + let width = match self.solved_values.get(&display_rect.width) { + Some(w) => *w, + None => self.solved_values[&self.window_constraints.width_var], + }; + + let height = match self.solved_values.get(&display_rect.height) { + Some(h) => *h, + None => self.solved_values[&self.window_constraints.height_var], + }; + + let top = self.solved_values.get(&display_rect.top).and_then(|x| Some(*x)).unwrap_or(0.0); + let left = self.solved_values.get(&display_rect.left).and_then(|x| Some(*x)).unwrap_or(0.0); + + TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new(width as f32, height as f32)) + } + + pub(crate) fn get_rect_constraints(&self, rect_id: NodeId) -> Option { + let dom_hash = &self.dom_tree_cache.previous_layout.arena.get(&rect_id)?; + self.edit_variable_cache.map.get(&dom_hash.data).and_then(|rect| Some(rect.1)) + } +} + +fn css_constraints_to_cassowary_constraints(rect: &RectConstraintVariables, css: &[CssConstraint]) +-> Vec +{ + css.iter().flat_map(|constraint| + match *constraint { + CssConstraint::Size(constraint, strength) => { + constraint.build(&rect, strength.0) + } + CssConstraint::Padding(constraint, strength, padding) => { + constraint.build(&rect, strength.0, padding.0) + } + } + ).collect() +} \ No newline at end of file diff --git a/src/window.rs b/src/window.rs index 0a847ac98..5d55fba54 100644 --- a/src/window.rs +++ b/src/window.rs @@ -19,22 +19,15 @@ use glium::{ backend::{Context, Facade, glutin::DisplayCreationError}, }; use gleam::gl::{self, Gl}; -use euclid::TypedScale; -use cassowary::{ - Variable, Solver, - strength::*, -}; use { dom::{Texture, Callback}, css::{Css, FakeCss}, window_state::{WindowState, MouseState, KeyboardState}, - display_list::SolvedLayout, traits::Layout, - cache::{EditVariableCache, DomTreeCache}, - id_tree::NodeId, compositor::Compositor, app::FrameEventInfo, resources::AppResources, + ui_solver::UiSolver, }; /// azul-internal ID for a window @@ -447,7 +440,7 @@ impl Default for WindowMonitorTarget { } /// Represents one graphical window to be rendered -pub struct Window { +pub struct Window { // TODO: technically, having one EventsLoop for all windows is sufficient pub(crate) events_loop: EventsLoop, /// Current state of the window, stores the keyboard / mouse state, @@ -466,58 +459,13 @@ pub struct Window { /// The `WindowInternal` allows us to solve some borrowing issues pub(crate) internal: WindowInternal, /// The solver for the UI, for caching the results of the computations - pub(crate) solver: UiSolver, + pub(crate) ui_solver: UiSolver, // The background thread that is running for this window. // pub(crate) background_thread: Option>, /// The css (how the current window is styled) pub css: Css, } -/// Used in the solver, for the root constraint -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) struct WindowDimensions { - pub(crate) layout_size: LayoutSize, - pub(crate) width_var: Variable, - pub(crate) height_var: Variable, -} - -impl WindowDimensions { - pub fn new_from_layout_size(layout_size: LayoutSize) -> Self { - Self { - layout_size: layout_size, - width_var: Variable::new(), - height_var: Variable::new(), - } - } - - pub fn width(&self) -> f32 { - self.layout_size.width_typed().get() - } - pub fn height(&self) -> f32 { - self.layout_size.height_typed().get() - } -} - -/// Solver for solving the UI of the current window -pub(crate) struct UiSolver { - /// The actual solver - pub(crate) solver: Solver, - /// Solved layout from the previous frame (empty by default) - /// This is necessary for caching the constraints of the given layout - pub(crate) solved_layout: SolvedLayout, - /// The list of variables that has been added to the solver - pub(crate) edit_variable_cache: EditVariableCache, - /// The cache of the previous frames DOM tree - pub(crate) dom_tree_cache: DomTreeCache, -} - -impl UiSolver { - pub(crate) fn query_bounds_of_rect(&self, rect_id: NodeId) { - // TODO: After solving the UI, use this function to get the actual coordinates of an item in the UI. - // This function should cache values accordingly - } -} - pub(crate) struct WindowInternal { pub(crate) last_display_list_builder: BuiltDisplayList, pub(crate) api: RenderApi, @@ -526,10 +474,10 @@ pub(crate) struct WindowInternal { pub(crate) document_id: DocumentId, } -impl Window { +impl Window { /// Creates a new window - pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { + pub fn new(mut options: WindowCreateOptions, css: Css) -> Result { let events_loop = EventsLoop::new(); @@ -693,20 +641,13 @@ impl Window { let document_id = api.add_document(framebuffer_size, 0); let epoch = Epoch(0); let pipeline_id = PipelineId(0, 0); - let layout_size = framebuffer_size.to_f32() / TypedScale::new(device_pixel_ratio as f32); -/* + /* let (sender, receiver) = channel(); let thread = Builder::new().name(options.title.clone()).spawn(move || Self::handle_event(receiver))?; -*/ - let mut solver = Solver::new(); + */ - let window_dim = WindowDimensions::new_from_layout_size(layout_size); - - solver.add_edit_variable(window_dim.width_var, STRONG).unwrap(); - solver.add_edit_variable(window_dim.height_var, STRONG).unwrap(); - solver.suggest_value(window_dim.width_var, window_dim.width() as f64).unwrap(); - solver.suggest_value(window_dim.height_var, window_dim.height() as f64).unwrap(); + let ui_solver = UiSolver::new(&options.state.size.dimensions); renderer.set_external_image_handler(Box::new(Compositor::default())); @@ -723,12 +664,7 @@ impl Window { document_id: document_id, last_display_list_builder: BuiltDisplayList::default(), }, - solver: UiSolver { - solver: solver, - solved_layout: SolvedLayout::empty(), - edit_variable_cache: EditVariableCache::empty(), - dom_tree_cache: DomTreeCache::empty(), - } + ui_solver, }; Ok(window) @@ -836,7 +772,7 @@ pub(crate) fn get_gl_context(display: &Display) -> Result, WindowCreateEr } } -impl Drop for Window { +impl Drop for Window { fn drop(&mut self) { // self.background_thread.take().unwrap().join(); let renderer = self.renderer.take().unwrap(); From 3a50f32dbd0efb25b042f84c62312cf7a17ce61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 25 Aug 2018 13:48:32 +0200 Subject: [PATCH 223/868] More tests regarding cassowary layout --- examples/debug.rs | 27 ++++++++++++++++++++++++++- src/css.rs | 10 +++++----- src/display_list.rs | 31 ++++++++++++++++++++++++------- src/ui_solver.rs | 38 ++++++++++++++++++++++++++++---------- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 5def79890..1b984e06f 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -48,10 +48,15 @@ impl Layout for MyAppData { .with_callback(On::Scroll, Callback(scroll_map_contents)) .with_callback(On::MouseOver, Callback(check_hovered_font)) } else { + /* // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file Button::with_label("Load SVG file...").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) + */ + Dom::new(NodeType::Div).with_id("wrapper_1") + .with_child(Dom::new(NodeType::Div).with_id("red")) + .with_child(Dom::new(NodeType::Div).with_id("green")) } } } @@ -196,7 +201,27 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { + // should show a large red rectangle at the top + // and a small green rectangle at the bottom + let css = Css::new_from_str(" + #wrapper_1 { + flex-direction: column; + } + + #red { + background-color: red; + width: 200px; + height: 200px; + } + + #green { + background-color: green; + width: 50px; + height: 50px; + } + ").unwrap(); + let mut app = App::new(MyAppData { map: None }, AppConfig::default()); - app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/css.rs b/src/css.rs index 683e12330..29cf45368 100644 --- a/src/css.rs +++ b/src/css.rs @@ -96,7 +96,7 @@ impl<'a> From> for CssParseError<'a> { /// Rule that applies to some "path" in the CSS, i.e. /// `div#myid.myclass -> ("justify-content", "center")` /// -/// The CSS rule is currently not cascaded, use `Css::new_from_string()` +/// The CSS rule is currently not cascaded, use `Css::new_from_str()` /// to do the cascading. #[derive(Debug, Clone, PartialEq)] pub(crate) struct CssRule { @@ -158,7 +158,7 @@ impl Css { } /// Parses a CSS string (single-threaded) and returns the parsed rules - pub fn new_from_string<'a>(css_string: &'a str) -> Result> { + pub fn new_from_str<'a>(css_string: &'a str) -> Result> { use simplecss::{Tokenizer, Token}; use std::collections::HashSet; @@ -258,19 +258,19 @@ impl Css { /// Returns the native style for the OS #[cfg(target_os="windows")] pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_WINDOWS).unwrap() + Self::new_from_str(NATIVE_CSS_WINDOWS).unwrap() } /// Returns the native style for the OS #[cfg(target_os="linux")] pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_LINUX).unwrap() + Self::new_from_str(NATIVE_CSS_LINUX).unwrap() } /// Returns the native style for the OS #[cfg(target_os="macos")] pub fn native() -> Self { - Self::new_from_string(NATIVE_CSS_MACOS).unwrap() + Self::new_from_str(NATIVE_CSS_MACOS).unwrap() } } diff --git a/src/display_list.rs b/src/display_list.rs index 3eb167109..9d9d16fd2 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -264,10 +264,12 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // Recalculate the actual layout if css.needs_relayout || has_window_size_changed { - ui_solver.update_window_size(&window_size.dimensions); - ui_solver.update_layout_cache(); + } + ui_solver.update_window_size(&window_size.dimensions); + ui_solver.update_layout_cache(); + css.needs_relayout = false; let LogicalSize { width, height } = window_size.dimensions; @@ -285,6 +287,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // Ask the solver what the bounds of the current rectangle is let bounds = ui_solver.query_bounds_of_rect(rect_idx); + println!("id: {} - bounds: {}", rect_idx, bounds); // temporary: fill the whole window with each rectangle displaylist_handle_rect( @@ -976,6 +979,10 @@ fn create_layout_constraints<'a, T: Layout>( use cassowary::strength::*; use constraints::{SizeConstraint, PaddingConstraint, Strength, Padding}; + const AZ_WEAK: f64 = 3.0; + const AZ_MEDIUM: f64 = 30.0; + const AZ_STRONG: f64 = 300.0; + let rect = &display_rectangles[node_id].data; let dom_node = &dom[node_id]; @@ -983,22 +990,32 @@ fn create_layout_constraints<'a, T: Layout>( // Insert the max height and width constraints if let Some(max_width) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Width) { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(max_width), Strength(MEDIUM))); + layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(max_width), Strength(AZ_WEAK))); } if let Some(max_height) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Height) { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(max_height), Strength(MEDIUM))); + layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(max_height), Strength(AZ_WEAK))); } // Testing only - each rectangle should be below its previous sibling DOM element + if let Some(parent) = dom_node.parent { + let parent = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::BoundBy(parent), Strength(REQUIRED), Padding(20.0))); + } else { + let window = ui_solver.get_window_constraints(); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::MatchWidth(window.width_var), Strength(REQUIRED), Padding(0.0))); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::MatchHeight(window.height_var), Strength(REQUIRED), Padding(0.0))); + } +/* if let Some(previous_sibling) = dom_node.previous_sibling { + println!("inserting bottom constraint for ID: {}", node_id); // The variable must have been initialized before `create_layout_constraints` // was called, so this `unwrap()` should never panic let previous_rect_var = ui_solver.get_rect_constraints(previous_sibling).unwrap(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.bottom), Strength(STRONG), Padding(0.0))); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.top), Strength(STRONG), Padding(0.0))); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignTop(previous_rect_var.bottom), Strength(REQUIRED), Padding(20.0))); + // layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.top), Strength(STRONG), Padding(0.0))); } - +*/ layout_constraints } diff --git a/src/ui_solver.rs b/src/ui_solver.rs index 1bf30bd79..f2a059fc5 100644 --- a/src/ui_solver.rs +++ b/src/ui_solver.rs @@ -73,10 +73,19 @@ impl UiSolver { } pub(crate) fn insert_css_constraints_for_rect(&mut self, rect_idx: NodeId, constraints: &[CssConstraint]) { + use cassowary::strength::*; + use constraints::{SizeConstraint, Strength, Point}; + let dom_hash = &self.dom_tree_cache.previous_layout.arena[rect_idx]; - let display_rect = self.edit_variable_cache.map[&dom_hash.data]; - let cassowary_constraints = css_constraints_to_cassowary_constraints(&display_rect.1, constraints); - self.solver.add_constraints(&cassowary_constraints).unwrap(); + let display_rect = &self.edit_variable_cache.map[&dom_hash.data].1; + + println!("display_rect {} - variables: {:?}", rect_idx, display_rect); + + self.solver.add_constraints(&css_constraints_to_cassowary_constraints(display_rect, &[ + CssConstraint::Size(SizeConstraint::TopLeft(Point::new(0.0, 0.0)), Strength(STRONG)) + ])).unwrap(); + + self.solver.add_constraints(&css_constraints_to_cassowary_constraints(display_rect, constraints)).unwrap(); } /// Notifies the solver that the window size has changed @@ -87,6 +96,7 @@ impl UiSolver { pub(crate) fn update_layout_cache(&mut self) { for (variable, solved_value) in self.solver.fetch_changes() { + println!("variable {:?} - solved value: {}", variable, solved_value); self.solved_values.insert(*variable, *solved_value); } } @@ -95,26 +105,34 @@ impl UiSolver { let display_rect = self.get_rect_constraints(rect_id).unwrap(); - let width = match self.solved_values.get(&display_rect.width) { + let top = self.solved_values.get(&display_rect.top).and_then(|x| Some(*x)).unwrap_or(0.0); + let left = self.solved_values.get(&display_rect.left).and_then(|x| Some(*x)).unwrap_or(0.0); + + let right = match self.solved_values.get(&display_rect.right) { Some(w) => *w, None => self.solved_values[&self.window_constraints.width_var], }; - let height = match self.solved_values.get(&display_rect.height) { + let bottom = match self.solved_values.get(&display_rect.bottom) { Some(h) => *h, None => self.solved_values[&self.window_constraints.height_var], }; - - let top = self.solved_values.get(&display_rect.top).and_then(|x| Some(*x)).unwrap_or(0.0); - let left = self.solved_values.get(&display_rect.left).and_then(|x| Some(*x)).unwrap_or(0.0); - - TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new(width as f32, height as f32)) +/* + println!("rect id: {} - top: {}, left: {}, right: {}, bottom: {}", + rect_id, top, left, right, bottom + ); +*/ + TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new((right - left) as f32, (bottom - top) as f32)) } pub(crate) fn get_rect_constraints(&self, rect_id: NodeId) -> Option { let dom_hash = &self.dom_tree_cache.previous_layout.arena.get(&rect_id)?; self.edit_variable_cache.map.get(&dom_hash.data).and_then(|rect| Some(rect.1)) } + + pub(crate) fn get_window_constraints(&self) -> WindowSizeConstraints { + self.window_constraints + } } fn css_constraints_to_cassowary_constraints(rect: &RectConstraintVariables, css: &[CssConstraint]) From 161a8c8d0e098030d7563d66b43b95f05bb90f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 25 Aug 2018 15:02:38 +0200 Subject: [PATCH 224/868] Replaced width / min-width / max-width with constraints --- examples/debug.rs | 2 + src/constraints.rs | 10 +++- src/display_list.rs | 133 ++++++++------------------------------------ src/ui_solver.rs | 26 ++------- 4 files changed, 40 insertions(+), 131 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 1b984e06f..a44c6da25 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -210,7 +210,9 @@ fn main() { #red { background-color: red; + min-width: 100px; width: 200px; + max-width: 300px; height: 200px; } diff --git a/src/constraints.rs b/src/constraints.rs index f27dc39e2..f31a385b5 100644 --- a/src/constraints.rs +++ b/src/constraints.rs @@ -2,7 +2,7 @@ use cassowary::{ Solver, Variable, Constraint, - WeightedRelation::{EQ, GE}, + WeightedRelation::{EQ, GE, LE}, strength::{WEAK, REQUIRED}, }; use euclid::{Point2D, Size2D}; @@ -75,6 +75,8 @@ pub(crate) enum SizeConstraint { Height(f32), MinWidth(f32), MinHeight(f32), + MaxWidth(f32), + MaxHeight(f32), Size(Size), MinSize(Size), AspectRatio(f32), @@ -128,6 +130,12 @@ impl SizeConstraint { MinHeight(height) => { vec![ rect.height | GE(strength) | height ] }, + MaxWidth(width) => { + vec![ rect.width | LE(strength) | width ] + }, + MaxHeight(height) => { + vec![ rect.height | LE(strength) | height ] + }, Size(size) => { vec![ rect.width | EQ(strength) | size.width, diff --git a/src/display_list.rs b/src/display_list.rs index 9d9d16fd2..edb42e145 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -977,7 +977,7 @@ fn create_layout_constraints<'a, T: Layout>( -> Vec { use cassowary::strength::*; - use constraints::{SizeConstraint, PaddingConstraint, Strength, Padding}; + use constraints::{SizeConstraint, PaddingConstraint, Strength, Padding, Point}; const AZ_WEAK: f64 = 3.0; const AZ_MEDIUM: f64 = 30.0; @@ -989,121 +989,36 @@ fn create_layout_constraints<'a, T: Layout>( let mut layout_constraints = Vec::new(); // Insert the max height and width constraints - if let Some(max_width) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Width) { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(max_width), Strength(AZ_WEAK))); + // + // min-width and max-width are stronger than width because the width has to be between min and max width + if let Some(min_width) = rect.layout.min_width { + layout_constraints.push(CssConstraint::Size(SizeConstraint::MinWidth(min_width.0.to_pixels()), Strength(REQUIRED))); } - - if let Some(max_height) = display_rectangles.get_wh_for_rectangle(node_id, WidthOrHeight::Height) { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(max_height), Strength(AZ_WEAK))); + if let Some(width) = rect.layout.width { + layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(width.0.to_pixels()), Strength(STRONG))); + } + if let Some(max_width) = rect.layout.max_width { + layout_constraints.push(CssConstraint::Size(SizeConstraint::MaxWidth(max_width.0.to_pixels()), Strength(REQUIRED))); } - // Testing only - each rectangle should be below its previous sibling DOM element - if let Some(parent) = dom_node.parent { - let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::BoundBy(parent), Strength(REQUIRED), Padding(20.0))); - } else { - let window = ui_solver.get_window_constraints(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::MatchWidth(window.width_var), Strength(REQUIRED), Padding(0.0))); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::MatchHeight(window.height_var), Strength(REQUIRED), Padding(0.0))); + if let Some(min_height) = rect.layout.min_height { + layout_constraints.push(CssConstraint::Size(SizeConstraint::MinHeight(min_height.0.to_pixels()), Strength(REQUIRED))); } -/* - if let Some(previous_sibling) = dom_node.previous_sibling { - println!("inserting bottom constraint for ID: {}", node_id); - // The variable must have been initialized before `create_layout_constraints` - // was called, so this `unwrap()` should never panic - let previous_rect_var = ui_solver.get_rect_constraints(previous_sibling).unwrap(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignTop(previous_rect_var.bottom), Strength(REQUIRED), Padding(20.0))); - // layout_constraints.push(CssConstraint::Padding(PaddingConstraint::Below(previous_rect_var.top), Strength(STRONG), Padding(0.0))); + if let Some(height) = rect.layout.height { + layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(height.0.to_pixels()), Strength(STRONG))); + } + if let Some(max_height) = rect.layout.max_height { + layout_constraints.push(CssConstraint::Size(SizeConstraint::MaxWidth(max_height.0.to_pixels()), Strength(REQUIRED))); } -*/ - layout_constraints -} - -// Layout / tracing-related functions - -// What constraint (width or height) to search for when looking for a fitting width / height constraint -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum WidthOrHeight { - Width, - Height, -} - -impl<'a> Arena> { - - /// Recursive algorithm for getting the dimensions of a rectangle - /// - /// This function can be used on any rectangle to get the maximum allowed width - /// (for inserting the width / height constraint into the layout solver). - /// It simply traverses upwards through the nodes, until it finds a matching min-width / width - /// constraint, returns None, if the root node is reached (with no constraints) - /// - /// Usually, you'd use it like: - /// - /// ```no_run,ignore - /// let max_width = arena.get_wh_for_rectangle(id, WidthOrHeight::Width) - /// .unwrap_or(window_dimensions.width); - /// ``` - fn get_wh_for_rectangle(&self, id: NodeId, field: WidthOrHeight) -> Option { - - use self::WidthOrHeight::*; - - let node = &self[id]; - - macro_rules! get_wh { - ($field_name:ident, $min_field:ident) => ({ - let mut $field_name: Option = None; - - match node.data.layout.$min_field { - Some(m_w) => { - let m_w_px = m_w.0.to_pixels(); - match node.data.layout.$field_name { - Some(w) => { - // width + min_width - let w_px = w.0.to_pixels(); - $field_name = Some(m_w_px.max(w_px)); - }, - None => { - // min_width - $field_name = Some(m_w_px); - } - } - }, - None => { - match node.data.layout.$field_name { - Some(w) => { - // width - let w_px = w.0.to_pixels(); - $field_name = Some(w_px); - }, - None => { - // neither width nor min_width - } - } - } - }; - - if $field_name.is_none() { - match node.parent() { - Some(p) => $field_name = self.get_wh_for_rectangle(p, field), - None => { }, - } - } - - $field_name - }) - } - match field { - Width => { - let w = get_wh!(width, min_width); - w - }, - Height => { - let h = get_wh!(height, min_height); - h - } - } + if let Some(previous_sibling) = dom_node.previous_sibling { + let previous_sibling = ui_solver.get_rect_constraints(previous_sibling).unwrap(); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignTop(previous_sibling.bottom), Strength(REQUIRED), Padding(0.0))); + } else { + layout_constraints.push(CssConstraint::Size(SizeConstraint::TopLeft(Point::new(0.0, 0.0)), Strength(STRONG))); } + + layout_constraints } // Empty test, for some reason codecov doesn't detect any files (and therefore diff --git a/src/ui_solver.rs b/src/ui_solver.rs index f2a059fc5..60d43dbdf 100644 --- a/src/ui_solver.rs +++ b/src/ui_solver.rs @@ -79,12 +79,8 @@ impl UiSolver { let dom_hash = &self.dom_tree_cache.previous_layout.arena[rect_idx]; let display_rect = &self.edit_variable_cache.map[&dom_hash.data].1; - println!("display_rect {} - variables: {:?}", rect_idx, display_rect); - - self.solver.add_constraints(&css_constraints_to_cassowary_constraints(display_rect, &[ - CssConstraint::Size(SizeConstraint::TopLeft(Point::new(0.0, 0.0)), Strength(STRONG)) - ])).unwrap(); - + println!("display_rect {} - variables: {:#?}", rect_idx, display_rect); + self.solver.add_constraints(&css_constraints_to_cassowary_constraints(display_rect, constraints)).unwrap(); } @@ -107,22 +103,10 @@ impl UiSolver { let top = self.solved_values.get(&display_rect.top).and_then(|x| Some(*x)).unwrap_or(0.0); let left = self.solved_values.get(&display_rect.left).and_then(|x| Some(*x)).unwrap_or(0.0); + let width = self.solved_values.get(&display_rect.width).and_then(|x| Some(*x)).unwrap_or(0.0); + let height = self.solved_values.get(&display_rect.height).and_then(|x| Some(*x)).unwrap_or(0.0); - let right = match self.solved_values.get(&display_rect.right) { - Some(w) => *w, - None => self.solved_values[&self.window_constraints.width_var], - }; - - let bottom = match self.solved_values.get(&display_rect.bottom) { - Some(h) => *h, - None => self.solved_values[&self.window_constraints.height_var], - }; -/* - println!("rect id: {} - top: {}, left: {}, right: {}, bottom: {}", - rect_id, top, left, right, bottom - ); -*/ - TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new((right - left) as f32, (bottom - top) as f32)) + TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new(width as f32, height as f32)) } pub(crate) fn get_rect_constraints(&self, rect_id: NodeId) -> Option { From 84cdcc849fb3c6da8f16934d83d52f4d388aee61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 25 Aug 2018 15:34:09 +0200 Subject: [PATCH 225/868] Managed to get a better layout somehow --- examples/debug.rs | 5 +---- src/display_list.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index a44c6da25..e7764490c 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -204,14 +204,11 @@ fn main() { // should show a large red rectangle at the top // and a small green rectangle at the bottom let css = Css::new_from_str(" - #wrapper_1 { - flex-direction: column; - } #red { background-color: red; min-width: 100px; - width: 200px; + width: 400px; max-width: 300px; height: 200px; } diff --git a/src/display_list.rs b/src/display_list.rs index edb42e145..eccb58e46 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1011,11 +1011,18 @@ fn create_layout_constraints<'a, T: Layout>( layout_constraints.push(CssConstraint::Size(SizeConstraint::MaxWidth(max_height.0.to_pixels()), Strength(REQUIRED))); } + if let Some(previous_sibling) = dom_node.previous_sibling { let previous_sibling = ui_solver.get_rect_constraints(previous_sibling).unwrap(); layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignTop(previous_sibling.bottom), Strength(REQUIRED), Padding(0.0))); } else { - layout_constraints.push(CssConstraint::Size(SizeConstraint::TopLeft(Point::new(0.0, 0.0)), Strength(STRONG))); + layout_constraints.push(CssConstraint::Size(SizeConstraint::TopLeft(Point::new(100.0, 100.0)), Strength(STRONG))); + } + + if let Some(next_sibling) = dom_node.next_sibling { + let next_sibling = ui_solver.get_rect_constraints(next_sibling).unwrap(); + layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignBelow(next_sibling.top), Strength(REQUIRED), Padding(0.0))); + } layout_constraints From 221e6c6a79971887b2b56f810a99eb43bda25d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 25 Aug 2018 17:19:35 +0200 Subject: [PATCH 226/868] Fixed layout, removed useless constraints.rs file --- README.md | 3 +- examples/debug.rs | 26 +---- src/cache.rs | 11 +- src/constraints.rs | 272 -------------------------------------------- src/display_list.rs | 54 +++++---- src/lib.rs | 2 - src/ui_solver.rs | 63 +++++----- 7 files changed, 72 insertions(+), 359 deletions(-) delete mode 100644 src/constraints.rs diff --git a/README.md b/README.md index e7b2b09fd..2b2708019 100644 --- a/README.md +++ b/README.md @@ -498,8 +498,7 @@ Several projects have helped azul severely and should be noted here: - Chris Tollidays [limn](https://github.com/christolliday/limn) framework has helped severely with discovering undocumented parts of webrender as well as working with - constraints (the `constraints.rs` file was copied from limn with the [permission of - the author](https://github.com/christolliday/limn/issues/22#issuecomment-362545167)) + constraints. - Nicolas Silva for his work on [lyon](https://github.com/nical/lyon) - without this, the SVG renderer wouldn't have been possible - All webrender contributors who have been patient enough to answer my questions on IRC diff --git a/examples/debug.rs b/examples/debug.rs index e7764490c..5def79890 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -48,15 +48,10 @@ impl Layout for MyAppData { .with_callback(On::Scroll, Callback(scroll_map_contents)) .with_callback(On::MouseOver, Callback(check_hovered_font)) } else { - /* // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file Button::with_label("Load SVG file...").dom() .with_callback(On::LeftMouseUp, Callback(my_button_click_handler)) - */ - Dom::new(NodeType::Div).with_id("wrapper_1") - .with_child(Dom::new(NodeType::Div).with_id("red")) - .with_child(Dom::new(NodeType::Div).with_id("green")) } } } @@ -201,26 +196,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { - // should show a large red rectangle at the top - // and a small green rectangle at the bottom - let css = Css::new_from_str(" - - #red { - background-color: red; - min-width: 100px; - width: 400px; - max-width: 300px; - height: 200px; - } - - #green { - background-color: green; - width: 50px; - height: 50px; - } - ").unwrap(); - let mut app = App::new(MyAppData { map: None }, AppConfig::default()); - app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/cache.rs b/src/cache.rs index f28d104e1..d423d58ce 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -39,9 +39,8 @@ use std::{ ops::Deref, collections::BTreeMap, }; -use cassowary::Solver; use { - constraints::RectConstraintVariables, + ui_solver::RectConstraintVariables, id_tree::{NodeId, Arena}, traits::Layout, dom::NodeData, @@ -243,7 +242,7 @@ impl EditVariableCache { } } - pub(crate) fn initialize_new_rectangles(&mut self, solver: &mut Solver, rects: &DomChangeSet) { + pub(crate) fn initialize_new_rectangles(&mut self, rects: &DomChangeSet) { use std::collections::btree_map::Entry::*; for dom_hash in rects.added_nodes.values() { @@ -255,7 +254,6 @@ impl EditVariableCache { }, Vacant(e) => { let rect = RectConstraintVariables::default(); - rect.add_to_solver(solver); e.insert((true, rect)); } } @@ -264,13 +262,12 @@ impl EditVariableCache { /// Last step of the caching algorithm: /// Remove all edit variables where the `bool` is set to false - pub(crate) fn remove_unused_variables(&mut self, solver: &mut Solver) { + pub(crate) fn remove_unused_variables(&mut self) { let mut to_be_removed = Vec::::new(); - for (key, &(active, variable_rect)) in &self.map { + for (key, &(active, _)) in &self.map { if !active { - variable_rect.remove_from_solver(solver); to_be_removed.push(*key); } } diff --git a/src/constraints.rs b/src/constraints.rs deleted file mode 100644 index f31a385b5..000000000 --- a/src/constraints.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! Constraint building (mostly taken from `limn_layout`) - -use cassowary::{ - Solver, Variable, Constraint, - WeightedRelation::{EQ, GE, LE}, - strength::{WEAK, REQUIRED}, -}; -use euclid::{Point2D, Size2D}; - -pub type Size = Size2D; -pub type Point = Point2D; - -/// A set of cassowary `Variable`s representing the -/// bounding rectangle of a layout. -#[derive(Debug, Copy, Clone)] -pub(crate) struct RectConstraintVariables { - pub left: Variable, - pub top: Variable, - pub right: Variable, - pub bottom: Variable, - pub width: Variable, - pub height: Variable, -} - -impl Default for RectConstraintVariables { - fn default() -> Self { - Self { - left: Variable::new(), - top: Variable::new(), - right: Variable::new(), - bottom: Variable::new(), - width: Variable::new(), - height: Variable::new(), - } - } -} - -impl RectConstraintVariables { - - pub fn add_to_solver(&self, solver: &mut Solver) { - solver.add_edit_variable(self.left, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.top, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.right, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.bottom, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.width, WEAK).unwrap_or_else(|_e| { }); - solver.add_edit_variable(self.height, WEAK).unwrap_or_else(|_e| { }); - } - - pub fn remove_from_solver(&self, solver: &mut Solver) { - solver.remove_edit_variable(self.left).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.top).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.right).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.bottom).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.width).unwrap_or_else(|_e| { }); - solver.remove_edit_variable(self.height).unwrap_or_else(|_e| { }); - } - -} - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Strength(pub f64); - -#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub struct Padding(pub f32); - -#[derive(Debug, Copy, Clone)] -pub(crate) enum CssConstraint { - Size(SizeConstraint, Strength), - Padding(PaddingConstraint, Strength, Padding) -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum SizeConstraint { - Width(f32), - Height(f32), - MinWidth(f32), - MinHeight(f32), - MaxWidth(f32), - MaxHeight(f32), - Size(Size), - MinSize(Size), - AspectRatio(f32), - Shrink, - ShrinkHorizontal, - ShrinkVertical, - TopLeft(Point), - Center(RectConstraintVariables), - CenterHorizontal(Variable, Variable), - CenterVertical(Variable, Variable), -} - -#[derive(Debug, Copy, Clone)] -pub(crate) enum PaddingConstraint { - AlignTop(Variable), - AlignBottom(Variable), - AlignLeft(Variable), - AlignRight(Variable), - AlignAbove(Variable), - AlignBelow(Variable), - AlignToLeftOf(Variable), - AlignToRightOf(Variable), - Above(Variable), - Below(Variable), - ToLeftOf(Variable), - ToRightOf(Variable), - BoundLeft(Variable), - BoundTop(Variable), - BoundRight(Variable), - BoundBottom(Variable), - BoundBy(RectConstraintVariables), - MatchLayout(RectConstraintVariables), - MatchWidth(Variable), - MatchHeight(Variable), -} - -impl SizeConstraint { - pub(crate) fn build(&self, rect: &RectConstraintVariables, strength: f64) -> Vec { - use self::SizeConstraint::*; - - match *self { - Width(width) => { - vec![ rect.width | EQ(strength) | width ] - }, - Height(height) => { - vec![ rect.height | EQ(strength) | height ] - }, - MinWidth(width) => { - vec![ rect.width | GE(strength) | width ] - }, - MinHeight(height) => { - vec![ rect.height | GE(strength) | height ] - }, - MaxWidth(width) => { - vec![ rect.width | LE(strength) | width ] - }, - MaxHeight(height) => { - vec![ rect.height | LE(strength) | height ] - }, - Size(size) => { - vec![ - rect.width | EQ(strength) | size.width, - rect.height | EQ(strength) | size.height, - ] - }, - MinSize(size) => { - vec![ - rect.width | GE(strength) | size.width, - rect.height | GE(strength) | size.height, - ] - }, - AspectRatio(aspect_ratio) => { - vec![ aspect_ratio * rect.width | EQ(strength) | rect.height ] - }, - Shrink => { - vec![ - rect.width | EQ(strength) | 0.0, - rect.height | EQ(strength) | 0.0, - ] - }, - ShrinkHorizontal => { - vec![ rect.width | EQ(strength) | 0.0 ] - }, - ShrinkVertical => { - vec![ rect.height | EQ(strength) | 0.0 ] - }, - TopLeft(point) => { - vec![ - rect.left | EQ(strength) | point.x, - rect.top | EQ(strength) | point.y, - ] - }, - Center(other) => { - vec![ - rect.left - other.left | EQ(REQUIRED) | other.right - rect.right, - rect.top - other.top | EQ(REQUIRED) | other.bottom - rect.bottom, - ] - }, - CenterHorizontal(left, right) => { - vec![ rect.left - left | EQ(REQUIRED) | right - rect.right ] - }, - CenterVertical(top, bottom) => { - vec![ rect.top - top | EQ(REQUIRED) | bottom - rect.bottom ] - }, - } - } -} - -impl PaddingConstraint { - pub(crate) fn build(&self, rect: &RectConstraintVariables, strength: f64, padding: f32) -> Vec { - use self::PaddingConstraint::*; - match *self { - AlignTop(top) => { - vec![ rect.top - top | EQ(strength) | padding ] - }, - AlignBottom(bottom) => { - vec![ bottom - rect.bottom | EQ(strength) | padding ] - }, - AlignLeft(left) => { - vec![ rect.left - left | EQ(strength) | padding ] - }, - AlignRight(right) => { - vec![ right - rect.right | EQ(strength) | padding ] - }, - AlignAbove(top) => { - vec![ top - rect.bottom | EQ(strength) | padding ] - }, - AlignBelow(bottom) => { - vec![ rect.top - bottom | EQ(strength) | padding ] - }, - AlignToLeftOf(left) => { - vec![ left - rect.right | EQ(strength) | padding ] - }, - AlignToRightOf(right) => { - vec![ rect.left - right | EQ(strength) | padding ] - }, - Above(top) => { - vec![ top - rect.bottom | GE(strength) | padding ] - }, - Below(bottom) => { - vec![ rect.top - bottom | GE(strength) | padding ] - }, - ToLeftOf(left) => { - vec![ left - rect.right | GE(strength) | padding ] - }, - ToRightOf(right) => { - vec![ rect.left - right | GE(strength) | padding ] - }, - BoundLeft(left) => { - vec![ rect.left - left | GE(strength) | padding ] - }, - BoundTop(top) => { - vec![ rect.top - top | GE(strength) | padding ] - }, - BoundRight(right) => { - vec![ right - rect.right | GE(strength) | padding ] - }, - BoundBottom(bottom) => { - vec![ bottom - rect.bottom | GE(strength) | padding ] - }, - BoundBy(other) => { - vec![ - rect.left - other.left | GE(strength) | padding, - rect.top - other.top | GE(strength) | padding, - other.right - rect.right | GE(strength) | padding, - other.bottom - rect.bottom | GE(strength) | padding, - ] - }, - MatchLayout(other) => { - vec![ - rect.left - other.left | EQ(strength) | padding, - rect.top - other.top | EQ(strength) | padding, - other.right - rect.right | EQ(strength) | padding, - other.bottom - rect.bottom | EQ(strength) | padding, - ] - }, - MatchWidth(width) => { - vec![ width - rect.width | EQ(strength) | padding ] - }, - MatchHeight(height) => { - vec![ height - rect.height | EQ(strength) | padding ] - }, - } - } -} - -// Empty test, for some reason codecov doesn't detect any files (and therefore -// doesn't report codecov % correctly) except if they have at least one test in -// the file. This is an empty test, which should be updated later on -#[test] -fn __codecov_test_constraints_file() { - -} \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index eccb58e46..c9fcb4c55 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -8,7 +8,6 @@ use { FastHashMap, resources::AppResources, traits::Layout, - constraints::CssConstraint, ui_description::{UiDescription, StyledNode}, ui_solver::UiSolver, window_state::WindowSize, @@ -253,7 +252,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { &ui_solver, ); - ui_solver.insert_css_constraints_for_rect(rect_idx, &layout_contraints); + ui_solver.insert_css_constraints_for_rect(&layout_contraints); } // If we push or pop constraints that means we also need to re-layout the window @@ -968,22 +967,24 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash } } +use cassowary::Constraint; + // Returns the constraints for one rectangle fn create_layout_constraints<'a, T: Layout>( node_id: NodeId, display_rectangles: &Arena>, dom: &Arena>, ui_solver: &UiSolver) --> Vec +-> Vec { - use cassowary::strength::*; - use constraints::{SizeConstraint, PaddingConstraint, Strength, Padding, Point}; - - const AZ_WEAK: f64 = 3.0; - const AZ_MEDIUM: f64 = 30.0; - const AZ_STRONG: f64 = 300.0; + use cassowary::{ + WeightedRelation::{EQ, GE, LE}, + strength::{MEDIUM, STRONG, REQUIRED}, + }; let rect = &display_rectangles[node_id].data; + let self_rect = ui_solver.get_rect_constraints(node_id).unwrap(); + let dom_node = &dom[node_id]; let mut layout_constraints = Vec::new(); @@ -992,37 +993,46 @@ fn create_layout_constraints<'a, T: Layout>( // // min-width and max-width are stronger than width because the width has to be between min and max width if let Some(min_width) = rect.layout.min_width { - layout_constraints.push(CssConstraint::Size(SizeConstraint::MinWidth(min_width.0.to_pixels()), Strength(REQUIRED))); + layout_constraints.push(self_rect.width | GE(REQUIRED) | min_width.0.to_pixels()); } if let Some(width) = rect.layout.width { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Width(width.0.to_pixels()), Strength(STRONG))); + layout_constraints.push(self_rect.width | EQ(STRONG) | width.0.to_pixels()); } if let Some(max_width) = rect.layout.max_width { - layout_constraints.push(CssConstraint::Size(SizeConstraint::MaxWidth(max_width.0.to_pixels()), Strength(REQUIRED))); + layout_constraints.push(self_rect.width | LE(REQUIRED) | max_width.0.to_pixels()); } if let Some(min_height) = rect.layout.min_height { - layout_constraints.push(CssConstraint::Size(SizeConstraint::MinHeight(min_height.0.to_pixels()), Strength(REQUIRED))); + layout_constraints.push(self_rect.height | GE(REQUIRED) | min_height.0.to_pixels()); } if let Some(height) = rect.layout.height { - layout_constraints.push(CssConstraint::Size(SizeConstraint::Height(height.0.to_pixels()), Strength(STRONG))); + layout_constraints.push(self_rect.height | EQ(STRONG) | height.0.to_pixels()); } if let Some(max_height) = rect.layout.max_height { - layout_constraints.push(CssConstraint::Size(SizeConstraint::MaxWidth(max_height.0.to_pixels()), Strength(REQUIRED))); + layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } - - if let Some(previous_sibling) = dom_node.previous_sibling { - let previous_sibling = ui_solver.get_rect_constraints(previous_sibling).unwrap(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignTop(previous_sibling.bottom), Strength(REQUIRED), Padding(0.0))); + if let Some(parent) = dom_node.parent { + let parent = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(self_rect.top | GE(STRONG) | parent.top); + layout_constraints.push(self_rect.left | GE(STRONG) | parent.left); + layout_constraints.push(self_rect.height | EQ(MEDIUM) | parent.height); + layout_constraints.push(self_rect.width | EQ(MEDIUM) | parent.width); } else { - layout_constraints.push(CssConstraint::Size(SizeConstraint::TopLeft(Point::new(100.0, 100.0)), Strength(STRONG))); + let window_constraints = ui_solver.get_window_constraints(); + layout_constraints.push(self_rect.width | EQ(STRONG / 2.0) | window_constraints.width_var); + layout_constraints.push(self_rect.height | EQ(STRONG / 2.0) | window_constraints.height_var); + } + + if let Some(child) = dom_node.first_child { + let child = ui_solver.get_rect_constraints(child).unwrap(); + layout_constraints.push(child.top | EQ(STRONG) | 100.0); + layout_constraints.push(child.left | EQ(STRONG) | 100.0); } if let Some(next_sibling) = dom_node.next_sibling { let next_sibling = ui_solver.get_rect_constraints(next_sibling).unwrap(); - layout_constraints.push(CssConstraint::Padding(PaddingConstraint::AlignBelow(next_sibling.top), Strength(REQUIRED), Padding(0.0))); - + layout_constraints.push((self_rect.top + self_rect.height) | GE(REQUIRED) | next_sibling.top); } layout_constraints diff --git a/src/lib.rs b/src/lib.rs index 4f1da7738..3675dd3f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,8 +92,6 @@ mod css; mod resources; /// UI Description & display list handling (webrender) mod ui_description; -/// Constraint handling -mod constraints; /// Converts the UI description (the styled HTML nodes) /// to an actual display list (+ layout) mod display_list; diff --git a/src/ui_solver.rs b/src/ui_solver.rs index 60d43dbdf..2cc4b1398 100644 --- a/src/ui_solver.rs +++ b/src/ui_solver.rs @@ -10,10 +10,38 @@ use { id_tree::{NodeId, Arena}, dom::NodeData, cache::{EditVariableCache, DomTreeCache, DomChangeSet}, - constraints::{CssConstraint, RectConstraintVariables}, traits::Layout, }; +/// A set of cassowary `Variable`s representing the +/// bounding rectangle of a layout. +#[derive(Debug, Copy, Clone)] +pub(crate) struct RectConstraintVariables { + pub left: Variable, + pub top: Variable, + pub width: Variable, + pub height: Variable, +} + +impl Default for RectConstraintVariables { + fn default() -> Self { + Self { + left: Variable::new(), + top: Variable::new(), + width: Variable::new(), + height: Variable::new(), + } + } +} + +// Empty test, for some reason codecov doesn't detect any files (and therefore +// doesn't report codecov % correctly) except if they have at least one test in +// the file. This is an empty test, which should be updated later on +#[test] +fn __codecov_test_constraints_file() { + +} + /// Stores the variables of the root width and height (but not the values themselves) #[derive(Debug, Copy, Clone, PartialEq)] pub(crate) struct WindowSizeConstraints { @@ -67,21 +95,13 @@ impl UiSolver { pub(crate) fn update_dom(&mut self, root: &NodeId, arena: &Arena>) -> DomChangeSet { let changeset = self.dom_tree_cache.update(*root, arena); - self.edit_variable_cache.initialize_new_rectangles(&mut self.solver, &changeset); - self.edit_variable_cache.remove_unused_variables(&mut self.solver); + self.edit_variable_cache.initialize_new_rectangles(&changeset); + self.edit_variable_cache.remove_unused_variables(); changeset } - pub(crate) fn insert_css_constraints_for_rect(&mut self, rect_idx: NodeId, constraints: &[CssConstraint]) { - use cassowary::strength::*; - use constraints::{SizeConstraint, Strength, Point}; - - let dom_hash = &self.dom_tree_cache.previous_layout.arena[rect_idx]; - let display_rect = &self.edit_variable_cache.map[&dom_hash.data].1; - - println!("display_rect {} - variables: {:#?}", rect_idx, display_rect); - - self.solver.add_constraints(&css_constraints_to_cassowary_constraints(display_rect, constraints)).unwrap(); + pub(crate) fn insert_css_constraints_for_rect(&mut self, constraints: &[Constraint]) { + self.solver.add_constraints(constraints).unwrap(); } /// Notifies the solver that the window size has changed @@ -106,7 +126,7 @@ impl UiSolver { let width = self.solved_values.get(&display_rect.width).and_then(|x| Some(*x)).unwrap_or(0.0); let height = self.solved_values.get(&display_rect.height).and_then(|x| Some(*x)).unwrap_or(0.0); - TypedRect::new(TypedPoint2D::new(top as f32, left as f32), TypedSize2D::new(width as f32, height as f32)) + TypedRect::new(TypedPoint2D::new(left as f32, top as f32), TypedSize2D::new(width as f32, height as f32)) } pub(crate) fn get_rect_constraints(&self, rect_id: NodeId) -> Option { @@ -117,19 +137,4 @@ impl UiSolver { pub(crate) fn get_window_constraints(&self) -> WindowSizeConstraints { self.window_constraints } -} - -fn css_constraints_to_cassowary_constraints(rect: &RectConstraintVariables, css: &[CssConstraint]) --> Vec -{ - css.iter().flat_map(|constraint| - match *constraint { - CssConstraint::Size(constraint, strength) => { - constraint.build(&rect, strength.0) - } - CssConstraint::Padding(constraint, strength, padding) => { - constraint.build(&rect, strength.0, padding.0) - } - } - ).collect() } \ No newline at end of file From 6e547fedacf427e54333beae5218361a08ff05de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 04:21:49 +0200 Subject: [PATCH 227/868] Fixed crash in text layout --- README.md | 10 ++++------ examples/test.rs | 39 +++++++++++++++++++++++++++++++++++++++ src/display_list.rs | 6 ++---- src/text_layout.rs | 36 +++++++++++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 examples/test.rs diff --git a/README.md b/README.md index e7b2b09fd..ea9e505c7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -# azul - -# WARNING: The features advertised don't work yet. -# See the /examples folder for an example of what's currently possible. - -[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +# azul [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) [![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) [![Rust Compiler Version](https://img.shields.io/badge/rustc-1.28%20stable-blue.svg)]() +# WARNING: The features advertised don't work yet. +# See the /examples folder for an example of what's currently possible. + azul is a cross-platform, stylable GUI framework using Mozillas `webrender` engine for rendering and a CSS / DOM model for layout and rendering diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 000000000..c702a5eae --- /dev/null +++ b/examples/test.rs @@ -0,0 +1,39 @@ +extern crate azul; + +use azul::prelude::*; + +struct MyDataModel { + counter: usize, +} + +impl Layout for MyDataModel { + fn layout(&self, _info: WindowInfo) -> Dom { + Dom::new(NodeType::Div).with_id("wrapper") + .with_child(Dom::new(NodeType::Label(format!("{}", self.counter))).with_id("red")) + .with_child(Dom::new(NodeType::Div).with_id("green")) + } +} + +fn main() { + + let css = Css::new_from_str(" + #wrapper { + background-color: blue; + } + #red { + background-color: red; + color: white; + font-size: 10px; + font-family: sans-serif; + width: 200px; + height: 200px; + } + #green { + background-color: green; + } + ").unwrap(); + + let mut app = App::new(MyDataModel { counter: 0 }, AppConfig::default()); + app.create_window(WindowCreateOptions::default(), css).unwrap(); + app.run().unwrap(); +} \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index 9d9d16fd2..a81f0eecf 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -263,11 +263,9 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // TODO: early return based on changeset? // Recalculate the actual layout - if css.needs_relayout || has_window_size_changed { - + if has_window_size_changed { + ui_solver.update_window_size(&window_size.dimensions); } - - ui_solver.update_window_size(&window_size.dimensions); ui_solver.update_layout_cache(); css.needs_relayout = false; diff --git a/src/text_layout.rs b/src/text_layout.rs index 69368f99d..fc17f58b3 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -28,7 +28,10 @@ pub const PX_TO_PT: f32 = 72.0 / 96.0; /// Be careful when caching this - the `Words` are independent of the /// original font, so be sure to note the font ID if you cache this struct. #[derive(Debug, Clone)] -pub struct Words(Vec); +pub struct Words { + pub items: Vec, + pub longest_word_width: f32, +} /// A `Word` contains information about the layout of a single word #[derive(Debug, Clone)] @@ -183,6 +186,8 @@ pub(crate) fn get_glyphs( scrollbar_info: &ScrollbarInfo) -> (Vec, TextOverflowPass2) { + let mut bounds = *bounds; + let target_font = match app_resources.get_font(target_font_id) { Some(s) => s, None => panic!("Drawing with invalid font!: {:?}", target_font_id), @@ -207,6 +212,11 @@ pub(crate) fn get_glyphs( }, }; + // Prevent negative width / rect height or a too small rectangle - + // the rect must be at least wide enough for the longest word + bounds.size.width = bounds.size.width.max(words.longest_word_width); + bounds.size.height = bounds.size.height.abs(); + // (2) Calculate the additions / subtractions that have to be take into account // let harfbuzz_adjustments = calculate_harfbuzz_adjustments(&text, &target_font.0); @@ -342,7 +352,7 @@ fn scale_words(words: &mut Words, scale_factor: f32) { // we simply scale each glyph position by 13 / 12. This is faster than // re-calculating the font metrics (from Rusttype) each time we scale a // large amount of text. - for word in words.0.iter_mut() { + for word in words.items.iter_mut() { if let SemanticWordItem::Word(ref mut w) = word { w.glyphs.iter_mut().for_each(|g| g.point.x *= scale_factor); w.total_width *= scale_factor; @@ -366,10 +376,15 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: let mut glyphs_in_this_word = Vec::new(); let mut last_glyph = None; + // In case the rectangle is smaller than the longest word, + // we need to expand the rectangle to be that size + let mut longest_word_width = 0.0; + fn end_word(words: &mut Vec, glyphs_in_this_word: &mut Vec, cur_word_length: &mut f32, word_caret: &mut f32, + longest_word_width: &mut f32, last_glyph: &mut Option) { // End of word @@ -378,6 +393,10 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: total_width: *cur_word_length, })); + if cur_word_length > longest_word_width { + *longest_word_width = *cur_word_length; + } + // Reset everything *last_glyph = None; *word_caret = 0.0; @@ -399,6 +418,7 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, + &mut longest_word_width, &mut last_glyph); } words.push(SemanticWordItem::Tab); @@ -411,6 +431,7 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, + &mut longest_word_width, &mut last_glyph); } words.push(SemanticWordItem::Return); @@ -422,6 +443,7 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, + &mut longest_word_width, &mut last_glyph); } }, @@ -466,10 +488,14 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: &mut glyphs_in_this_word, &mut cur_word_length, &mut word_caret, + &mut longest_word_width, &mut last_glyph); } - Words(words) + Words { + items: words, + longest_word_width: longest_word_width, + } } // First pass: calculate if the words will overflow (using the tabs) @@ -483,7 +509,7 @@ fn estimate_overflow_pass_1( { use self::SemanticWordItem::*; - let words = &words.0; + let words = &words.items; let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; @@ -666,7 +692,7 @@ fn words_to_left_aligned_glyphs<'a>( font_metrics: &FontMetrics) -> (Vec, Vec<(usize, f32)>, f32, f32) { - let words = &words.0; + let words = &words.items; let FontMetrics { space_width, tab_width, vertical_advance, offset_top, font_size_no_line_height, .. } = *font_metrics; From a2afe6ca8993df88ef152c5c897c9f7baad5621d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 04:45:28 +0200 Subject: [PATCH 228/868] Fixed test example layout --- src/display_list.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 2d5f2d5b1..6d64f275a 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -687,7 +687,7 @@ fn push_triangle( tag: None, }; - const TRANSPARENT: ColorU = ColorU { r: 0, b: 0, g: 0, a: 0 }; + const TRANSPARENT: ColorU = ColorU { r: 0, b: 0, g: 0, a: 0 }; // make all borders but one transparent let [b_left, b_right, b_top, b_bottom] = match direction { @@ -977,7 +977,7 @@ fn create_layout_constraints<'a, T: Layout>( { use cassowary::{ WeightedRelation::{EQ, GE, LE}, - strength::{MEDIUM, STRONG, REQUIRED}, + strength::*, }; let rect = &display_rectangles[node_id].data; @@ -1011,23 +1011,19 @@ fn create_layout_constraints<'a, T: Layout>( } if let Some(parent) = dom_node.parent { + // child element: try to fit the parent width / height let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.top | GE(STRONG) | parent.top); - layout_constraints.push(self_rect.left | GE(STRONG) | parent.left); - layout_constraints.push(self_rect.height | EQ(MEDIUM) | parent.height); - layout_constraints.push(self_rect.width | EQ(MEDIUM) | parent.width); + layout_constraints.push(self_rect.top | GE(STRONG / 2.0) | parent.top); + layout_constraints.push(self_rect.left | GE(STRONG / 2.0) | parent.left); + layout_constraints.push(self_rect.height | EQ(WEAK) | parent.height); + layout_constraints.push(self_rect.width | EQ(WEAK) | parent.width); } else { + // root element: fill window width / height let window_constraints = ui_solver.get_window_constraints(); layout_constraints.push(self_rect.width | EQ(STRONG / 2.0) | window_constraints.width_var); layout_constraints.push(self_rect.height | EQ(STRONG / 2.0) | window_constraints.height_var); } - if let Some(child) = dom_node.first_child { - let child = ui_solver.get_rect_constraints(child).unwrap(); - layout_constraints.push(child.top | EQ(STRONG) | 100.0); - layout_constraints.push(child.left | EQ(STRONG) | 100.0); - } - if let Some(next_sibling) = dom_node.next_sibling { let next_sibling = ui_solver.get_rect_constraints(next_sibling).unwrap(); layout_constraints.push((self_rect.top + self_rect.height) | GE(REQUIRED) | next_sibling.top); From 14a3743d8359ca6839de9ec1afc3ef73347682b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 07:10:24 +0200 Subject: [PATCH 229/868] Adjusted the rendering, added proper constraints for row / column layout --- examples/test.rs | 13 +++-- src/css_parser.rs | 24 ++++++++-- src/display_list.rs | 113 +++++++++++++++++++++++++++++++------------- 3 files changed, 107 insertions(+), 43 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index c702a5eae..fc303c39a 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -19,17 +19,20 @@ fn main() { let css = Css::new_from_str(" #wrapper { background-color: blue; + flex-direction: column; + } + #green { + background-color: green; + width: 200px; + height: 200px; } #red { background-color: red; color: white; font-size: 10px; font-family: sans-serif; - width: 200px; - height: 200px; - } - #green { - background-color: green; + width: 50px; + height: 50px; } ").unwrap(); diff --git a/src/css_parser.rs b/src/css_parser.rs index ef82e92ba..6a81c45e7 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1512,8 +1512,16 @@ pub struct LineHeight(pub PercentageValue); #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutDirection { - Horizontal, - Vertical, + Row, + RowReverse, + Column, + ColumnReverse, +} + +impl Default for LayoutDirection { + fn default() -> Self { + LayoutDirection::Column + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -1522,6 +1530,12 @@ pub enum LayoutWrap { NoWrap, } +impl Default for LayoutWrap { + fn default() -> Self { + LayoutWrap::Wrap + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LayoutJustifyContent { /// Default value. Items are positioned at the beginning of the container @@ -1756,8 +1770,10 @@ pub(crate) fn parse_css_font_family<'a>(input: &'a str) -> Result DisplayList<'a, T> { if changeset.is_empty() { None } else { Some(changeset) } }); + let root = match self.ui_descr.ui_descr_root { + Some(r) => r, + None => panic!("Dom has no root element!"), + }; + if css.needs_relayout { // Constraints were added or removed during the last frame - for rect_idx in self.rectangles.linear_iter() { - - let layout_contraints = create_layout_constraints( + for rect_idx in root.following_siblings(&self.rectangles) { + let constraints = create_layout_constraints( rect_idx, &self.rectangles, &*self.ui_descr.ui_descr_arena.borrow(), &ui_solver, ); - - ui_solver.insert_css_constraints_for_rect(&layout_contraints); + ui_solver.insert_css_constraints_for_rect(&constraints); + for child_idx in rect_idx.children(&self.rectangles) { + let constraints = create_layout_constraints( + child_idx, + &self.rectangles, + &*self.ui_descr.ui_descr_arena.borrow(), + &ui_solver, + ); + ui_solver.insert_css_constraints_for_rect(&constraints); + } } // If we push or pop constraints that means we also need to re-layout the window @@ -277,27 +289,34 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { // Upload image and font resources Self::update_resources(render_api, app_resources, &mut resource_updates); - for rect_idx in self.rectangles.linear_iter() { - - let arena = self.ui_descr.ui_descr_arena.borrow(); - let node_type = &arena[rect_idx].data.node_type; + let arena = self.ui_descr.ui_descr_arena.borrow(); - // Ask the solver what the bounds of the current rectangle is - let bounds = ui_solver.query_bounds_of_rect(rect_idx); - println!("id: {} - bounds: {}", rect_idx, bounds); - - // temporary: fill the whole window with each rectangle + for rect_idx in root.following_siblings(&self.rectangles) { displaylist_handle_rect( &mut builder, current_epoch, rect_idx, &self.rectangles, - node_type, - bounds, + &arena[rect_idx].data.node_type, + ui_solver.query_bounds_of_rect(rect_idx), full_screen_rect, app_resources, render_api, &mut resource_updates); + + for child_idx in rect_idx.reverse_children(&self.rectangles) { + displaylist_handle_rect( + &mut builder, + current_epoch, + child_idx, + &self.rectangles, + &arena[child_idx].data.node_type, + ui_solver.query_bounds_of_rect(child_idx), + full_screen_rect, + app_resources, + render_api, + &mut resource_updates); + } } render_api.update_resources(resource_updates); @@ -462,14 +481,14 @@ fn determine_text_alignment<'a>(rect_idx: NodeId, arena: &Arena { + Row | RowReverse => { horz_alignment = match justify_content { Start => TextAlignmentHorz::Left, End => TextAlignmentHorz::Right, Center | SpaceBetween | SpaceAround => TextAlignmentHorz::Center, }; }, - Vertical => { + Column | ColumnReverse => { vert_alignment = match justify_content { Start => TextAlignmentVert::Top, End => TextAlignmentVert::Bottom, @@ -977,9 +996,15 @@ fn create_layout_constraints<'a, T: Layout>( { use cassowary::{ WeightedRelation::{EQ, GE, LE}, - strength::*, }; + use std::f64; + + const WEAK: f64 = 3.0; + const MEDIUM: f64 = 30.0; + const STRONG: f64 = 300.0; + const REQUIRED: f64 = f64::MAX; + let rect = &display_rectangles[node_id].data; let self_rect = ui_solver.get_rect_constraints(node_id).unwrap(); @@ -1009,26 +1034,46 @@ fn create_layout_constraints<'a, T: Layout>( if let Some(max_height) = rect.layout.max_height { layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } - - if let Some(parent) = dom_node.parent { - // child element: try to fit the parent width / height - let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.top | GE(STRONG / 2.0) | parent.top); - layout_constraints.push(self_rect.left | GE(STRONG / 2.0) | parent.left); - layout_constraints.push(self_rect.height | EQ(WEAK) | parent.height); - layout_constraints.push(self_rect.width | EQ(WEAK) | parent.width); - } else { - // root element: fill window width / height + + if dom_node.parent.is_none() { + // Root node: fill window width / height let window_constraints = ui_solver.get_window_constraints(); - layout_constraints.push(self_rect.width | EQ(STRONG / 2.0) | window_constraints.width_var); - layout_constraints.push(self_rect.height | EQ(STRONG / 2.0) | window_constraints.height_var); + layout_constraints.push(self_rect.top | EQ(REQUIRED) | 0.0); + layout_constraints.push(self_rect.left | EQ(REQUIRED) | 0.0); + layout_constraints.push(self_rect.width | EQ(REQUIRED) | window_constraints.width_var); + layout_constraints.push(self_rect.height | EQ(REQUIRED) | window_constraints.height_var); } - if let Some(next_sibling) = dom_node.next_sibling { - let next_sibling = ui_solver.get_rect_constraints(next_sibling).unwrap(); - layout_constraints.push((self_rect.top + self_rect.height) | GE(REQUIRED) | next_sibling.top); + let direction = rect.layout.direction.unwrap_or_default(); + + let mut next_child_id = dom_node.first_child; + while let Some(child_id) = next_child_id { + let child = ui_solver.get_rect_constraints(child_id).unwrap(); + + match direction { + LayoutDirection::Row => { + layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); + layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); + }, + LayoutDirection::RowReverse => { + layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); + layout_constraints.push(child.left | EQ(STRONG) | (self_rect.left + (self_rect.width - child.width))); + }, + LayoutDirection::Column => { + layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); + layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); + }, + LayoutDirection::ColumnReverse => { + layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); + layout_constraints.push(child.top | EQ(STRONG) | (self_rect.top + (self_rect.height - child.height))); + }, + } + + next_child_id = dom[child_id].next_sibling; } + println!("direction: {:?}", direction); + layout_constraints } From 1f86826a1a6c4a2651a41390d033e50672f535ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 07:19:55 +0200 Subject: [PATCH 230/868] row layout is stable --- examples/test.rs | 2 +- src/display_list.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index fc303c39a..5a6354eb1 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -19,7 +19,7 @@ fn main() { let css = Css::new_from_str(" #wrapper { background-color: blue; - flex-direction: column; + flex-direction: row; } #green { background-color: green; diff --git a/src/display_list.rs b/src/display_list.rs index 335ff8619..bf582410d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -997,7 +997,7 @@ fn create_layout_constraints<'a, T: Layout>( use cassowary::{ WeightedRelation::{EQ, GE, LE}, }; - + use ui_solver::RectConstraintVariables; use std::f64; const WEAK: f64 = 3.0; @@ -1047,13 +1047,18 @@ fn create_layout_constraints<'a, T: Layout>( let direction = rect.layout.direction.unwrap_or_default(); let mut next_child_id = dom_node.first_child; + let mut previous_child: Option = None; + while let Some(child_id) = next_child_id { let child = ui_solver.get_rect_constraints(child_id).unwrap(); match direction { LayoutDirection::Row => { layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); - layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); + match previous_child { + None => layout_constraints.push(child.left | EQ(STRONG) | self_rect.left), + Some(prev) => layout_constraints.push(child.left | EQ(STRONG) | (prev.left + prev.width)), + } }, LayoutDirection::RowReverse => { layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); @@ -1069,6 +1074,7 @@ fn create_layout_constraints<'a, T: Layout>( }, } + previous_child = Some(child); next_child_id = dom[child_id].next_sibling; } From 8c209c6e8b67d120c26abff2aed1a6df9c0b5dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 07:31:29 +0200 Subject: [PATCH 231/868] row-reverse, column & column-reverse now work stable --- examples/test.rs | 8 +++++++- src/display_list.rs | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index 5a6354eb1..3fc941329 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -11,6 +11,7 @@ impl Layout for MyDataModel { Dom::new(NodeType::Div).with_id("wrapper") .with_child(Dom::new(NodeType::Label(format!("{}", self.counter))).with_id("red")) .with_child(Dom::new(NodeType::Div).with_id("green")) + .with_child(Dom::new(NodeType::Div).with_id("yellow")) } } @@ -19,7 +20,7 @@ fn main() { let css = Css::new_from_str(" #wrapper { background-color: blue; - flex-direction: row; + flex-direction: column-reverse; } #green { background-color: green; @@ -34,6 +35,11 @@ fn main() { width: 50px; height: 50px; } + #yellow { + background-color: yellow; + width: 100px; + height: 100px; + } ").unwrap(); let mut app = App::new(MyDataModel { counter: 0 }, AppConfig::default()); diff --git a/src/display_list.rs b/src/display_list.rs index bf582410d..8fa44269e 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1062,15 +1062,24 @@ fn create_layout_constraints<'a, T: Layout>( }, LayoutDirection::RowReverse => { layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); - layout_constraints.push(child.left | EQ(STRONG) | (self_rect.left + (self_rect.width - child.width))); + match previous_child { + None => layout_constraints.push(child.left | EQ(STRONG) | (self_rect.left + (self_rect.width - child.width))), + Some(prev) => layout_constraints.push((child.left + child.width) | EQ(STRONG) | prev.left), + } }, LayoutDirection::Column => { - layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); + match previous_child { + None => layout_constraints.push(child.top | EQ(STRONG) | self_rect.top), + Some(prev) => layout_constraints.push(child.top | EQ(STRONG) | (prev.top + prev.height)), + } layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); }, LayoutDirection::ColumnReverse => { layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); - layout_constraints.push(child.top | EQ(STRONG) | (self_rect.top + (self_rect.height - child.height))); + match previous_child { + None => layout_constraints.push(child.top | EQ(STRONG) | (self_rect.top + (self_rect.height - child.height))), + Some(prev) => layout_constraints.push((child.top + child.height) | EQ(STRONG) | prev.top), + } }, } From b0d1098f06d5599d0f697a5f95628eda31d1fb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 07:46:03 +0200 Subject: [PATCH 232/868] Filling width & height works --- examples/test.rs | 6 +----- src/display_list.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index 3fc941329..42f2a41f0 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -20,12 +20,11 @@ fn main() { let css = Css::new_from_str(" #wrapper { background-color: blue; - flex-direction: column-reverse; + flex-direction: row-reverse; } #green { background-color: green; width: 200px; - height: 200px; } #red { background-color: red; @@ -33,12 +32,9 @@ fn main() { font-size: 10px; font-family: sans-serif; width: 50px; - height: 50px; } #yellow { background-color: yellow; - width: 100px; - height: 100px; } ").unwrap(); diff --git a/src/display_list.rs b/src/display_list.rs index 8fa44269e..e51a35310 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1020,6 +1020,11 @@ fn create_layout_constraints<'a, T: Layout>( } if let Some(width) = rect.layout.width { layout_constraints.push(self_rect.width | EQ(STRONG) | width.0.to_pixels()); + } else { + if let Some(parent) = dom_node.parent { + let parent = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(self_rect.width | EQ(STRONG) | parent.width); + } } if let Some(max_width) = rect.layout.max_width { layout_constraints.push(self_rect.width | LE(REQUIRED) | max_width.0.to_pixels()); @@ -1030,11 +1035,17 @@ fn create_layout_constraints<'a, T: Layout>( } if let Some(height) = rect.layout.height { layout_constraints.push(self_rect.height | EQ(STRONG) | height.0.to_pixels()); + } else { + if let Some(parent) = dom_node.parent { + let parent = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(self_rect.height | EQ(STRONG) | parent.height); + } } if let Some(max_height) = rect.layout.max_height { layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } + if dom_node.parent.is_none() { // Root node: fill window width / height let window_constraints = ui_solver.get_window_constraints(); From a9c51e38ee46d412e77a414e1cd85250f3be998d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 08:08:17 +0200 Subject: [PATCH 233/868] Fixed rendering more than one level of depth --- examples/test.rs | 19 +++++++++++++------ src/display_list.rs | 29 ++--------------------------- src/ui_solver.rs | 1 - 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index 42f2a41f0..b747cc6e3 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -10,8 +10,10 @@ impl Layout for MyDataModel { fn layout(&self, _info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") .with_child(Dom::new(NodeType::Label(format!("{}", self.counter))).with_id("red")) - .with_child(Dom::new(NodeType::Div).with_id("green")) - .with_child(Dom::new(NodeType::Div).with_id("yellow")) + .with_child(Dom::new(NodeType::Div).with_id("green") + .with_child(Dom::new(NodeType::Div).with_id("yellow")) + .with_child(Dom::new(NodeType::Div).with_id("grey")) + ) } } @@ -22,10 +24,6 @@ fn main() { background-color: blue; flex-direction: row-reverse; } - #green { - background-color: green; - width: 200px; - } #red { background-color: red; color: white; @@ -33,8 +31,17 @@ fn main() { font-family: sans-serif; width: 50px; } + #green { + background-color: green; + flex-direction: column; + width: 500px; + } #yellow { background-color: yellow; + height: 200px; + } + #grey { + background-color: grey; } ").unwrap(); diff --git a/src/display_list.rs b/src/display_list.rs index e51a35310..ea4b078eb 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -246,9 +246,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { }; if css.needs_relayout { - - // Constraints were added or removed during the last frame - for rect_idx in root.following_siblings(&self.rectangles) { + for rect_idx in self.rectangles.linear_iter() { let constraints = create_layout_constraints( rect_idx, &self.rectangles, @@ -256,15 +254,6 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { &ui_solver, ); ui_solver.insert_css_constraints_for_rect(&constraints); - for child_idx in rect_idx.children(&self.rectangles) { - let constraints = create_layout_constraints( - child_idx, - &self.rectangles, - &*self.ui_descr.ui_descr_arena.borrow(), - &ui_solver, - ); - ui_solver.insert_css_constraints_for_rect(&constraints); - } } // If we push or pop constraints that means we also need to re-layout the window @@ -291,7 +280,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let arena = self.ui_descr.ui_descr_arena.borrow(); - for rect_idx in root.following_siblings(&self.rectangles) { + for rect_idx in root.descendants(&self.rectangles) { displaylist_handle_rect( &mut builder, current_epoch, @@ -303,20 +292,6 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { app_resources, render_api, &mut resource_updates); - - for child_idx in rect_idx.reverse_children(&self.rectangles) { - displaylist_handle_rect( - &mut builder, - current_epoch, - child_idx, - &self.rectangles, - &arena[child_idx].data.node_type, - ui_solver.query_bounds_of_rect(child_idx), - full_screen_rect, - app_resources, - render_api, - &mut resource_updates); - } } render_api.update_resources(resource_updates); diff --git a/src/ui_solver.rs b/src/ui_solver.rs index 2cc4b1398..f19f122a4 100644 --- a/src/ui_solver.rs +++ b/src/ui_solver.rs @@ -112,7 +112,6 @@ impl UiSolver { pub(crate) fn update_layout_cache(&mut self) { for (variable, solved_value) in self.solver.fetch_changes() { - println!("variable {:?} - solved value: {}", variable, solved_value); self.solved_values.insert(*variable, *solved_value); } } From b5be217d5f7d8f64291cfd51a80c7b3a81ea3422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 09:58:50 +0200 Subject: [PATCH 234/868] "Fixed" the async demo until proper button layout is done --- src/display_list.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index ea4b078eb..1a7a04770 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -245,7 +245,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { None => panic!("Dom has no root element!"), }; - if css.needs_relayout { + if css.needs_relayout || changeset.is_some() { for rect_idx in self.rectangles.linear_iter() { let constraints = create_layout_constraints( rect_idx, @@ -1073,8 +1073,6 @@ fn create_layout_constraints<'a, T: Layout>( next_child_id = dom[child_id].next_sibling; } - println!("direction: {:?}", direction); - layout_constraints } From 8e1887ee01aa472750f7d7f13cebb92cec152900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 12:18:14 +0200 Subject: [PATCH 235/868] Fixed linear-gradient bug --- examples/test.rs | 5 ++--- src/css.rs | 18 ++++++++++++++++++ src/css_parser.rs | 19 +++++++++++++++++-- src/display_list.rs | 18 ++++++++++++++---- src/traits.rs | 18 ++++++++---------- src/ui_description.rs | 9 ++++++++- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index b747cc6e3..be89f8f95 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -21,8 +21,8 @@ fn main() { let css = Css::new_from_str(" #wrapper { - background-color: blue; - flex-direction: row-reverse; + background: linear-gradient(135deg, #004e92 0%,#000428 100%); + flex-direction: row; } #red { background-color: red; @@ -32,7 +32,6 @@ fn main() { width: 50px; } #green { - background-color: green; flex-direction: column; width: 500px; } diff --git a/src/css.rs b/src/css.rs index 29cf45368..872faaffd 100644 --- a/src/css.rs +++ b/src/css.rs @@ -116,6 +116,16 @@ pub(crate) enum CssDeclaration { Dynamic(DynamicCssProperty), } +impl CssDeclaration { + pub fn is_inheritable(&self) -> bool { + use self::CssDeclaration::*; + match self { + Static(s) => s.is_inheritable(), + Dynamic(d) => d.is_inheritable(), + } + } +} + /// A `CssProperty` is a type of CSS Rule, /// but the contents of the rule is dynamic. /// @@ -138,6 +148,14 @@ pub(crate) struct DynamicCssProperty { pub(crate) default: ParsedCssProperty, } +impl DynamicCssProperty { + pub fn is_inheritable(&self) -> bool { + // Since the overridden value has to have the same enum type + // we can just check if the default value is inheritable + self.default.is_inheritable() + } +} + impl CssRule { pub fn needs_relayout(&self) -> bool { // RELAYOUT_RULES.iter().any(|r| self.declaration.0 == *r) diff --git a/src/css_parser.rs b/src/css_parser.rs index 6a81c45e7..f9d95f69c 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -101,6 +101,21 @@ pub enum ParsedCssProperty { Overflow(LayoutOverflow), } +impl ParsedCssProperty { + /// Returns whether this property will be inherited during cascading + pub fn is_inheritable(&self) -> bool { + use self::ParsedCssProperty::*; + match self { + | TextColor(_) + | FontFamily(_) + | FontSize(_) + | LineHeight(_) + | TextAlign(_) => true, + _ => false, + } + } +} + impl_from_no_lifetimes!(BorderRadius, ParsedCssProperty::BorderRadius); impl_from_no_lifetimes!(Background, ParsedCssProperty::Background); impl_from_no_lifetimes!(FontSize, ParsedCssProperty::FontSize); @@ -1028,8 +1043,8 @@ impl Direction { { match *self { Direction::Angle(ref deg) => { - // todo!! - let mut point: LayoutPoint = TypedPoint2D::new(rect.size.width, rect.size.height); + let max = rect.size.width.max(rect.size.height); + let mut point: LayoutPoint = TypedPoint2D::new(max, 0.0); let rot = TypedRotation2D::new(Angle::radians(deg.to_radians())); (LayoutPoint::zero(), rot.transform_point(&point)) }, diff --git a/src/display_list.rs b/src/display_list.rs index 1a7a04770..f95f98159 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -784,12 +784,22 @@ fn push_background( builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, Background::LinearGradient(gradient) => { + let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), color: gradient_pre.color, }).collect(); - let (begin_pt, end_pt) = gradient.direction.to_points(&bounds); + + let (mut begin_pt, mut end_pt) = gradient.direction.to_points(&bounds); + + end_pt.x = -end_pt.x; + + // webrender "normalizes" gradient stops, TODO: file a bug about this? + + begin_pt.x /= 100.0; begin_pt.y /= 100.0; + end_pt.x /= 100.0; end_pt.y /= 100.0; + let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, @@ -998,7 +1008,7 @@ fn create_layout_constraints<'a, T: Layout>( } else { if let Some(parent) = dom_node.parent { let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.width | EQ(STRONG) | parent.width); + layout_constraints.push(self_rect.width | EQ(STRONG) | parent.width); } } if let Some(max_width) = rect.layout.max_width { @@ -1013,13 +1023,13 @@ fn create_layout_constraints<'a, T: Layout>( } else { if let Some(parent) = dom_node.parent { let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.height | EQ(STRONG) | parent.height); + layout_constraints.push(self_rect.height | EQ(STRONG) | parent.height); } } if let Some(max_height) = rect.layout.max_height { layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } - + if dom_node.parent.is_none() { // Root node: fill window width / height diff --git a/src/traits.rs b/src/traits.rs index 02b44c2b5..746c4f539 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -168,7 +168,7 @@ fn match_dom_css_selectors<'a, T: Layout>( { let mut root_constraints = CssConstraintList::default(); for global_rule in &parsed_css.pure_global_rules { - push_rule(&mut root_constraints, global_rule); + root_constraints.push_rule(global_rule); } let arena_borrow = &*(*arena).borrow(); @@ -203,7 +203,10 @@ fn match_dom_css_selectors_inner<'a, T: Layout>( { let mut styled_nodes = BTreeMap::::new(); - let mut current_constraints = parent_constraints.clone(); + let mut current_constraints = CssConstraintList { + list: parent_constraints.list.iter().filter(|prop| prop.is_inheritable()).cloned().collect(), + }; + cascade_constraints(&arena[root].data, &mut current_constraints, parsed_css, css); let current_node = StyledNode { @@ -230,7 +233,7 @@ fn cascade_constraints<'a, T: Layout>( { for div_rule in &parsed_css.pure_div_rules { if *node.node_type.get_css_id() == div_rule.html_type { - push_rule(list, div_rule); + list.push_rule(div_rule); } } @@ -256,7 +259,7 @@ fn cascade_constraints<'a, T: Layout>( } if should_insert_rule { - push_rule(list, class_rule); + list.push_rule(class_rule); } } @@ -267,7 +270,7 @@ fn cascade_constraints<'a, T: Layout>( // if the node has an ID for id_rule in &parsed_css.pure_id_rules { if *id_rule.id.as_ref().unwrap() == *node_id { - push_rule(list, id_rule); + list.push_rule(id_rule); } } } @@ -275,11 +278,6 @@ fn cascade_constraints<'a, T: Layout>( // TODO: all the mixed rules } -#[inline] -fn push_rule(list: &mut CssConstraintList, rule: &CssRule) { - list.list.push(rule.declaration.1.clone()); -} - // Empty test, for some reason codecov doesn't detect any files (and therefore // doesn't report codecov % correctly) except if they have at least one test in // the file. This is an empty test, which should be updated later on diff --git a/src/ui_description.rs b/src/ui_description.rs index 007452024..aceb25897 100644 --- a/src/ui_description.rs +++ b/src/ui_description.rs @@ -9,7 +9,7 @@ use { id_tree::{Arena, NodeId}, traits::Layout, ui_state::UiState, - css::{Css, CssDeclaration}, + css::{Css, CssRule, CssDeclaration}, dom::NodeData, }; @@ -73,6 +73,13 @@ pub(crate) struct CssConstraintList { pub(crate) list: Vec } +impl CssConstraintList { + #[inline] + pub(crate) fn push_rule(&mut self, rule: &CssRule) { + self.list.push(rule.declaration.1.clone()); + } +} + // Empty test, for some reason codecov doesn't detect any files (and therefore // doesn't report codecov % correctly) except if they have at least one test in // the file. This is an empty test, which should be updated later on From be797b6f4dae547ad19042fee77c457a7a298c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 12:58:37 +0200 Subject: [PATCH 236/868] Improved rendering for radial-gradient --- examples/test.rs | 15 +++++++++++---- src/display_list.rs | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/examples/test.rs b/examples/test.rs index be89f8f95..e6c8067c4 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -10,8 +10,9 @@ impl Layout for MyDataModel { fn layout(&self, _info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") .with_child(Dom::new(NodeType::Label(format!("{}", self.counter))).with_id("red")) - .with_child(Dom::new(NodeType::Div).with_id("green") - .with_child(Dom::new(NodeType::Div).with_id("yellow")) + .with_child(Dom::new(NodeType::Div).with_id("sub-wrapper") + .with_child(Dom::new(NodeType::Div).with_id("yellow") + .with_child(Dom::new(NodeType::Div).with_id("below-yellow"))) .with_child(Dom::new(NodeType::Div).with_id("grey")) ) } @@ -31,13 +32,19 @@ fn main() { font-family: sans-serif; width: 50px; } - #green { - flex-direction: column; + #sub-wrapper { + flex-direction: column-reverse; width: 500px; } #yellow { background-color: yellow; height: 200px; + flex-direction: row-reverse; + } + #below-yellow { + background-color: red; + width: 50px; + height: 50px; } #grey { background-color: grey; diff --git a/src/display_list.rs b/src/display_list.rs index f95f98159..887eec498 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -773,13 +773,24 @@ fn push_background( { match background { Background::RadialGradient(gradient) => { + use css_parser::Shape; + let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), color: gradient_pre.color, }).collect(); - let center = bounds.bottom_left(); // TODO - expose in CSS - let radius = TypedSize2D::new(40.0, 40.0); // TODO - expose in CSS + + let center = bounds.center(); + + // Note: division by 2.0 because it's the radius, not the diameter + let radius = match gradient.shape { + Shape::Ellipse => TypedSize2D::new(bounds.size.width / 2.0, bounds.size.height / 2.0), + Shape::Circle => { + let largest_bound_size = bounds.size.width.max(bounds.size.height); + TypedSize2D::new(largest_bound_size / 2.0, largest_bound_size / 2.0) + }, + }; let gradient = builder.create_radial_gradient(center, radius, stops, gradient.extend_mode); builder.push_radial_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, From b01bb765856b1febe2a20cf11bb345e298bd1c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sun, 26 Aug 2018 15:14:24 +0200 Subject: [PATCH 237/868] Added CSS hot reload + "fixed" memory leak in constraint solver For now, all constraints are simply removed after the frame, so that we don't create excess constraints in the solver Hot reloading CSS files is now possible, but only in debug mode (to prevent accidentally releasing an app without embedding the CSS). Because of this, the CSS simply gets reloaded every 500ms, edited or not. box-shadow has a slight bug regarding layout, as well as linear-gradient, but the important part is that it all mostly works now. What's missing from the proper layout is absolute / relative layout and multi-window handling, but then the first version of this library should be done. --- examples/test.css | 35 ++++++++++++++++++++++++ examples/test.rs | 33 +---------------------- src/app.rs | 16 +++++++++++ src/css.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ src/css_parser.rs | 4 +-- src/display_list.rs | 43 ++++++++++++++--------------- src/ui_solver.rs | 19 +++++++++++++ 7 files changed, 161 insertions(+), 55 deletions(-) create mode 100644 examples/test.css diff --git a/examples/test.css b/examples/test.css new file mode 100644 index 000000000..a2d60ff29 --- /dev/null +++ b/examples/test.css @@ -0,0 +1,35 @@ +#wrapper { + background: linear-gradient(blue 0%, green 25%); + flex-direction: row-reverse; +} + +#red { + background-color: red; + color: white; + font-size: 30px; + font-family: sans-serif; + width: 200px; + flex-direction: column; +} + +#sub-wrapper { + flex-direction: column-reverse; + width: 900px; +} + +#yellow { + background-color: yellow; + height: 700px; + border: 10px dashed black; + flex-direction: row-reverse; + border-radius: 40px; +} + +#grey { + background-color: grey; +} + +#below-yellow { + background-color: dark-grey; + width: 50px; +} \ No newline at end of file diff --git a/examples/test.rs b/examples/test.rs index e6c8067c4..f9c5cfb72 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -19,38 +19,7 @@ impl Layout for MyDataModel { } fn main() { - - let css = Css::new_from_str(" - #wrapper { - background: linear-gradient(135deg, #004e92 0%,#000428 100%); - flex-direction: row; - } - #red { - background-color: red; - color: white; - font-size: 10px; - font-family: sans-serif; - width: 50px; - } - #sub-wrapper { - flex-direction: column-reverse; - width: 500px; - } - #yellow { - background-color: yellow; - height: 200px; - flex-direction: row-reverse; - } - #below-yellow { - background-color: red; - width: 50px; - height: 50px; - } - #grey { - background-color: grey; - } - ").unwrap(); - + let css = Css::hot_reload("C:/please/use/an/absolute/pathname/../test.css").unwrap(); let mut app = App::new(MyDataModel { counter: 0 }, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); diff --git a/src/app.rs b/src/app.rs index 1c0dd1eb3..f61e1986d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -197,6 +197,9 @@ impl App { let mut force_redraw_cache = vec![1_usize; self.windows.len()]; let mut awakened_task = vec![false; self.windows.len()]; + #[cfg(debug_assertions)] + let mut last_css_reload = Instant::now(); + while !self.windows.is_empty() { let time_start = Instant::now(); @@ -277,6 +280,19 @@ impl App { } } + #[cfg(debug_assertions)] { + for (window_idx, window) in self.windows.iter_mut().enumerate() { + // Hot-reload CSS if necessary + if window.css.hot_reload_path.is_some() && Instant::now() - last_css_reload > Duration::from_millis(500) { + window.css.reload_css(); + window.css.needs_relayout = true; + last_css_reload = Instant::now(); + window.events_loop.create_proxy().wakeup().unwrap_or(()); + awakened_task[window_idx] = true; + } + } + } + // Close windows if necessary closed_windows.into_iter().for_each(|closed_window_id| { ui_state_cache.remove(closed_window_id); diff --git a/src/css.rs b/src/css.rs index 872faaffd..25ac397b5 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,4 +1,7 @@ //! CSS parsing and styling + +#[cfg(debug_assertions)] +use std::io::Error as IoError; use { FastHashMap, traits::IntoParsedCssProperty, @@ -24,6 +27,10 @@ const RELAYOUT_RULES: [&str; 13] = [ /// created once. Animations / conditional styling is implemented using dynamic fields #[derive(Debug, Clone, PartialEq)] pub struct Css { + /// Path to hot-reload the CSS file from + #[cfg(debug_assertions)] + pub(crate) hot_reload_path: Option, + /// The CSS rules making up the document pub(crate) rules: Vec, /// The dynamic properties that have to be overridden for this frame /// @@ -164,17 +171,74 @@ impl CssRule { } } +#[cfg(debug_assertions)] +#[derive(Debug)] +pub enum HotReloadError { + Io(IoError, String), + // TODO: get the CSS + FailedToReload, +} + impl Css { /// Creates an empty set of CSS rules pub fn empty() -> Self { Self { + #[cfg(debug_assertions)] + hot_reload_path: None, rules: Vec::new(), needs_relayout: false, dynamic_css_overrides: FastHashMap::default(), } } + /// **NOTE**: Only available in debug mode, can crash if the file isn't found + #[cfg(debug_assertions)] + pub fn hot_reload(file_path: &str) -> Result { + use std::fs; + let initial_css = fs::read_to_string(&file_path).map_err(|e| HotReloadError::Io(e, file_path.to_string()))?; + let mut css = match Self::new_from_str(&initial_css) { + Ok(o) => o, + Err(e) => panic!("Hot reload parsing error in file {}: {:?}", file_path, e), + }; + css.hot_reload_path = Some(file_path.into()); + Ok(css) + } + + #[cfg(debug_assertions)] + pub fn reload_css(&mut self) { + + use std::fs; + + let file_path = if let Some(f) = &self.hot_reload_path { + f.clone() + } else { + error!("No file to hot-reload the CSS from!"); + return; + }; + + let reloaded_css = match fs::read_to_string(&file_path) { + Ok(o) => o, + Err(e) => { + error!("Failed to hot-reload \"{}\":\r\n{:?}", file_path, e); + return; + }, + }; + + let mut parsed_css = match Self::new_from_str(&reloaded_css) { + Ok(o) => o, + Err(e) => { + error!("Failed to reload - parse error\"{}\":\r\n{:?}", file_path, e); + return; + }, + }; + + parsed_css.hot_reload_path = self.hot_reload_path.clone(); + parsed_css.dynamic_css_overrides = self.dynamic_css_overrides.clone(); + + *self = parsed_css; + } + /// Parses a CSS string (single-threaded) and returns the parsed rules pub fn new_from_str<'a>(css_string: &'a str) -> Result> { use simplecss::{Tokenizer, Token}; @@ -266,6 +330,8 @@ impl Css { } Ok(Self { + #[cfg(debug_assertions)] + hot_reload_path: None, rules: css_rules, // force re-layout for the first frame needs_relayout: true, diff --git a/src/css_parser.rs b/src/css_parser.rs index f9d95f69c..eed54a789 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1632,7 +1632,7 @@ pub enum TextAlignmentHorz { impl Default for TextAlignmentHorz { fn default() -> Self { - TextAlignmentHorz::Left + TextAlignmentHorz::Center } } @@ -1645,7 +1645,7 @@ pub enum TextAlignmentVert { impl Default for TextAlignmentVert { fn default() -> Self { - TextAlignmentVert::Top + TextAlignmentVert::Center } } diff --git a/src/display_list.rs b/src/display_list.rs index 887eec498..d78ed0533 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -246,6 +246,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { }; if css.needs_relayout || changeset.is_some() { + // inefficient for now, but prevents memory leak + ui_solver.clear_all_constraints(); for rect_idx in self.rectangles.linear_iter() { let constraints = create_layout_constraints( rect_idx, @@ -254,6 +256,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { &ui_solver, ); ui_solver.insert_css_constraints_for_rect(&constraints); + ui_solver.push_added_constraints(rect_idx, constraints); } // If we push or pop constraints that means we also need to re-layout the window @@ -449,31 +452,29 @@ fn determine_text_alignment<'a>(rect_idx: NodeId, arena: &Arena { - horz_alignment = match justify_content { - Start => TextAlignmentHorz::Left, - End => TextAlignmentHorz::Right, - Center | SpaceBetween | SpaceAround => TextAlignmentHorz::Center, - }; - }, - Column | ColumnReverse => { - vert_alignment = match justify_content { - Start => TextAlignmentVert::Top, - End => TextAlignmentVert::Bottom, - Center | SpaceBetween | SpaceAround => TextAlignmentVert::Center, - }; - }, + if let Some(align_items) = rect.data.layout.align_items { + // Vertical text alignment + use css_parser::LayoutAlignItems; + match align_items { + LayoutAlignItems::Start => vert_alignment = TextAlignmentVert::Top, + LayoutAlignItems::End => vert_alignment = TextAlignmentVert::Bottom, + // technically stretch = blocktext, but we don't have that yet + _ => vert_alignment = TextAlignmentVert::Center, + } + } + + if let Some(justify_content) = rect.data.layout.justify_content { + use css_parser::LayoutJustifyContent; + // Horizontal text alignment + match justify_content { + LayoutJustifyContent::Start => horz_alignment = TextAlignmentHorz::Left, + LayoutJustifyContent::End => horz_alignment = TextAlignmentHorz::Right, + _ => horz_alignment = TextAlignmentHorz::Center, } } if let Some(text_align) = rect.data.style.text_align { + // Horizontal text alignment with higher priority horz_alignment = text_align; } diff --git a/src/ui_solver.rs b/src/ui_solver.rs index f19f122a4..52681da1a 100644 --- a/src/ui_solver.rs +++ b/src/ui_solver.rs @@ -62,6 +62,11 @@ impl WindowSizeConstraints { pub struct UiSolver { /// The actual cassowary solver solver: Solver, + /// In order to remove constraints, we need to store them somewhere + /// and then remove them from the cassowary solver when they aren't necessary + /// anymore. This is a pretty hard problem, which is why we need `DomChangeSet` + /// to get a list of removed NodeIds + added_constraints: BTreeMap>, /// The size constraints of the root window window_constraints: WindowSizeConstraints, /// The list of variables that has been added to the solver @@ -86,6 +91,7 @@ impl UiSolver { Self { solver: solver, + added_constraints: BTreeMap::new(), solved_values: BTreeMap::new(), window_constraints: window_constraints, edit_variable_cache: EditVariableCache::empty(), @@ -133,6 +139,19 @@ impl UiSolver { self.edit_variable_cache.map.get(&dom_hash.data).and_then(|rect| Some(rect.1)) } + pub(crate) fn push_added_constraints(&mut self, rect_id: NodeId, constraints: Vec) { + self.added_constraints.entry(rect_id).or_insert_with(|| Vec::new()).extend(constraints); + } + + pub(crate) fn clear_all_constraints(&mut self) { + for entry in self.added_constraints.values() { + for constraint in entry { + self.solver.remove_constraint(constraint).unwrap(); + } + } + self.added_constraints = BTreeMap::new(); + } + pub(crate) fn get_window_constraints(&self) -> WindowSizeConstraints { self.window_constraints } From 88ad100c6b0b37c3219ae4138ebf7ad20f15a0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 27 Aug 2018 10:09:42 +0200 Subject: [PATCH 238/868] Fixed rendering order + correct color for 'transparent' --- examples/test.css | 20 ++++---- examples/test.rs | 2 +- src/css_parser.rs | 2 +- src/display_list.rs | 116 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 104 insertions(+), 36 deletions(-) diff --git a/examples/test.css b/examples/test.css index a2d60ff29..1897ac604 100644 --- a/examples/test.css +++ b/examples/test.css @@ -1,35 +1,37 @@ #wrapper { - background: linear-gradient(blue 0%, green 25%); - flex-direction: row-reverse; + background-color: salmon; + background: linear-gradient(135deg, blue 0%, salmon 100%); + flex-direction: row; } #red { background-color: red; color: white; - font-size: 30px; + font-size: 12px; font-family: sans-serif; width: 200px; - flex-direction: column; } #sub-wrapper { flex-direction: column-reverse; - width: 900px; + width: 400px; + box-shadow: 0px 0px 50px black; } #yellow { background-color: yellow; - height: 700px; - border: 10px dashed black; + height: 70px; + border: 2px dashed black; flex-direction: row-reverse; - border-radius: 40px; + box-shadow: 0px 0px 50px black; } #grey { - background-color: grey; + background-color: transparent; } #below-yellow { background-color: dark-grey; + box-shadow: 0px 0px 50px black; width: 50px; } \ No newline at end of file diff --git a/examples/test.rs b/examples/test.rs index f9c5cfb72..7503c5fec 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -19,7 +19,7 @@ impl Layout for MyDataModel { } fn main() { - let css = Css::hot_reload("C:/please/use/an/absolute/pathname/../test.css").unwrap(); + let css = Css::hot_reload("/home/felix/Development/azul/examples/test.css").unwrap(); let mut app = App::new(MyDataModel { counter: 0 }, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); diff --git a/src/css_parser.rs b/src/css_parser.rs index eed54a789..3669b1211 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -711,7 +711,7 @@ fn parse_color_builtin<'a>(input: &'a str) "WhiteSmoke" | "white-smoke" => "F5F5F5", "Yellow" | "yellow" => "FFFF00", "YellowGreen" | "yellow-green" => "9ACD32", - "Transparent" | "transparent" => "FFFFFFFF", + "Transparent" | "transparent" => "FFFFFF00", _ => { return Err(CssColorParseError::InvalidColor(input)); } }; parse_color_no_hash(color) diff --git a/src/display_list.rs b/src/display_list.rs index d78ed0533..1098ba50d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -234,7 +234,8 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { -> DisplayListBuilder { use glium::glutin::dpi::LogicalSize; - + use std::collections::BTreeMap; + let changeset = self.ui_descr.ui_descr_root.as_ref().and_then(|root| { let changeset = ui_solver.update_dom(root, &*(self.ui_descr.ui_descr_arena.borrow())); if changeset.is_empty() { None } else { Some(changeset) } @@ -283,18 +284,42 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { let arena = self.ui_descr.ui_descr_arena.borrow(); - for rect_idx in root.descendants(&self.rectangles) { - displaylist_handle_rect( - &mut builder, - current_epoch, - rect_idx, - &self.rectangles, - &arena[rect_idx].data.node_type, - ui_solver.query_bounds_of_rect(rect_idx), - full_screen_rect, - app_resources, - render_api, - &mut resource_updates); + // Determine the correct implicit z-index rendering order of every rectangle + let mut rects_in_rendering_order = BTreeMap::>::new(); + + for rect_id in self.rectangles.linear_iter() { + + // how many z-levels does this rectangle have until we get to the root? + let z_index = { + let mut index = 0; + let mut cur_rect_idx = rect_id; + while let Some(parent) = self.rectangles[cur_rect_idx].parent() { + index += 1; + cur_rect_idx = parent; + } + index + }; + + rects_in_rendering_order + .entry(z_index) + .or_insert_with(|| Vec::new()) + .push(rect_id); + } + + for (z_index, rects) in rects_in_rendering_order.into_iter() { + for rect_idx in rects { + displaylist_handle_rect( + &mut builder, + current_epoch, + rect_idx, + &self.rectangles, + &arena[rect_idx].data.node_type, + ui_solver.query_bounds_of_rect(rect_idx), + full_screen_rect, + app_resources, + render_api, + &mut resource_updates); + } } render_api.update_resources(resource_updates); @@ -487,7 +512,7 @@ fn push_rect( builder: &mut DisplayListBuilder, color: &BackgroundColor) { - builder.push_rect(&info, color.0.into()); + builder.push_rect(&info, srgba_to_linear(color.0.into())); } #[inline] @@ -549,7 +574,7 @@ fn push_text( &scrollbar_style ); - let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); + let font_color = srgba_to_linear(style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into()); let mut flags = FontInstanceFlags::empty(); flags.set(FontInstanceFlags::SUBPIXEL_BGR, true); flags.set(FontInstanceFlags::FONT_SMOOTHING, true); @@ -732,7 +757,6 @@ fn push_box_shadow( // The pre_shadow is missing the BorderRadius & LayoutRect let border_radius = style.border_radius.unwrap_or(BorderRadius::zero()); - if pre_shadow.clip_mode != shadow_type { return; } @@ -746,20 +770,19 @@ fn push_box_shadow( // calculate the maximum extent of the outset shadow let mut clip_rect = *bounds; - let origin_displace = pre_shadow.spread_radius - pre_shadow.blur_radius; - clip_rect.origin.x = clip_rect.origin.x + pre_shadow.offset.x - origin_displace; - clip_rect.origin.y = clip_rect.origin.y + pre_shadow.offset.y - origin_displace; + let origin_displace = (pre_shadow.spread_radius + pre_shadow.blur_radius) * 2.0; + clip_rect.origin.x = clip_rect.origin.x - pre_shadow.offset.x - origin_displace; + clip_rect.origin.y = clip_rect.origin.y - pre_shadow.offset.y - origin_displace; - let spread = (pre_shadow.spread_radius * 2.0) + (pre_shadow.blur_radius * 2.0); - clip_rect.size.height = clip_rect.size.height + spread; - clip_rect.size.width = clip_rect.size.width + spread; + clip_rect.size.height = clip_rect.size.height + (origin_displace * 2.0); + clip_rect.size.width = clip_rect.size.width + (origin_displace * 2.0); // prevent shadows that are larger than the full screen clip_rect.intersection(full_screen_rect).unwrap_or(clip_rect) }; let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), clip_rect); - builder.push_box_shadow(&info, *bounds, pre_shadow.offset, pre_shadow.color, + builder.push_box_shadow(&info, *bounds, pre_shadow.offset, srgba_to_linear(pre_shadow.color), pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); } @@ -779,7 +802,7 @@ fn push_background( let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), - color: gradient_pre.color, + color: srgba_to_linear(gradient_pre.color), }).collect(); let center = bounds.center(); @@ -800,7 +823,7 @@ fn push_background( let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), - color: gradient_pre.color, + color: srgba_to_linear(gradient_pre.color), }).collect(); let (mut begin_pt, mut end_pt) = gradient.direction.to_points(&bounds); @@ -1105,3 +1128,46 @@ fn create_layout_constraints<'a, T: Layout>( fn __codecov_test_display_list_file() { } + + +/// Taken from the `palette` crate - I wouldn't want to +/// import the entire crate just for one function (due to added compile time) +/// +/// The MIT License (MIT) +/// +/// Copyright (c) 2015 Erik Hedvall +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy +/// of this software and associated documentation files (the "Software"), to deal +/// in the Software without restriction, including without limitation the rights +/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +/// copies of the Software, and to permit persons to whom the Software is +/// furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all +/// copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +/// SOFTWARE. +fn srgba_to_linear(color: ColorF) -> ColorF { + + fn into_linear(x: f32) -> f32 { + if x <= 0.04045 { + x / 12.92 + } else { + ((x + 0.055) / 1.055).powf(2.4) + } + } + + ColorF { + r: into_linear(color.r), + g: into_linear(color.g), + b: into_linear(color.b), + a: color.a, + } +} \ No newline at end of file From 9a097b4db9143d9cad4f1ebabb8680498600608d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 27 Aug 2018 10:25:30 +0200 Subject: [PATCH 239/868] Updated README --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index f4f811139..2f4780cfd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,4 @@ -# azul [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) -[![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) -[![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) -[![Rust Compiler Version](https://img.shields.io/badge/rustc-1.28%20stable-blue.svg)]() +# azul [![Build Status Linux / macOS](https://travis-ci.org/maps4print/azul.svg?branch=master)](https://travis-ci.org/maps4print/azul) [![Build status Windows](https://ci.appveyor.com/api/projects/status/p487hewqh6bxeucv?svg=true)](https://ci.appveyor.com/project/fschutt/azul) [![codecov](https://codecov.io/gh/maps4print/azul/branch/master/graph/badge.svg)](https://codecov.io/gh/maps4print/azul) [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Rust Compiler Version](https://img.shields.io/badge/rustc-1.28%20stable-blue.svg)]() # WARNING: The features advertised don't work yet. # See the /examples folder for an example of what's currently possible. From 8eddcd62449fb3d43ec03354baad906ff7b09b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Mon, 27 Aug 2018 15:52:36 +0200 Subject: [PATCH 240/868] Partially fixed calculations for linear-gradient points --- Cargo.toml | 2 +- examples/test.css | 12 +++---- examples/test.rs | 16 +++++---- src/css_parser.rs | 67 ++++++++++++++++++++++++++++++---- src/display_list.rs | 87 ++++++++++++++------------------------------- 5 files changed, 104 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b834964e..0d4ae10ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ optional = true [dependencies.webrender] git = "https://github.com/servo/webrender" -rev = "c939a61b83bcc9dc10742977704793e9a85b3858" +rev = "816ff14c1805c145ccd60d0227d82b1541fc24eb" [features] default = ["logging", "svg"] diff --git a/examples/test.css b/examples/test.css index 1897ac604..98b1a54aa 100644 --- a/examples/test.css +++ b/examples/test.css @@ -1,11 +1,10 @@ #wrapper { - background-color: salmon; - background: linear-gradient(135deg, blue 0%, salmon 100%); + background: linear-gradient(300deg, red 0%, blue 100%); flex-direction: row; } #red { - background-color: red; + background-color: #BF0C2B; color: white; font-size: 12px; font-family: sans-serif; @@ -19,9 +18,8 @@ } #yellow { - background-color: yellow; + background-color: #F5900E; height: 70px; - border: 2px dashed black; flex-direction: row-reverse; box-shadow: 0px 0px 50px black; } @@ -31,7 +29,7 @@ } #below-yellow { - background-color: dark-grey; - box-shadow: 0px 0px 50px black; + background-color: #F14C13; + box-shadow: 2px 2px 10px black; width: 50px; } \ No newline at end of file diff --git a/examples/test.rs b/examples/test.rs index 7503c5fec..ba5a29c00 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -2,14 +2,12 @@ extern crate azul; use azul::prelude::*; -struct MyDataModel { - counter: usize, -} +struct MyDataModel; impl Layout for MyDataModel { fn layout(&self, _info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") - .with_child(Dom::new(NodeType::Label(format!("{}", self.counter))).with_id("red")) + .with_child(Dom::new(NodeType::Label(format!("Hello World"))).with_id("red")) .with_child(Dom::new(NodeType::Div).with_id("sub-wrapper") .with_child(Dom::new(NodeType::Div).with_id("yellow") .with_child(Dom::new(NodeType::Div).with_id("below-yellow"))) @@ -19,8 +17,14 @@ impl Layout for MyDataModel { } fn main() { - let css = Css::hot_reload("/home/felix/Development/azul/examples/test.css").unwrap(); - let mut app = App::new(MyDataModel { counter: 0 }, AppConfig::default()); + const CSS_PATH: &str = "/please/use/an/absolute/file/path/../test.css"; + + #[cfg(debug_assertions)] + let css = Css::hot_reload(CSS_PATH).unwrap(); + #[cfg(not(debug_assertions))] + let css = Css::new_from_str(include_str!(CSS_PATH)).unwrap(); + + let mut app = App::new(MyDataModel, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/css_parser.rs b/src/css_parser.rs index 3669b1211..6c21bc003 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1041,14 +1041,69 @@ impl Direction { pub fn to_points(&self, rect: &LayoutRect) -> (LayoutPoint, LayoutPoint) { - match *self { - Direction::Angle(ref deg) => { - let max = rect.size.width.max(rect.size.height); - let mut point: LayoutPoint = TypedPoint2D::new(max, 0.0); + match self { + Direction::Angle(deg) => { + // note: assumes that the LayoutRect has positive sides + + // see: https://hugogiraudel.com/2013/02/04/css-gradients/ + + let width_half = rect.size.width / 2.0; + let height_half = rect.size.height / 2.0; + + // hypothenuse_len is the length of the center of the rect to the corners + let hypothenuse_len = (width_half.powi(2) + height_half.powi(2)).sqrt(); + + // clamp the degree to 360 (so 410deg = 50deg) + let mut deg = deg % 360.0; + if deg < 0.0 { + deg = 360.0 + deg; + } + + // now deg is in the range of +0..+360 + debug_assert!(deg >= 0.0 && deg <= 360.0); + + // The corner also serves to determine what quadrant we're in + // Get the quadrant (corner) the angle is in and get the degree associated + // with that corner. + + let angle_to_top_left = (height_half / width_half).atan().to_degrees(); + + // We need to calculate the angle from the center to the corner! + let ending_point_degrees = if deg <= 90.0 { + // top left corner + 90.0 - angle_to_top_left + } else if deg <= 180.0 { + // bottom left corner + 90.0 + angle_to_top_left + } else if deg <= 270.0 { + // bottom right corner + 270.0 - angle_to_top_left + } else /* deg > 270.0 && deg < 360.0 */ { + // top right corner + 270.0 + angle_to_top_left + }; + + // assuming deg = 36deg, then degree_diff_to_corner = 9deg + let degree_diff_to_corner = ending_point_degrees - deg; + + // Searched_len is the distance between the center of the rect and the + // ending point of the gradient + let searched_len = (hypothenuse_len * degree_diff_to_corner.cos()).abs(); + + // TODO: This searched_len is incorrect... + + // Once we have the length, we can simply rotate the length by the angle, + // then translate it to the center of the rect + let point_location = LayoutPoint::new(0.0, searched_len); let rot = TypedRotation2D::new(Angle::radians(deg.to_radians())); - (LayoutPoint::zero(), rot.transform_point(&point)) + let point_location: LayoutPoint = rot.transform_point(&point_location); + + let start_point_location = LayoutPoint::new(width_half + point_location.x, height_half + point_location.y); + let end_point_location = LayoutPoint::new(width_half - point_location.x, height_half - point_location.y); + + (start_point_location, end_point_location) }, - Direction::FromTo(ref from, ref to) => { + Direction::FromTo(from, to) => { (from.to_point(rect), to.to_point(rect)) } } diff --git a/src/display_list.rs b/src/display_list.rs index 1098ba50d..c9070c08a 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -459,7 +459,8 @@ fn displaylist_handle_rect<'a>( LayoutSize::zero(), ImageRendering::Auto, AlphaType::Alpha, - key); + key, + ColorF::WHITE); }, } @@ -512,7 +513,7 @@ fn push_rect( builder: &mut DisplayListBuilder, color: &BackgroundColor) { - builder.push_rect(&info, srgba_to_linear(color.0.into())); + builder.push_rect(&info, color.0.into()); } #[inline] @@ -574,7 +575,7 @@ fn push_text( &scrollbar_style ); - let font_color = srgba_to_linear(style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into()); + let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); let mut flags = FontInstanceFlags::empty(); flags.set(FontInstanceFlags::SUBPIXEL_BGR, true); flags.set(FontInstanceFlags::FONT_SMOOTHING, true); @@ -781,8 +782,24 @@ fn push_box_shadow( clip_rect.intersection(full_screen_rect).unwrap_or(clip_rect) }; + // Apply a gamma of 2.2 to the original value + // + // NOTE: strangely box-shadow is the only thing that needs to be gamma-corrected... + fn apply_gamma(color: ColorF) -> ColorF { + + const GAMMA: f32 = 2.2; + const GAMMA_F: f32 = 1.0 / GAMMA; + + ColorF { + r: color.r.powf(GAMMA_F), + g: color.g.powf(GAMMA_F), + b: color.b.powf(GAMMA_F), + a: color.a, + } + } + let info = LayoutPrimitiveInfo::with_clip_rect(LayoutRect::zero(), clip_rect); - builder.push_box_shadow(&info, *bounds, pre_shadow.offset, srgba_to_linear(pre_shadow.color), + builder.push_box_shadow(&info, *bounds, pre_shadow.offset, apply_gamma(pre_shadow.color), pre_shadow.blur_radius, pre_shadow.spread_radius, border_radius, pre_shadow.clip_mode); } @@ -802,7 +819,7 @@ fn push_background( let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { offset: gradient_pre.offset.unwrap(), - color: srgba_to_linear(gradient_pre.color), + color: gradient_pre.color, }).collect(); let center = bounds.center(); @@ -822,19 +839,11 @@ fn push_background( let mut stops: Vec = gradient.stops.iter().map(|gradient_pre| GradientStop { - offset: gradient_pre.offset.unwrap(), - color: srgba_to_linear(gradient_pre.color), + offset: gradient_pre.offset.unwrap() / 100.0, + color: gradient_pre.color, }).collect(); let (mut begin_pt, mut end_pt) = gradient.direction.to_points(&bounds); - - end_pt.x = -end_pt.x; - - // webrender "normalizes" gradient stops, TODO: file a bug about this? - - begin_pt.x /= 100.0; begin_pt.y /= 100.0; - end_pt.x /= 100.0; end_pt.y /= 100.0; - let gradient = builder.create_gradient(begin_pt, end_pt, stops, gradient.extend_mode); builder.push_gradient(&info, gradient, bounds.size, LayoutSize::zero()); }, @@ -864,8 +873,9 @@ fn push_image( bounds.size, LayoutSize::zero(), ImageRendering::Auto, - AlphaType::Alpha, - image_info.key); + AlphaType::PremultipliedAlpha, + image_info.key, + ColorF::WHITE); }, _ => { }, } @@ -1127,47 +1137,4 @@ fn create_layout_constraints<'a, T: Layout>( #[test] fn __codecov_test_display_list_file() { -} - - -/// Taken from the `palette` crate - I wouldn't want to -/// import the entire crate just for one function (due to added compile time) -/// -/// The MIT License (MIT) -/// -/// Copyright (c) 2015 Erik Hedvall -/// -/// Permission is hereby granted, free of charge, to any person obtaining a copy -/// of this software and associated documentation files (the "Software"), to deal -/// in the Software without restriction, including without limitation the rights -/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -/// copies of the Software, and to permit persons to whom the Software is -/// furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in all -/// copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -/// SOFTWARE. -fn srgba_to_linear(color: ColorF) -> ColorF { - - fn into_linear(x: f32) -> f32 { - if x <= 0.04045 { - x / 12.92 - } else { - ((x + 0.055) / 1.055).powf(2.4) - } - } - - ColorF { - r: into_linear(color.r), - g: into_linear(color.g), - b: into_linear(color.b), - a: color.a, - } } \ No newline at end of file From fde7267e7d8770683e3dc45528735f46b05326a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 28 Aug 2018 05:14:04 +0200 Subject: [PATCH 241/868] Fixed hot_reload example not compiling in release mode --- examples/{test.css => hot_reload.css} | 0 examples/{test.rs => hot_reload.rs} | 8 +++++--- 2 files changed, 5 insertions(+), 3 deletions(-) rename examples/{test.css => hot_reload.css} (100%) rename examples/{test.rs => hot_reload.rs} (74%) diff --git a/examples/test.css b/examples/hot_reload.css similarity index 100% rename from examples/test.css rename to examples/hot_reload.css diff --git a/examples/test.rs b/examples/hot_reload.rs similarity index 74% rename from examples/test.rs rename to examples/hot_reload.rs index ba5a29c00..6b65ea477 100644 --- a/examples/test.rs +++ b/examples/hot_reload.rs @@ -17,12 +17,14 @@ impl Layout for MyDataModel { } fn main() { - const CSS_PATH: &str = "/please/use/an/absolute/file/path/../test.css"; + + // workaround for: https://github.com/rust-lang/rust/issues/53749 + macro_rules! css_path { () => ("/please/use/an/absolute/file/path/../hot_reload.css") } #[cfg(debug_assertions)] - let css = Css::hot_reload(CSS_PATH).unwrap(); + let css = Css::hot_reload(css_path!()).unwrap(); #[cfg(not(debug_assertions))] - let css = Css::new_from_str(include_str!(CSS_PATH)).unwrap(); + let css = Css::new_from_str(include_str!(css_path!())).unwrap(); let mut app = App::new(MyDataModel, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); From f8980182c15a06ede4dde47cb342b3108a21b2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 28 Aug 2018 09:58:58 +0200 Subject: [PATCH 242/868] Fixed pt parsing bug + added test case --- src/css_parser.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index 6c21bc003..b6fe542e0 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -482,7 +482,7 @@ fn parse_pixel_value<'a>(input: &'a str) let unit = match unit { "px" => CssMetric::Px, "em" => CssMetric::Em, - "ept" => CssMetric::Pt, + "pt" => CssMetric::Pt, _ => { return Err(PixelParseError::InvalidComponent(&input[(split_pos - 1)..])); } }; @@ -1063,7 +1063,7 @@ impl Direction { debug_assert!(deg >= 0.0 && deg <= 360.0); // The corner also serves to determine what quadrant we're in - // Get the quadrant (corner) the angle is in and get the degree associated + // Get the quadrant (corner) the angle is in and get the degree associated // with that corner. let angle_to_top_left = (height_half / width_half).atan().to_degrees(); @@ -1086,7 +1086,7 @@ impl Direction { // assuming deg = 36deg, then degree_diff_to_corner = 9deg let degree_diff_to_corner = ending_point_degrees - deg; - // Searched_len is the distance between the center of the rect and the + // Searched_len is the distance between the center of the rect and the // ending point of the gradient let searched_len = (hypothenuse_len * degree_diff_to_corner.cos()).abs(); @@ -2297,6 +2297,11 @@ mod css_tests { #[test] fn test_parse_pixel_value_3() { + assert_eq!(parse_pixel_value("11pt"), Ok(PixelValue { metric: CssMetric::Pt, number: 11000 })); + } + + #[test] + fn test_parse_pixel_value_4() { assert_eq!(parse_pixel_value("aslkfdjasdflk"), Err(PixelParseError::InvalidComponent("aslkfdjasdflk"))); } From 8248a080640d73acba10b1ec2ce79ae73fe70480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Tue, 28 Aug 2018 11:17:20 +0200 Subject: [PATCH 243/868] "Fixed" the OpenGL crashing issue by simply not drawing invalid textures This isn't a clean solution, but on the other hand, we won't have any crashing issues with panics anymore - if a texture ID isn't found we simply return ExternalImageSource::Invalid The issue seems to be that cleanup_unused_opengl_textures is called while the window is minimized or at least before it is restored. However, winit doesn't have a clean API to check if the window was minimized, so adding this would overcomplicate the main loop. Not drawing anything is way simpler. --- src/compositor.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 1f8c8280a..029fbbfb9 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -66,19 +66,36 @@ impl ExternalImageHandler for Compositor { let gl_tex_lock = ACTIVE_GL_TEXTURES.lock().unwrap(); // Search all epoch hash maps for the given key - // There does not seemt to be a way to get the epoch for the key, so we simply have to search all active epochs - let tex = gl_tex_lock + // There does not seemt to be a way to get the epoch for the key, + // so we simply have to search all active epochs + // + // NOTE: Invalid textures can be generated on minimize / maximize + // Luckily, webrender simply ignores an invalid texture, so we don't + // need to check whether a window is maximized or minimized - if + // we encounter an invalid ID, webrender simply won't draw anything, + // but at least it won't crash. Usually invalid textures are also 0x0 + // pixels large - so it's not like we had anything to draw anyway. + let (tex, wh) = gl_tex_lock .values() .filter_map(|epoch_map| epoch_map.get(&key)) .next() - .expect("Missing OpenGL texture"); + .and_then(|tex| { + Some(( + ExternalImageSource::NativeTexture(tex.texture.inner.get_id()), + TypedPoint2D::::new( + tex.texture.inner.width() as f32, + tex.texture.inner.height() as f32 + ) + )) + }) + .unwrap_or((ExternalImageSource::Invalid, TypedPoint2D::zero())); ExternalImage { uv: TexelRect { uv0: TypedPoint2D::zero(), - uv1: TypedPoint2D::::new(tex.texture.inner.width() as f32, tex.texture.inner.height() as f32), + uv1: wh, }, - source: ExternalImageSource::NativeTexture(tex.texture.inner.get_id()), + source: tex, } } From 6141d6e9bd85abb1933fc99cf06c2c7387c933eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 06:21:52 +0200 Subject: [PATCH 244/868] Fixed double-return text layout bug --- src/text_layout.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/text_layout.rs b/src/text_layout.rs index fc17f58b3..122231e0d 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -863,13 +863,38 @@ fn align_text_horz(alignment: TextAlignmentHorz, glyphs: &mut [GlyphInstance], l Right => 1.0, // move the line by the full width }; - let mut current_line_num = 0; - for (glyph_idx, glyph) in glyphs.iter_mut().enumerate() { - if glyph_idx > line_breaks[current_line_num].0 { - current_line_num += 1; + // If we have the characters "ABC\n\nDEF", this will result in: + // + // [ Glyph(A), Glyph(B), Glyph(C), Glyph(D), Glyph(E), Glyph(F)] + // + // [LineBreak(2), LineBreak(2), LineBreak(5)] + // + // If we'd just shift every character after the line break, we'd get into + // the problem of shifting the 3rd character twice, because of the \n\n. + // + // To avoid the double-line-break problem, we can use ranges: + // + // - from 0..2, shift the characters at i by X amount + // - from 2..2 (e.g. 0 characters) shift the characters at i by X amount + // - from 2..5 shift the characters by X amount + // + // Because the middle range selects 0 characters, the shift is effectively + // ignored, which is what we want - because there are no characters to shift. + + let mut start_range_char = 0; + + // last line break is special, here we have to use an upper-bound-inclusive range, i.e. 2..=5 + for (line_break_char, line_break_amount) in line_breaks.iter().take(line_breaks.len() - 1) { + for glyph in &mut glyphs[start_range_char..*line_break_char] { + glyph.point.x += line_break_amount * multiply_factor; } - let space_added_full = line_breaks[current_line_num].1; - glyph.point.x += space_added_full * multiply_factor; + start_range_char = *line_break_char; + } + + // last line: use an inclusive range: 2..=5 + let (last_line_break_char, last_line_break_amount) = line_breaks[line_breaks.len() - 1]; + for glyph in &mut glyphs[start_range_char..=last_line_break_char] { + glyph.point.x += last_line_break_amount * multiply_factor; } } From 0e2bfa8fa83995c831c1b5ecf59327d086cae572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 08:02:36 +0200 Subject: [PATCH 245/868] Preliminary callback-based OpenGL rendering This enables users to know the bounds of the rectangle they are rendering into. For example, if we want the OpenGL texture to be inside of a rectangle, we need to know the width and the height of the texture, so we can draw the thing we want to draw properly. For this, we stop the rendering process, lock the users data, and invoke a user-defined callback (the GlTextureCallback) with the width and the height that the layout solver gave us. This way the user knows the width and height of the component it should render. Right now, the layout doesn't work very stable - the rectangle can jitter around, but at least it draws properly. --- examples/debug.css | 37 +++++++++++++++ examples/debug.rs | 39 ++++++++++++---- src/app.rs | 19 ++++++-- src/display_list.rs | 90 ++++++++++++++++++++++-------------- src/dom.rs | 109 ++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- src/widgets/svg.rs | 18 +++++--- src/window.rs | 1 + 8 files changed, 249 insertions(+), 66 deletions(-) create mode 100644 examples/debug.css diff --git a/examples/debug.css b/examples/debug.css new file mode 100644 index 000000000..f80c175b3 --- /dev/null +++ b/examples/debug.css @@ -0,0 +1,37 @@ +.__azul-native-button { + border: 1px solid #3399ff; /* inactive: #acacac */ + background: linear-gradient(#f0f0f0, #e5e5e5); + width: 85px; + height: 21px; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.__azul-native-label { + text-align: center; + flex-direction: column; + justify-content: center; +} + +#parent-wrapper { + background-color: red; + flex-direction: row; +} + +#child-1 { + background-color: green; + max-width: 200px; +} + +#child-2 { + background-color: yellow; + max-height: 600px; +} + +* { + font-size: 14.66px; + font-family: sans-serif; + color: #000; + background-color: #f0f0f0; /* Windows Background color, rgb(240, 240, 240) */ +} \ No newline at end of file diff --git a/examples/debug.rs b/examples/debug.rs index 5def79890..9c96904dd 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -37,16 +37,13 @@ pub struct Map { } impl Layout for MyAppData { - fn layout(&self, info: WindowInfo) + fn layout(&self, _info: WindowInfo) -> Dom { - if let Some(map) = &self.map { - Svg::with_layers(build_layers(&map.layers, &map.texts, &map.hovered_text, &map.font_cache, &info.resources)) - .with_pan(map.pan_horz as f32, map.pan_vert as f32) - .with_zoom(map.zoom as f32) - .dom(&info.window, &map.cache) - .with_callback(On::Scroll, Callback(scroll_map_contents)) - .with_callback(On::MouseOver, Callback(check_hovered_font)) + if let Some(_map) = &self.map { + Dom::new(NodeType::Div).with_id("parent-wrapper") + .with_child(Dom::new(NodeType::Div).with_id("child-1")) + .with_child(gl_texture_dom().with_id("child-2")) } else { // TODO: If this is changed to Label::new(), the text is cut off at the top // because of the (offset_top / 2.0) - see text_layout.rs file @@ -56,6 +53,20 @@ impl Layout for MyAppData { } } +fn gl_texture_dom() -> Dom { + Dom::new(NodeType::GlTexture(GlTextureCallback(render_map))) + .with_callback(On::Scroll, Callback(scroll_map_contents)) + .with_callback(On::MouseOver, Callback(check_hovered_font)) +} + +fn render_map(data: &MyAppData, info: WindowInfo, width: usize, height: usize) -> Option { + let map = data.map.as_ref()?; + Svg::with_layers(build_layers(&map.layers, &map.texts, &map.hovered_text, &map.font_cache, &info.resources)) + .with_pan(map.pan_horz as f32, map.pan_vert as f32) + .with_zoom(map.zoom as f32) + .render_svg(&map.cache, &info.window, width, height) +} + fn build_layers( existing_layers: &[SvgLayerId], texts: &HashMap, @@ -196,7 +207,15 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv } fn main() { + + macro_rules! css_path { () => ("/please/use/an/absolute/filepath/../debug.css") } + + #[cfg(debug_assertions)] + let css = Css::hot_reload(css_path!()).unwrap(); + #[cfg(not(debug_assertions))] + let css = Css::new_from_str(include_str!(css_path!())).unwrap(); + let mut app = App::new(MyAppData { map: None }, AppConfig::default()); - app.create_window(WindowCreateOptions::default(), Css::native()).unwrap(); + app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); -} \ No newline at end of file +} diff --git a/src/app.rs b/src/app.rs index f61e1986d..1d547ab02 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ use log::LevelFilter; use { images::ImageType, errors::{FontError, ClipboardError}, - window::{Window, WindowCreateOptions, WindowCreateError, WindowId}, + window::{Window, WindowCreateOptions, ReadOnlyWindow, WindowCreateError, WindowId}, css_parser::{FontId, PixelValue}, text_cache::TextId, dom::UpdateScreen, @@ -267,7 +267,7 @@ impl App { let window_id = WindowId { id: idx }; let read_only_window = ReadOnlyWindow { inner: window.display.clone() }; ui_state_cache[idx] = UiState::from_app_state( - &self.app_state, window_id, read_only_window + &self.app_state, window_id, read_only_window.clone() ); // Style the DOM @@ -275,7 +275,11 @@ impl App { // send webrender the size and buffer of the display Self::update_display(&window); // render the window (webrender will send an Awakened event when the frame is done) - render(window, &WindowId { id: idx }, &ui_description_cache[idx], &mut self.app_state.resources, true); + + let arc_mutext_t_clone = self.app_state.data.clone(); + let window_id = WindowId { id: idx }; + + render(arc_mutext_t_clone, window, window_id, read_only_window, &ui_description_cache[idx], &mut self.app_state.resources, true); awakened_task[idx] = false; } } @@ -687,8 +691,10 @@ fn do_hit_test_and_call_callbacks( } fn render( + app_data: Arc>, window: &mut Window, - _window_id: &WindowId, + window_id: WindowId, + read_only_window: ReadOnlyWindow, ui_description: &UiDescription, app_resources: &mut AppResources, has_window_size_changed: bool) @@ -700,6 +706,7 @@ fn render( let display_list = DisplayList::new_from_ui_description(ui_description); let builder = display_list.into_display_list_builder( + app_data, window.internal.pipeline_id, window.internal.epoch, &mut window.ui_solver, @@ -707,7 +714,9 @@ fn render( app_resources, &window.internal.api, has_window_size_changed, - &window.state.size); + &window.state.size, + window_id, + read_only_window); // NOTE: Display list has to be rebuilt every frame, otherwise, the epochs get out of sync window.internal.last_display_list_builder = builder.finalize().2; diff --git a/src/display_list.rs b/src/display_list.rs index c9070c08a..969ef6cde 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1,6 +1,7 @@ #![allow(unused_variables)] #![allow(unused_macros)] +use std::sync::{Arc, Mutex}; use webrender::api::*; use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; @@ -19,6 +20,7 @@ use { images::ImageId, text_cache::TextId, compositor::new_opengl_texture_id, + window::{WindowId, ReadOnlyWindow}, }; const DEFAULT_FONT_COLOR: TextColor = TextColor(ColorU { r: 0, b: 0, g: 0, a: 255 }); @@ -223,6 +225,7 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { pub fn into_display_list_builder( &self, + app_data: Arc>, pipeline_id: PipelineId, current_epoch: Epoch, ui_solver: &mut UiSolver, @@ -230,12 +233,14 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { app_resources: &mut AppResources, render_api: &RenderApi, mut has_window_size_changed: bool, - window_size: &WindowSize) + window_size: &WindowSize, + window_id: WindowId, + read_only_window: ReadOnlyWindow) -> DisplayListBuilder { use glium::glutin::dpi::LogicalSize; use std::collections::BTreeMap; - + let changeset = self.ui_descr.ui_descr_root.as_ref().and_then(|root| { let changeset = ui_solver.update_dom(root, &*(self.ui_descr.ui_descr_arena.borrow())); if changeset.is_empty() { None } else { Some(changeset) } @@ -307,18 +312,22 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } for (z_index, rects) in rects_in_rendering_order.into_iter() { - for rect_idx in rects { + for rect_idx in rects { + let bounds = ui_solver.query_bounds_of_rect(rect_idx); displaylist_handle_rect( &mut builder, current_epoch, rect_idx, &self.rectangles, &arena[rect_idx].data.node_type, - ui_solver.query_bounds_of_rect(rect_idx), + bounds, full_screen_rect, app_resources, render_api, - &mut resource_updates); + &mut resource_updates, + &app_data, + window_id, + read_only_window.clone()); } } @@ -328,17 +337,20 @@ impl<'a, T: Layout + 'a> DisplayList<'a, T> { } } -fn displaylist_handle_rect<'a>( +fn displaylist_handle_rect<'a, T: Layout>( builder: &mut DisplayListBuilder, current_epoch: Epoch, rect_idx: NodeId, arena: &Arena>, - html_node: &NodeType, + html_node: &NodeType, bounds: TypedRect, full_screen_rect: TypedRect, app_resources: &mut AppResources, render_api: &RenderApi, - resource_updates: &mut Vec) + resource_updates: &mut Vec, + app_data: &Arc>, + window_id: WindowId, + read_only_window: ReadOnlyWindow) { let rect = &arena[rect_idx].data; @@ -429,38 +441,48 @@ fn displaylist_handle_rect<'a>( Image(image_id) => { push_image(&info, builder, &bounds, app_resources, image_id); }, - GlTexture(texture) => { + GlTexture(texture_callback) => { + use window::WindowInfo; + + let t_locked = app_data.lock().unwrap(); + let window_info = WindowInfo { + window_id: window_id, + window: read_only_window, + resources: &app_resources, + }; + if let Some(texture) = (texture_callback.0)(&t_locked, window_info, bounds.size.width as usize, bounds.size.height as usize) { + use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; - use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; + let opaque = false; + let allow_mipmaps = true; + let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); + let key = render_api.generate_image_key(); + let external_image_id = ExternalImageId(new_opengl_texture_id() as u64); - let opaque = true; - let allow_mipmaps = true; - let descriptor = ImageDescriptor::new(texture.inner.width(), texture.inner.height(), ImageFormat::BGRA8, opaque, allow_mipmaps); - let key = render_api.generate_image_key(); - let external_image_id = ExternalImageId(new_opengl_texture_id() as u64); + let data = ImageData::External(ExternalImageData { + id: external_image_id, + channel_index: 0, + image_type: ExternalImageType::TextureHandle(TextureTarget::Default), + }); - let data = ImageData::External(ExternalImageData { - id: external_image_id, - channel_index: 0, - image_type: ExternalImageType::TextureHandle(TextureTarget::Default), - }); + ACTIVE_GL_TEXTURES.lock().unwrap() + .entry(current_epoch).or_insert_with(|| FastHashMap::default()) + .insert(external_image_id, ActiveTexture { texture: texture.clone() }); - ACTIVE_GL_TEXTURES.lock().unwrap() - .entry(current_epoch).or_insert_with(|| FastHashMap::default()) - .insert(external_image_id, ActiveTexture { texture: texture.clone() }); + resource_updates.push(ResourceUpdate::AddImage( + AddImage { key, descriptor, data, tiling: None } + )); - resource_updates.push(ResourceUpdate::AddImage( - AddImage { key, descriptor, data, tiling: None } - )); + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::Alpha, + key, + ColorF::WHITE); + } - builder.push_image( - &info, - bounds.size, - LayoutSize::zero(), - ImageRendering::Auto, - AlphaType::Alpha, - key, - ColorF::WHITE); }, } diff --git a/src/dom.rs b/src/dom.rs index 0f73c5ac2..f9c04d027 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -8,7 +8,7 @@ use std::{ }; use glium::{Texture2d, framebuffer::SimpleFrameBuffer}; use { - window::WindowEvent, + window::{WindowEvent, WindowInfo}, images::ImageId, cache::DomHash, text_cache::TextId, @@ -75,8 +75,7 @@ impl Eq for Callback { } impl Copy for Callback { } /// List of core DOM node types built-into by `azul`. -#[derive(Debug, Clone, PartialEq, Hash, Eq)] -pub enum NodeType { +pub enum NodeType { /// Regular div with no particular type of data attached Div, /// A small label that can be (optionally) be selectable with the mouse @@ -89,10 +88,100 @@ pub enum NodeType { /// OpenGL texture. The `Svg` widget deserizalizes itself into a texture /// Equality and Hash values are only checked by the OpenGl texture ID, /// azul does not check that the contents of two textures are the same - GlTexture(Texture), + GlTexture(GlTextureCallback), } -impl NodeType { +pub struct GlTextureCallback(pub fn(&T, WindowInfo, usize, usize) -> Option); + +// #[derive(Debug, Clone, PartialEq, Hash, Eq)] for GlTextureCallback + +impl fmt::Debug for GlTextureCallback { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "GlTextureCallback @ 0x{:x}", self.0 as usize) + } +} + +impl Clone for GlTextureCallback { + fn clone(&self) -> Self { + GlTextureCallback(self.0.clone()) + } +} + +impl Hash for GlTextureCallback { + fn hash(&self, state: &mut H) where H: Hasher { + state.write_usize(self.0 as usize); + } +} + +impl PartialEq for GlTextureCallback { + fn eq(&self, rhs: &Self) -> bool { + self.0 as usize == rhs.0 as usize + } +} + +impl Eq for GlTextureCallback { } +impl Copy for GlTextureCallback { } + +// #[derive(Debug, Clone, PartialEq, Hash, Eq)] for NodeType + +impl fmt::Debug for NodeType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::NodeType::*; + match self { + Div => write!(f, "NodeType::Div"), + Label(a) => write!(f, "NodeType::Label {{ {:?} }}", a), + Text(a) => write!(f, "NodeType::Text {{ {:?} }}", a), + Image(a) => write!(f, "NodeType::Image {{ {:?} }}", a), + GlTexture(a) => write!(f, "NodeType::GlTexture {{ {:?} }}", a), + } + } +} + +impl Clone for NodeType { + fn clone(&self) -> Self { + use self::NodeType::*; + match self { + Div => Div, + Label(a) => Label(a.clone()), + Text(a) => Text(a.clone()), + Image(a) => Image(a.clone()), + GlTexture(a) => GlTexture(a.clone()), + } + } +} + +impl Hash for NodeType { + fn hash(&self, state: &mut H) where H: Hasher { + use self::NodeType::*; + use std::mem; + mem::discriminant(&self).hash(state); + match self { + Div => { }, + Label(a) => a.hash(state), + Text(a) => a.hash(state), + Image(a) => a.hash(state), + GlTexture(a) => a.hash(state), + } + } +} + +impl PartialEq for NodeType { + fn eq(&self, rhs: &Self) -> bool { + use self::NodeType::*; + match (self, rhs) { + (Div, Div) => true, + (Label(a), Label(b)) => a == b, + (Text(a), Text(b)) => a == b, + (Image(a), Image(b)) => a == b, + (GlTexture(a), GlTexture(b)) => a == b, + _ => false, + } + } +} + +impl Eq for NodeType { } + +impl NodeType { pub(crate) fn get_css_id(&self) -> &'static str { use self::NodeType::*; match self { @@ -204,7 +293,7 @@ pub enum On { pub struct NodeData { /// `div` - pub node_type: NodeType, + pub node_type: NodeType, /// `#main` pub id: Option, /// `.myclass .otherclass` @@ -311,7 +400,7 @@ impl CallbackList { impl NodeData { /// Creates a new NodeData - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType) -> Self { Self { node_type: node_type, id: None, @@ -437,8 +526,8 @@ impl FromIterator> for Dom { } } -impl FromIterator for Dom { - fn from_iter>(iter: I) -> Self { +impl FromIterator> for Dom { + fn from_iter>>(iter: I) -> Self { iter.into_iter().map(|i| NodeData { node_type: i, .. Default::default() }).collect() } } @@ -447,7 +536,7 @@ impl Dom { /// Creates an empty DOM #[inline] - pub fn new(node_type: NodeType) -> Self { + pub fn new(node_type: NodeType) -> Self { let mut arena = Arena::new(); let root = arena.new_node(NodeData::new(node_type)); Self { diff --git a/src/lib.rs b/src/lib.rs index 3675dd3f5..393241805 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub mod prelude { pub use app::{App, AppConfig}; pub use app_state::AppState; pub use css::{Css, FakeCss}; - pub use dom::{Dom, NodeType, NodeData, Callback, On, UpdateScreen}; + pub use dom::{Dom, NodeType, NodeData, Callback, On, UpdateScreen, Texture, GlTextureCallback}; pub use traits::{Layout, Modify}; pub use window::{MonitorIter, Window, WindowCreateOptions, WindowId, MouseMode, UpdateBehaviour, UpdateMode, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 6198c800e..e730ad213 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -32,7 +32,7 @@ use webrender::api::{ColorU, ColorF, GlyphInstance}; use rusttype::{Font, Glyph}; use { FastHashMap, - dom::{Dom, NodeType, Callback}, + dom::{Callback, Texture}, traits::Layout, window::ReadOnlyWindow, css_parser::{FontId, FontSize}, @@ -2201,12 +2201,18 @@ impl Svg { self } +/* /// Renders the SVG to an OpenGL texture and creates the DOM pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) -> Dom where T: Layout { - let (window_width, window_height) = window.get_physical_size(); - let tex = window.create_texture(window_width as u32, window_height as u32); + Dom::new(NodeType::GlTexture(tex)) + } +*/ + + pub fn render_svg(&self, svg_cache: &SvgCache, window: &ReadOnlyWindow, width: usize, height: usize) -> Option { + + let tex = window.create_texture(width as u32, height as u32); // TODO: This currently doesn't work - only the first draw call is drawn // This is probably because either webrender or glium messes with the texture @@ -2216,12 +2222,12 @@ impl Svg { background_color.r, background_color.g, background_color.b, - background_color.a); + 0.0); let z_index: f32 = 0.5; let bbox: TypedRect = TypedRect { origin: TypedPoint2D::new(0.0, 0.0), - size: TypedSize2D::new(window_width as f32, window_height as f32), + size: TypedSize2D::new(width as f32, height as f32), }; let shader = svg_cache.init_shader(window); @@ -2292,7 +2298,7 @@ impl Svg { // TODO: apply FXAA shader } - Dom::new(NodeType::GlTexture(tex)) + Some(tex) } } diff --git a/src/window.rs b/src/window.rs index 5d55fba54..79bf9cb7e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -86,6 +86,7 @@ impl FakeWindow { /// Read-only window which can be used to create / draw /// custom OpenGL texture during the `.layout()` phase +#[derive(Clone)] pub struct ReadOnlyWindow { pub(crate) inner: Rc, } From c2beaf2153eb2cd53db534ad66395cf30d9eeb0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 12:10:23 +0200 Subject: [PATCH 246/868] Added VectorizedFont::from_path --- examples/debug.rs | 2 +- src/font.rs | 2 +- src/lib.rs | 4 ++-- src/widgets/svg.rs | 10 ++++++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 9c96904dd..7d8728b76 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -208,7 +208,7 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv fn main() { - macro_rules! css_path { () => ("/please/use/an/absolute/filepath/../debug.css") } + macro_rules! css_path { () => ("/please/use/an/absolute/file/path/../debug.css") } #[cfg(debug_assertions)] let css = Css::hot_reload(css_path!()).unwrap(); diff --git a/src/font.rs b/src/font.rs index 068480736..bb91f5b61 100644 --- a/src/font.rs +++ b/src/font.rs @@ -34,7 +34,7 @@ impl From for FontError { } /// Read font data to get font information, v_metrics, glyph info etc. -pub(crate) fn rusttype_load_font(data: Vec, index: Option) -> Result<(Font<'static>, Vec), FontError> { +pub fn rusttype_load_font(data: Vec, index: Option) -> Result<(Font<'static>, Vec), FontError> { let collection = FontCollection::from_bytes(data.clone())?; let font = collection.clone().into_font().unwrap_or(collection.font_at(index.and_then(|i| Some(i as usize)).unwrap_or(0))?); Ok((font, data)) diff --git a/src/lib.rs b/src/lib.rs index 393241805..e8cef8297 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,8 @@ pub mod traits; pub mod widgets; /// Window handling pub mod window; +/// Font handling +pub mod font; /// Global application (Initialization starts here) mod app; /// Wrapper for the application data & application state @@ -105,8 +107,6 @@ mod ui_state; mod cache; /// Image handling mod images; -/// Font handling -mod font; /// Window state handling, event filtering mod window_state; /// Application / context menu handling. Currently Win32 only. Also has parsing functions diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index e730ad213..13903c9b4 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1240,6 +1240,16 @@ impl VectorizedFont { glyph_stroke_map: Arc::new(Mutex::new(FastHashMap::default())), } } + + /// Loads a vectorized font from a path + pub fn from_path(path: &str) -> Option { + use std::fs; + use font::rusttype_load_font; + + let file_contents = fs::read(path).ok()?; + let font = rusttype_load_font(file_contents, None).ok()?.0; + Some(Self::from_font(&font)) + } } /// Note: Since `VectorizedFont` has to lock access on this, you'll want to get the From 7a5a292d1ea9b1a65943f221c303dcb479b39f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 12:33:44 +0200 Subject: [PATCH 247/868] Added VectorizedFont::has_font --- src/widgets/svg.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index 13903c9b4..dbae647cd 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1347,6 +1347,11 @@ impl VectorizedFontCache { self.vectorized_fonts.lock().unwrap().insert(id, Arc::new(font)); } + /// Returns true if the font cache has the respective font + pub fn has_font(&self, id: &FontId) -> bool { + self.vectorized_fonts.lock().unwrap().get(id).is_some() + } + pub fn get_font(&self, id: &FontId, app_resources: &AppResources) -> Option> { self.vectorized_fonts.lock().unwrap().entry(id.clone()) .or_insert_with(|| Arc::new(VectorizedFont::from_font(&*app_resources.get_font(&id).unwrap().0))); From 4cf77d1461530b17636310e34ca17c44f3c6eda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 12:38:58 +0200 Subject: [PATCH 248/868] Fixed AppResources::has_font --- src/app.rs | 4 ++-- src/{resources.rs => app_resources.rs} | 31 +++++++++++++------------- src/app_state.rs | 4 ++-- src/display_list.rs | 2 +- src/lib.rs | 4 ++-- src/text_layout.rs | 2 +- src/widgets/svg.rs | 2 +- src/window.rs | 2 +- 8 files changed, 26 insertions(+), 25 deletions(-) rename src/{resources.rs => app_resources.rs} (88%) diff --git a/src/app.rs b/src/app.rs index 1d547ab02..3532b19dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,7 +24,7 @@ use { dom::UpdateScreen, window::FakeWindow, css::{Css, FakeCss}, - resources::AppResources, + app_resources::AppResources, app_state::AppState, traits::Layout, ui_state::UiState, @@ -397,7 +397,7 @@ impl App { } /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) + pub fn has_font(&mut self, id: &FontId) -> bool { self.app_state.has_font(id) diff --git a/src/resources.rs b/src/app_resources.rs similarity index 88% rename from src/resources.rs rename to src/app_resources.rs index 5826573fc..4a8b3ea60 100644 --- a/src/resources.rs +++ b/src/app_resources.rs @@ -77,7 +77,7 @@ impl AppResources { } /// See `AppState::add_image()` - pub(crate) fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) + pub fn add_image, R: Read>(&mut self, id: S, data: &mut R, image_type: ImageType) -> Result, ImageError> { use images; // the module, not the crate! @@ -106,7 +106,7 @@ impl AppResources { } /// See `AppState::delete_image()` - pub(crate) fn delete_image>(&mut self, id: S) + pub fn delete_image>(&mut self, id: S) -> Option<()> { let image_id = self.css_ids_to_image_ids.remove(id.as_ref())?; @@ -127,7 +127,7 @@ impl AppResources { } /// See `AppState::has_image()` - pub(crate) fn has_image>(&mut self, id: S) + pub fn has_image>(&mut self, id: S) -> bool { let image_id = match self.css_ids_to_image_ids.get(id.as_ref()) { @@ -139,7 +139,7 @@ impl AppResources { } /// See `AppState::add_font()` - pub(crate) fn add_font, R: Read>(&mut self, id: S, data: &mut R) + pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) -> Result, FontError> { use font; @@ -195,14 +195,14 @@ impl AppResources { } /// Checks if a font is currently registered and ready-to-use - pub(crate) fn has_font>(&mut self, id: S) + pub fn has_font(&self, id: &FontId) -> bool { - self.font_data.borrow().get(&ExternalFont(id.into())).is_some() + self.font_data.borrow().get(id).is_some() } /// See `AppState::delete_font()` - pub(crate) fn delete_font>(&mut self, id: S) + pub fn delete_font>(&mut self, id: S) -> Option<()> { let id = ExternalFont(id.into()); @@ -223,7 +223,7 @@ impl AppResources { Some(()) } - pub(crate) fn add_text_uncached>(&mut self, text: S) + pub fn add_text_uncached>(&mut self, text: S) -> TextId { self.text_cache.add_text(text) @@ -232,7 +232,7 @@ impl AppResources { /// Calculates the widths for the words, then stores the widths of the words + the actual words /// /// This leads to a faster layout cycle, but has an upfront performance cost - pub(crate) fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) + pub fn add_text_cached>(&mut self, text: S, font_id: &FontId, font_size: FontSize) -> TextId { // First, insert the text into the text cache @@ -241,8 +241,9 @@ impl AppResources { id } - /// Promotes (and calculates all the metrics) for a given text ID. - pub(crate) fn cache_text(&mut self, id: TextId, font: FontId, size: FontSize) { + /// Promotes an uncached text to a cached text and calculates all the metrics + /// for a given text ID. + pub fn cache_text(&mut self, id: TextId, font: FontId, size: FontSize) { use rusttype::Scale; @@ -259,21 +260,21 @@ impl AppResources { .insert(size, words); } - pub(crate) fn delete_text(&mut self, id: TextId) { + pub fn delete_text(&mut self, id: TextId) { self.text_cache.delete_text(id); } - pub(crate) fn clear_all_texts(&mut self) { + pub fn clear_all_texts(&mut self) { self.text_cache.clear_all_texts(); } - pub(crate) fn get_clipboard_string(&mut self) + pub fn get_clipboard_string(&mut self) -> Result { self.clipboard.get_string_contents() } - pub(crate) fn set_clipboard_string(&mut self, contents: String) + pub fn set_clipboard_string(&mut self, contents: String) -> Result<(), ClipboardError> { self.clipboard.set_string_contents(contents) diff --git a/src/app_state.rs b/src/app_state.rs index 265c08440..98a3d83b5 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -13,7 +13,7 @@ use { task::Task, dom::UpdateScreen, traits::Layout, - resources::AppResources, + app_resources::AppResources, images::ImageType, font::FontError, css_parser::{FontId, FontSize, PixelValue}, @@ -150,7 +150,7 @@ impl AppState { } /// Checks if a font is currently registered and ready-to-use - pub fn has_font>(&mut self, id: S) + pub fn has_font(&self, id: &FontId) -> bool { self.resources.has_font(id) diff --git a/src/display_list.rs b/src/display_list.rs index 969ef6cde..2c106cd86 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -7,7 +7,7 @@ use app_units::{AU_PER_PX, MIN_AU, MAX_AU, Au}; use euclid::{TypedRect, TypedSize2D}; use { FastHashMap, - resources::AppResources, + app_resources::AppResources, traits::Layout, ui_description::{UiDescription, StyledNode}, ui_solver::UiSolver, diff --git a/src/lib.rs b/src/lib.rs index e8cef8297..9a5609598 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ mod app_state; /// Styling & CSS parsing mod css; /// Font & image resource handling, lookup and caching -mod resources; +mod app_resources; /// UI Description & display list handling (webrender) mod ui_description; /// Converts the UI description (the styled HTML nodes) @@ -158,7 +158,7 @@ pub mod prelude { VirtualKeyCode, ScanCode, Icon, }; pub use rusttype::Font; - pub use resources::AppResources; + pub use app_resources::AppResources; pub use task::TerminateDaemon; #[cfg(feature = "logging")] diff --git a/src/text_layout.rs b/src/text_layout.rs index 122231e0d..a04badcf7 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -4,7 +4,7 @@ use webrender::api::LayoutPixel; use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; use rusttype::{Font, Scale, GlyphId}; use { - resources::AppResources, + app_resources::AppResources, display_list::TextInfo, css_parser::{ TextAlignmentHorz, FontSize, BackgroundColor, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index dbae647cd..ee1879ba1 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -36,7 +36,7 @@ use { traits::Layout, window::ReadOnlyWindow, css_parser::{FontId, FontSize}, - resources::AppResources, + app_resources::AppResources, text_layout::{FontMetrics, LayoutTextResult, layout_text, PX_TO_PT}, }; diff --git a/src/window.rs b/src/window.rs index 79bf9cb7e..af94bd00b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -26,7 +26,7 @@ use { traits::Layout, compositor::Compositor, app::FrameEventInfo, - resources::AppResources, + app_resources::AppResources, ui_solver::UiSolver, }; From 2a4651688228740b72d5e3d33e6cbb112b5252f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 13:10:02 +0200 Subject: [PATCH 249/868] Fixed add_font and delete_font API --- src/app.rs | 10 +++++----- src/app_resources.rs | 13 ++++--------- src/app_state.rs | 6 +++--- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3532b19dd..7264a048d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -390,7 +390,7 @@ impl App { /// - `Ok(Some(()))` if an font with the same ID already exists. /// - `Ok(None)` if the font was added, but didn't exist previously. /// - `Err(e)` if the font couldn't be decoded - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + pub fn add_font(&mut self, id: FontId, data: &mut R) -> Result, FontError> { self.app_state.add_font(id, data) @@ -437,16 +437,16 @@ impl App { /// # /// # fn main() { /// let mut app = App::new(MyAppData { }, AppConfig::default()); - /// app.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); - /// app.delete_font("Webly Sleeky UI"); + /// app.add_font(FontId::ExternalFont("Webly Sleeky UI".into()), &mut TEST_FONT).unwrap(); + /// app.delete_font(&FontId::ExternalFont("Webly Sleeky UI".into())); /// // NOTE: The font isn't immediately removed, only in the next draw call /// app.mock_render_frame(); - /// assert!(!app.has_font("Webly Sleeky UI")); + /// assert!(!app.has_font(&FontId::ExternalFont("Webly Sleeky UI".into()))); /// # } /// ``` /// /// [`AppState::delete_font`]: ../app_state/struct.AppState.html#method.delete_font - pub fn delete_font>(&mut self, id: S) + pub fn delete_font(&mut self, id: &FontId) -> Option<()> { self.app_state.delete_font(id) diff --git a/src/app_resources.rs b/src/app_resources.rs index 4a8b3ea60..0fd3148cf 100644 --- a/src/app_resources.rs +++ b/src/app_resources.rs @@ -15,10 +15,7 @@ use { text_cache::{TextId, TextCache}, font::{FontState, FontError}, images::{ImageId, ImageState, ImageType}, - css_parser::{ - FontSize, - FontId::{self, ExternalFont} - }, + css_parser::{FontSize, FontId}, }; /// Font and image keys @@ -139,12 +136,12 @@ impl AppResources { } /// See `AppState::add_font()` - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + pub fn add_font(&mut self, id: FontId, data: &mut R) -> Result, FontError> { use font; - match self.font_data.borrow_mut().entry(ExternalFont(id.into())) { + match self.font_data.borrow_mut().entry(id) { Occupied(_) => Ok(None), Vacant(v) => { let mut font_data = Vec::::new(); @@ -202,11 +199,9 @@ impl AppResources { } /// See `AppState::delete_font()` - pub fn delete_font>(&mut self, id: S) + pub fn delete_font(&mut self, id: &FontId) -> Option<()> { - let id = ExternalFont(id.into()); - // TODO: can fonts that haven't been uploaded yet be deleted? let mut to_delete_font_key = None; diff --git a/src/app_state.rs b/src/app_state.rs index 98a3d83b5..fd3c2879f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -139,11 +139,11 @@ impl AppState { /// /// fn my_callback(app_state: &mut AppState, event: WindowEvent) -> UpdateScreen { /// /// Here you can add your font at runtime to the app_state - /// app_state.add_font("Webly Sleeky UI", &mut TEST_FONT).unwrap(); + /// app_state.add_font(FontId::ExternalFont("Webly Sleeky UI".into()), &mut TEST_FONT).unwrap(); /// UpdateScreen::DontRedraw /// } /// ``` - pub fn add_font, R: Read>(&mut self, id: S, data: &mut R) + pub fn add_font(&mut self, id: FontId, data: &mut R) -> Result, FontError> { self.resources.add_font(id, data) @@ -180,7 +180,7 @@ impl AppState { /// You can also call this function on an `App` struct, see [`App::add_font`]. /// /// [`App::add_font`]: ../app/struct.App.html#method.add_font - pub fn delete_font>(&mut self, id: S) + pub fn delete_font(&mut self, id: &FontId) -> Option<()> { self.resources.delete_font(id) From 5f26c7a077c615290c0a813b504af17524e95a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 17:18:11 +0200 Subject: [PATCH 250/868] Make CSS path relative to crate root in the examples --- examples/debug.rs | 6 +++--- examples/hot_reload.rs | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/debug.rs b/examples/debug.rs index 7d8728b76..0fbbd9dce 100644 --- a/examples/debug.rs +++ b/examples/debug.rs @@ -208,12 +208,12 @@ fn my_button_click_handler(app_state: &mut AppState, _event: WindowEv fn main() { - macro_rules! css_path { () => ("/please/use/an/absolute/file/path/../debug.css") } + macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/examples/debug.css")) } #[cfg(debug_assertions)] - let css = Css::hot_reload(css_path!()).unwrap(); + let css = Css::hot_reload(CSS_PATH!()).unwrap(); #[cfg(not(debug_assertions))] - let css = Css::new_from_str(include_str!(css_path!())).unwrap(); + let css = Css::new_from_str(include_str!(CSS_PATH!())).unwrap(); let mut app = App::new(MyAppData { map: None }, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); diff --git a/examples/hot_reload.rs b/examples/hot_reload.rs index 6b65ea477..2d95228da 100644 --- a/examples/hot_reload.rs +++ b/examples/hot_reload.rs @@ -18,13 +18,12 @@ impl Layout for MyDataModel { fn main() { - // workaround for: https://github.com/rust-lang/rust/issues/53749 - macro_rules! css_path { () => ("/please/use/an/absolute/file/path/../hot_reload.css") } + macro_rules! CSS_PATH { () => (concat!(env!("CARGO_MANIFEST_DIR"), "/examples/hot_reload.css")) } #[cfg(debug_assertions)] - let css = Css::hot_reload(css_path!()).unwrap(); + let css = Css::hot_reload(CSS_PATH!()).unwrap(); #[cfg(not(debug_assertions))] - let css = Css::new_from_str(include_str!(css_path!())).unwrap(); + let css = Css::new_from_str(include_str!(CSS_PATH!())).unwrap(); let mut app = App::new(MyDataModel, AppConfig::default()); app.create_window(WindowCreateOptions::default(), css).unwrap(); From cc4330ff1ed9f23ebc822ae5fb8d38cf9de3e459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Wed, 29 Aug 2018 17:34:24 +0200 Subject: [PATCH 251/868] Add HiDPI awareness for SVG and OpenGL textures --- src/display_list.rs | 9 ++++++- src/widgets/svg.rs | 64 +++++++++++++++++++++++++-------------------- src/window.rs | 7 ++++- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 2c106cd86..be524a33c 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -442,15 +442,22 @@ fn displaylist_handle_rect<'a, T: Layout>( push_image(&info, builder, &bounds, app_resources, image_id); }, GlTexture(texture_callback) => { + use window::WindowInfo; + let hidpi_factor = read_only_window.get_hidpi_factor(); + let width = (bounds.size.width * hidpi_factor as f32) as usize; + let height = (bounds.size.height * hidpi_factor as f32) as usize; + let t_locked = app_data.lock().unwrap(); let window_info = WindowInfo { window_id: window_id, window: read_only_window, resources: &app_resources, }; - if let Some(texture) = (texture_callback.0)(&t_locked, window_info, bounds.size.width as usize, bounds.size.height as usize) { + + if let Some(texture) = (texture_callback.0)(&t_locked, window_info, width, height) { + use compositor::{ActiveTexture, ACTIVE_GL_TEXTURES}; let opaque = false; diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index ee1879ba1..c80d355a0 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -1667,6 +1667,8 @@ pub struct Svg { pub zoom: f32, /// Whether an FXAA shader should be applied to the resulting OpenGL texture pub enable_fxaa: bool, + /// Should the SVG add the current HiDPI factor to the zoom? + pub enable_hidpi: bool, /// Background color (default: transparent) pub background_color: ColorU, } @@ -1678,6 +1680,7 @@ impl Default for Svg { pan: (0.0, 0.0), zoom: 1.0, enable_fxaa: false, + enable_hidpi: true, background_color: ColorU { r: 0, b: 0, g: 0, a: 0 }, } } @@ -2178,55 +2181,54 @@ fn curved_vector_text_to_vertices( impl Svg { #[inline] - pub fn with_layers(layers: Vec) - -> Self - { + pub fn with_layers(layers: Vec) -> Self { Self { layers: layers, .. Default::default() } } #[inline] - pub fn with_pan(mut self, horz: f32, vert: f32) - -> Self - { + pub fn with_pan(mut self, horz: f32, vert: f32) -> Self { self.pan = (horz, vert); self } #[inline] - pub fn with_zoom(mut self, zoom: f32) - -> Self - { + pub fn with_zoom(mut self, zoom: f32) -> Self { self.zoom = zoom; self } #[inline] - pub fn with_background_color(mut self, color: ColorU) - -> Self - { + pub fn with_hidpi_enabled(mut self, hidpi_enabled: bool) -> Self { + self.enable_hidpi = hidpi_enabled; + self + } + + #[inline] + pub fn with_background_color(mut self, color: ColorU) -> Self { self.background_color = color; self } #[inline] - pub fn with_fxaa(mut self, enable_fxaa: bool) - -> Self - { + pub fn with_fxaa(mut self, enable_fxaa: bool) -> Self { self.enable_fxaa = enable_fxaa; self } -/* - /// Renders the SVG to an OpenGL texture and creates the DOM - pub fn dom(&self, window: &ReadOnlyWindow, svg_cache: &SvgCache) - -> Dom where T: Layout + /// Renders the SVG to a texture. This should be called in a callback, since + /// during DOM construction, the items don't know how large they will be. + /// + /// The final texture will be width * height large. Note that width and height + /// need to be multiplied with the current `HiDPI` factor, otherwise the texture + /// will be blurry on HiDPI screens. This isn't done automatically. + pub fn render_svg( + &self, + svg_cache: &SvgCache, + window: &ReadOnlyWindow, + width: usize, + height: usize) + -> Option { - Dom::new(NodeType::GlTexture(tex)) - } -*/ - - pub fn render_svg(&self, svg_cache: &SvgCache, window: &ReadOnlyWindow, width: usize, height: usize) -> Option { - let tex = window.create_texture(width as u32, height as u32); // TODO: This currently doesn't work - only the first draw call is drawn @@ -2246,6 +2248,10 @@ impl Svg { }; let shader = svg_cache.init_shader(window); + let hidpi = window.get_hidpi_factor() as f32; + let zoom = if self.enable_hidpi { self.zoom * hidpi } else { self.zoom }; + let pan = if self.enable_hidpi { (self.pan.0 * hidpi, self.pan.1 * hidpi) } else { self.pan }; + let draw_options = DrawParameters { primitive_restart_index: true, .. Default::default() @@ -2279,8 +2285,8 @@ impl Svg { &bbox, color.into(), z_index, - self.pan, - self.zoom); + pan, + zoom); } } @@ -2302,8 +2308,8 @@ impl Svg { &bbox, stroke_color.into(), z_index, - self.pan, - self.zoom); + pan, + zoom); } } } diff --git a/src/window.rs b/src/window.rs index af94bd00b..786a77d43 100644 --- a/src/window.rs +++ b/src/window.rs @@ -100,10 +100,15 @@ impl Facade for ReadOnlyWindow { impl ReadOnlyWindow { pub fn get_physical_size(&self) -> (u32, u32) { - let hidpi = self.inner.gl_window().get_hidpi_factor(); + let hidpi = self.get_hidpi_factor(); self.inner.gl_window().get_inner_size().unwrap().to_physical(hidpi).into() } + /// Returns the current HiDPI factor. + pub fn get_hidpi_factor(&self) -> f64 { + self.inner.gl_window().get_hidpi_factor() + } + // Since webrender is asynchronous, we can't let the user draw // directly onto the frame or the texture since that has to be timed // with webrender From 50f87b254f46839d8d2e99f64d7c051f7ee5fd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 13:18:42 +0200 Subject: [PATCH 252/868] Fixed jiggling when calculating linear-gradient: missing .to_radians() --- examples/hot_reload.css | 2 +- src/css_parser.rs | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/hot_reload.css b/examples/hot_reload.css index 98b1a54aa..9155f8e50 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -1,5 +1,5 @@ #wrapper { - background: linear-gradient(300deg, red 0%, blue 100%); + background: linear-gradient(15deg, red 0%, blue 100%); flex-direction: row; } diff --git a/src/css_parser.rs b/src/css_parser.rs index b6fe542e0..517ba3f3f 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -11,7 +11,7 @@ pub use { }, }; use webrender::api::{BorderStyle, BorderSide, LayoutRect}; -use euclid::{TypedRotation2D, Angle, TypedPoint2D}; +use euclid::TypedPoint2D; pub(crate) const EM_HEIGHT: f32 = 16.0; /// Webrender measures in points, not in pixels! @@ -1047,11 +1047,13 @@ impl Direction { // see: https://hugogiraudel.com/2013/02/04/css-gradients/ - let width_half = rect.size.width / 2.0; - let height_half = rect.size.height / 2.0; + let deg = -deg; // negate winding direction + + let width_half = rect.size.width as usize / 2; + let height_half = rect.size.height as usize / 2; // hypothenuse_len is the length of the center of the rect to the corners - let hypothenuse_len = (width_half.powi(2) + height_half.powi(2)).sqrt(); + let hypothenuse_len = (((width_half * width_half) + (height_half * height_half)) as f64).sqrt(); // clamp the degree to 360 (so 410deg = 50deg) let mut deg = deg % 360.0; @@ -1066,16 +1068,16 @@ impl Direction { // Get the quadrant (corner) the angle is in and get the degree associated // with that corner. - let angle_to_top_left = (height_half / width_half).atan().to_degrees(); + let angle_to_top_left = (height_half as f64 / width_half as f64).atan().to_degrees(); // We need to calculate the angle from the center to the corner! - let ending_point_degrees = if deg <= 90.0 { + let ending_point_degrees = if deg < 90.0 { // top left corner 90.0 - angle_to_top_left - } else if deg <= 180.0 { + } else if deg < 180.0 { // bottom left corner 90.0 + angle_to_top_left - } else if deg <= 270.0 { + } else if deg < 270.0 { // bottom right corner 270.0 - angle_to_top_left } else /* deg > 270.0 && deg < 360.0 */ { @@ -1084,22 +1086,21 @@ impl Direction { }; // assuming deg = 36deg, then degree_diff_to_corner = 9deg - let degree_diff_to_corner = ending_point_degrees - deg; + let degree_diff_to_corner = ending_point_degrees - deg as f64; // Searched_len is the distance between the center of the rect and the // ending point of the gradient - let searched_len = (hypothenuse_len * degree_diff_to_corner.cos()).abs(); + let searched_len = (hypothenuse_len * degree_diff_to_corner.to_radians().cos()).abs(); // TODO: This searched_len is incorrect... // Once we have the length, we can simply rotate the length by the angle, // then translate it to the center of the rect - let point_location = LayoutPoint::new(0.0, searched_len); - let rot = TypedRotation2D::new(Angle::radians(deg.to_radians())); - let point_location: LayoutPoint = rot.transform_point(&point_location); + let dx = deg.to_radians().sin() * searched_len as f32; + let dy = deg.to_radians().cos() * searched_len as f32; - let start_point_location = LayoutPoint::new(width_half + point_location.x, height_half + point_location.y); - let end_point_location = LayoutPoint::new(width_half - point_location.x, height_half - point_location.y); + let start_point_location = LayoutPoint::new(width_half as f32 + dx, height_half as f32 + dy); + let end_point_location = LayoutPoint::new(width_half as f32 - dx, height_half as f32 - dy); (start_point_location, end_point_location) }, From 97b6dde0d2751e579e20e1d56e41c132a565674b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 14:12:03 +0200 Subject: [PATCH 253/868] Added parsing for top, left, right, bottom, position and padding --- src/css_parser.rs | 119 +++++++++++++++++++++++++++++++++++++++++++- src/display_list.rs | 6 +++ src/lib.rs | 1 + 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/css_parser.rs b/src/css_parser.rs index 517ba3f3f..8bdb8d7d8 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -93,6 +93,12 @@ pub enum ParsedCssProperty { MaxWidth(LayoutMaxWidth), MaxHeight(LayoutMaxHeight), + Position(LayoutPosition), + Top(LayoutTop), + Right(LayoutRight), + Left(LayoutLeft), + Bottom(LayoutBottom), + FlexWrap(LayoutWrap), FlexDirection(LayoutDirection), JustifyContent(LayoutJustifyContent), @@ -131,6 +137,12 @@ impl_from_no_lifetimes!(LayoutMinHeight, ParsedCssProperty::MinHeight); impl_from_no_lifetimes!(LayoutMaxWidth, ParsedCssProperty::MaxWidth); impl_from_no_lifetimes!(LayoutMaxHeight, ParsedCssProperty::MaxHeight); +impl_from_no_lifetimes!(LayoutPosition, ParsedCssProperty::Position); +impl_from_no_lifetimes!(LayoutTop, ParsedCssProperty::Top); +impl_from_no_lifetimes!(LayoutBottom, ParsedCssProperty::Bottom); +impl_from_no_lifetimes!(LayoutRight, ParsedCssProperty::Right); +impl_from_no_lifetimes!(LayoutLeft, ParsedCssProperty::Left); + impl_from_no_lifetimes!(LayoutWrap, ParsedCssProperty::FlexWrap); impl_from_no_lifetimes!(LayoutDirection, ParsedCssProperty::FlexDirection); impl_from_no_lifetimes!(LayoutJustifyContent, ParsedCssProperty::JustifyContent); @@ -176,6 +188,12 @@ impl ParsedCssProperty { "max-width" => Ok(parse_layout_max_width(value)?.into()), "max-height" => Ok(parse_layout_max_height(value)?.into()), + "position" => Ok(parse_layout_position(value)?.into()), + "top" => Ok(parse_layout_top(value)?.into()), + "right" => Ok(parse_layout_right(value)?.into()), + "left" => Ok(parse_layout_left(value)?.into()), + "bottom" => Ok(parse_layout_bottom(value)?.into()), + "flex-wrap" => Ok(parse_layout_wrap(value)?.into()), "flex-direction" => Ok(parse_layout_direction(value)?.into()), "justify-content" => Ok(parse_layout_justify_content(value)?.into()), @@ -794,6 +812,69 @@ fn parse_color_no_hash<'a>(input: &'a str) } } +pub struct LayoutPadding { + top: Option, + bottom: Option, + left: Option, + right: Option, +} + +pub enum LayoutPaddingParseError<'a> { + PixelParseError(PixelParseError<'a>), + TooManyValues, + TooFewValues, +} + +impl_from!(PixelParseError, LayoutPaddingParseError::PixelParseError); + +/// Parse a padding value such as +/// +/// "10px 10px" +fn parse_layout_padding<'a>(input: &'a str) +-> Result +{ + let mut input_iter = input.split_whitespace(); + let first = parse_pixel_value(input_iter.next().ok_or(LayoutPaddingParseError::TooFewValues)?)?; + let second = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + bottom: Some(first), + left: Some(first), + right: Some(first), + }), + })?; + let third = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + bottom: Some(first), + left: Some(second), + right: Some(second), + }), + })?; + let fourth = parse_pixel_value(match input_iter.next() { + Some(s) => s, + None => return Ok(LayoutPadding { + top: Some(first), + left: Some(second), + right: Some(second), + bottom: Some(third), + }), + })?; + + if input_iter.next().is_some() { + return Err(LayoutPaddingParseError::TooManyValues); + } + + Ok(LayoutPadding { + top: Some(first), + right: Some(second), + bottom: Some(third), + left: Some(fourth), + }) +} + /// Parse a CSS border such as /// /// "5px solid red" @@ -1578,6 +1659,15 @@ pub struct LayoutMinHeight(pub PixelValue); #[derive(Debug, PartialEq, Copy, Clone)] pub struct LayoutMaxHeight(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutTop(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutLeft(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutRight(pub PixelValue); +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct LayoutBottom(pub PixelValue); + #[derive(Debug, PartialEq, Copy, Clone)] pub struct LineHeight(pub PercentageValue); @@ -1589,6 +1679,13 @@ pub enum LayoutDirection { ColumnReverse, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LayoutPosition { + Static, + Relative, + Absolute, +} + impl Default for LayoutDirection { fn default() -> Self { LayoutDirection::Column @@ -1705,6 +1802,8 @@ impl Default for TextAlignmentVert { } } +/// Stylistic options of the rectangle that don't influence the layout +/// (todo: border-box?) #[derive(Default, Debug, Clone, PartialEq)] pub(crate) struct RectStyle { /// Background color of this rectangle @@ -1731,20 +1830,28 @@ pub(crate) struct RectStyle { pub(crate) line_height: Option, } -// Layout constraints for a given rectangle, such as "" +// Layout constraints for a given rectangle, such as "width", "min-width", "height", etc. #[derive(Default, Debug, Copy, Clone, PartialEq)] pub struct RectLayout { + pub width: Option, pub height: Option, pub min_width: Option, pub min_height: Option, pub max_width: Option, pub max_height: Option, + pub direction: Option, pub wrap: Option, pub justify_content: Option, pub align_items: Option, pub align_content: Option, + + pub position: Option, + pub top: Option, + pub bottom: Option, + pub right: Option, + pub left: Option, } typed_pixel_value_parser!(parse_layout_width, LayoutWidth); @@ -1754,6 +1861,11 @@ typed_pixel_value_parser!(parse_layout_min_width, LayoutMinWidth); typed_pixel_value_parser!(parse_layout_max_width, LayoutMaxWidth); typed_pixel_value_parser!(parse_layout_max_height, LayoutMaxHeight); +typed_pixel_value_parser!(parse_layout_top, LayoutTop); +typed_pixel_value_parser!(parse_layout_bottom, LayoutBottom); +typed_pixel_value_parser!(parse_layout_right, LayoutRight); +typed_pixel_value_parser!(parse_layout_left, LayoutLeft); + fn parse_line_height(input: &str) -> Result { @@ -1875,6 +1987,11 @@ multi_type_parser!(parse_shape, Shape, ["circle", Circle], ["ellipse", Ellipse]); +multi_type_parser!(parse_layout_position, LayoutPosition, + ["static", Static], + ["absolute", Absolute], + ["relative", Relative]); + multi_type_parser!(parse_layout_text_overflow, TextOverflowBehaviourInner, ["auto", Auto], ["scroll", Scroll], diff --git a/src/display_list.rs b/src/display_list.rs index be524a33c..c5157fd3e 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1011,6 +1011,12 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, + Top(t) => { rect.layout.top = Some(*t); }, + Bottom(b) => { rect.layout.bottom = Some(*b); }, + Right(r) => { rect.layout.right = Some(*r); }, + Left(l) => { rect.layout.left = Some(*l); }, + Position(p) => { rect.layout.position = Some(*p); }, + FlexWrap(w) => { rect.layout.wrap = Some(*w); }, FlexDirection(d) => { rect.layout.direction = Some(*d); }, JustifyContent(j) => { rect.layout.justify_content = Some(*j); }, diff --git a/src/lib.rs b/src/lib.rs index 9a5609598..55fb0fdbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,6 +147,7 @@ pub mod prelude { LayoutMinWidth, LayoutMinHeight, LayoutMaxWidth, LayoutMaxHeight, LayoutWrap, LayoutDirection, LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, + LayoutTop, LayoutBottom, LayoutRight, LayoutLeft, LinearGradientPreInfo, RadialGradientPreInfo, CssImageId, FontId, CssColor, LayoutPixel, TypedSize2D, BoxShadowClipMode, ColorU, ColorF, LayoutVector2D, From b6a25e14ca9881fb5305426ed2def64495966182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 15:51:57 +0200 Subject: [PATCH 254/868] Added tests for CSS parsing --- examples/hot_reload.css | 2 ++ src/css_parser.rs | 52 +++++++++++++++++++++++++++++++++++++++++ src/display_list.rs | 19 +++++++++++---- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/examples/hot_reload.css b/examples/hot_reload.css index 9155f8e50..88460d728 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -15,6 +15,8 @@ flex-direction: column-reverse; width: 400px; box-shadow: 0px 0px 50px black; + padding: 10px; + position: relative; } #yellow { diff --git a/src/css_parser.rs b/src/css_parser.rs index 8bdb8d7d8..b3a83e84a 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -99,6 +99,8 @@ pub enum ParsedCssProperty { Left(LayoutLeft), Bottom(LayoutBottom), + Padding(LayoutPadding), + FlexWrap(LayoutWrap), FlexDirection(LayoutDirection), JustifyContent(LayoutJustifyContent), @@ -143,6 +145,8 @@ impl_from_no_lifetimes!(LayoutBottom, ParsedCssProperty::Bottom); impl_from_no_lifetimes!(LayoutRight, ParsedCssProperty::Right); impl_from_no_lifetimes!(LayoutLeft, ParsedCssProperty::Left); +impl_from_no_lifetimes!(LayoutPadding, ParsedCssProperty::Padding); + impl_from_no_lifetimes!(LayoutWrap, ParsedCssProperty::FlexWrap); impl_from_no_lifetimes!(LayoutDirection, ParsedCssProperty::FlexDirection); impl_from_no_lifetimes!(LayoutJustifyContent, ParsedCssProperty::JustifyContent); @@ -194,6 +198,8 @@ impl ParsedCssProperty { "left" => Ok(parse_layout_left(value)?.into()), "bottom" => Ok(parse_layout_bottom(value)?.into()), + "padding" => Ok(parse_layout_padding(value)?.into()), + "flex-wrap" => Ok(parse_layout_wrap(value)?.into()), "flex-direction" => Ok(parse_layout_direction(value)?.into()), "justify-content" => Ok(parse_layout_justify_content(value)?.into()), @@ -277,6 +283,7 @@ pub enum CssParsingError<'a> { CssBackgroundParseError(CssBackgroundParseError<'a>), CssColorParseError(CssColorParseError<'a>), CssBorderRadiusParseError(CssBorderRadiusParseError<'a>), + PaddingParseError(LayoutPaddingParseError<'a>), /// Key is not supported, i.e. `#div { aldfjasdflk: 400px }` results in an /// `UnsupportedCssKey("aldfjasdflk", "400px")` error UnsupportedCssKey(&'a str, &'a str), @@ -291,6 +298,7 @@ impl_from!(CssImageParseError, CssParsingError::CssImageParseError); impl_from!(CssFontFamilyParseError, CssParsingError::CssFontFamilyParseError); impl_from!(CssBackgroundParseError, CssParsingError::CssBackgroundParseError); impl_from!(CssBorderRadiusParseError, CssParsingError::CssBorderRadiusParseError); +impl_from!(LayoutPaddingParseError, CssParsingError::PaddingParseError); impl<'a> From<(&'a str, &'a str)> for CssParsingError<'a> { fn from((a, b): (&'a str, &'a str)) -> Self { @@ -812,6 +820,7 @@ fn parse_color_no_hash<'a>(input: &'a str) } } +#[derive(Debug, Copy, Clone, PartialEq)] pub struct LayoutPadding { top: Option, bottom: Option, @@ -819,6 +828,7 @@ pub struct LayoutPadding { right: Option, } +#[derive(Debug, Clone, PartialEq)] pub enum LayoutPaddingParseError<'a> { PixelParseError(PixelParseError<'a>), TooManyValues, @@ -1852,6 +1862,8 @@ pub struct RectLayout { pub bottom: Option, pub right: Option, pub left: Option, + + pub padding: Option, } typed_pixel_value_parser!(parse_layout_width, LayoutWidth); @@ -2483,4 +2495,44 @@ mod css_tests { CssImageId(String::from("Cat 01")) ))); } + + #[test] + fn test_parse_padding_1() { + assert_eq!(parse_layout_padding("10px"), Ok(LayoutPadding { + top: Some(PixelValue::from_metric(CssMetric::Px, 10.0)), + right: Some(PixelValue::from_metric(CssMetric::Px, 10.0)), + bottom: Some(PixelValue::from_metric(CssMetric::Px, 10.0)), + left: Some(PixelValue::from_metric(CssMetric::Px, 10.0)), + })); + } + + #[test] + fn test_parse_padding_2() { + assert_eq!(parse_layout_padding("25px 50px"), Ok(LayoutPadding { + top: Some(PixelValue::from_metric(CssMetric::Px, 25.0)), + right: Some(PixelValue::from_metric(CssMetric::Px, 50.0)), + bottom: Some(PixelValue::from_metric(CssMetric::Px, 25.0)), + left: Some(PixelValue::from_metric(CssMetric::Px, 50.0)), + })); + } + + #[test] + fn test_parse_padding_3() { + assert_eq!(parse_layout_padding("25px 50px 75px"), Ok(LayoutPadding { + top: Some(PixelValue::from_metric(CssMetric::Px, 25.0)), + right: Some(PixelValue::from_metric(CssMetric::Px, 50.0)), + left: Some(PixelValue::from_metric(CssMetric::Px, 50.0)), + bottom: Some(PixelValue::from_metric(CssMetric::Px, 75.0)), + })); + } + + #[test] + fn test_parse_padding_4() { + assert_eq!(parse_layout_padding("25px 50px 75px 100px"), Ok(LayoutPadding { + top: Some(PixelValue::from_metric(CssMetric::Px, 25.0)), + right: Some(PixelValue::from_metric(CssMetric::Px, 50.0)), + bottom: Some(PixelValue::from_metric(CssMetric::Px, 75.0)), + left: Some(PixelValue::from_metric(CssMetric::Px, 100.0)), + })); + } } \ No newline at end of file diff --git a/src/display_list.rs b/src/display_list.rs index c5157fd3e..a87e9640d 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -979,7 +979,7 @@ fn push_font( } } -/// Populate and parse the CSS style properties +/// Populate the CSS style properties of the `DisplayRectangle` fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHashMap) { use css_parser::ParsedCssProperty::{self, *}; @@ -1011,11 +1011,14 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash MaxWidth(mw) => { rect.layout.max_width = Some(*mw); }, MaxHeight(mh) => { rect.layout.max_height = Some(*mh); }, + Position(p) => { rect.layout.position = Some(*p); }, Top(t) => { rect.layout.top = Some(*t); }, Bottom(b) => { rect.layout.bottom = Some(*b); }, Right(r) => { rect.layout.right = Some(*r); }, Left(l) => { rect.layout.left = Some(*l); }, - Position(p) => { rect.layout.position = Some(*p); }, + + // TODO: merge new padding with existing padding + Padding(p) => { rect.layout.padding = Some(*p); }, FlexWrap(w) => { rect.layout.wrap = Some(*w); }, FlexDirection(d) => { rect.layout.direction = Some(*d); }, @@ -1038,9 +1041,15 @@ fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHash Dynamic(dynamic_property) => { let calculated_property = css_overrides.get(&dynamic_property.dynamic_id); if let Some(overridden_property) = calculated_property { - assert!(property_type_matches(overridden_property, &dynamic_property.default), - "css values don't have the same discriminant type"); - apply_parsed_css_property(rect, overridden_property); + if property_type_matches(overridden_property, &dynamic_property.default) { + apply_parsed_css_property(rect, overridden_property); + } else { + error!( + "Dynamic CSS property on rect {:?} don't have the same discriminant type,\r\n + cannot override {:?} with {:?} - enum discriminant mismatch", + rect, dynamic_property.default, overridden_property + ) + } } else { apply_parsed_css_property(rect, &dynamic_property.default); } From 16b9a2f352ddeb8fde37248e7967e87622bc341d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 17:36:45 +0200 Subject: [PATCH 255/868] Text now respects padding, added common position:relative search --- examples/hot_reload.css | 10 ++++--- examples/hot_reload.rs | 13 ++++++++- src/css_parser.rs | 8 +++--- src/display_list.rs | 64 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/examples/hot_reload.css b/examples/hot_reload.css index 88460d728..d3eeb56ac 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -6,17 +6,18 @@ #red { background-color: #BF0C2B; color: white; - font-size: 12px; + font-size: 30px; font-family: sans-serif; - width: 200px; + padding: 150px; + width: 400px; + text-align: left; } #sub-wrapper { flex-direction: column-reverse; width: 400px; box-shadow: 0px 0px 50px black; - padding: 10px; - position: relative; + position: absolute; } #yellow { @@ -34,4 +35,5 @@ background-color: #F14C13; box-shadow: 2px 2px 10px black; width: 50px; + position: relative; } \ No newline at end of file diff --git a/examples/hot_reload.rs b/examples/hot_reload.rs index 2d95228da..1ad1def3d 100644 --- a/examples/hot_reload.rs +++ b/examples/hot_reload.rs @@ -7,7 +7,18 @@ struct MyDataModel; impl Layout for MyDataModel { fn layout(&self, _info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") - .with_child(Dom::new(NodeType::Label(format!("Hello World"))).with_id("red")) + .with_child(Dom::new(NodeType::Label(format!( + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, \ + sed diam nonumy eirmod tempor invidunt ut labore et dolore \ + magna aliquyam erat, sed diam voluptua. At vero eos et accusam \ + et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ + no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem \ + ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam \ + erat, sed diam voluptua. At vero eos et accusam et justo duo \ + dolores et ea rebum. Stet clita kasd gubergren, no sea takimata \ + sanctus est Lorem ipsum dolor sit amet.") + )).with_id("red")) .with_child(Dom::new(NodeType::Div).with_id("sub-wrapper") .with_child(Dom::new(NodeType::Div).with_id("yellow") .with_child(Dom::new(NodeType::Div).with_id("below-yellow"))) diff --git a/src/css_parser.rs b/src/css_parser.rs index b3a83e84a..2403812ce 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -822,10 +822,10 @@ fn parse_color_no_hash<'a>(input: &'a str) #[derive(Debug, Copy, Clone, PartialEq)] pub struct LayoutPadding { - top: Option, - bottom: Option, - left: Option, - right: Option, + pub top: Option, + pub bottom: Option, + pub left: Option, + pub right: Option, } #[derive(Debug, Clone, PartialEq)] diff --git a/src/display_list.rs b/src/display_list.rs index a87e9640d..d5dacb94f 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -413,6 +413,27 @@ fn displaylist_handle_rect<'a, T: Layout>( match html_node { Div => { /* nothing special to do */ }, Label(text) => { + + // Adjust the bounds by the padding + let mut text_bounds = rect.layout.padding.as_ref().and_then(|padding| { + Some(subtract_padding(&bounds, padding)) + }).unwrap_or(bounds); + + text_bounds.size.width = text_bounds.size.width.max(0.0); + text_bounds.size.height = text_bounds.size.height.max(0.0); + + let text_clip_region_id = rect.layout.padding.and_then(|_| + Some(builder.define_clip(text_bounds, vec![ComplexClipRegion { + rect: text_bounds, + radii: BorderRadius::zero(), + mode: ClipMode::Clip, + }], None)) + ); + + if let Some(text_clip_id) = text_clip_region_id { + builder.push_clip_id(text_clip_id); + } + push_text( &info, &TextInfo::Uncached(text.clone()), @@ -420,10 +441,14 @@ fn displaylist_handle_rect<'a, T: Layout>( &rect.style, app_resources, &render_api, - &bounds, + &text_bounds, resource_updates, horz_alignment, vert_alignment); + + if text_clip_region_id.is_some() { + builder.pop_clip_id(); + } }, Text(text_id) => { push_text( @@ -1086,6 +1111,10 @@ fn create_layout_constraints<'a, T: Layout>( let mut layout_constraints = Vec::new(); + if let Some(LayoutPosition::Relative) = rect.layout.position { + println!("nearest common absolute ancestor for node {:?} is: {:?}", node_id, get_nearest_relative_ancestor(node_id, display_rectangles)); + } + // Insert the max height and width constraints // // min-width and max-width are stronger than width because the width has to be between min and max width @@ -1175,6 +1204,39 @@ fn create_layout_constraints<'a, T: Layout>( layout_constraints } +/// Subtracts the padding from the bounds, returning the new bounds +/// +/// Warning: The resulting rectangle may have negative width or height +fn subtract_padding(bounds: &TypedRect, padding: &LayoutPadding) +-> TypedRect +{ + let top = padding.top.and_then(|top| Some(top.to_pixels())).unwrap_or(0.0); + let bottom = padding.bottom.and_then(|bottom| Some(bottom.to_pixels())).unwrap_or(0.0); + let left = padding.left.and_then(|left| Some(left.to_pixels())).unwrap_or(0.0); + let right = padding.right.and_then(|right| Some(right.to_pixels())).unwrap_or(0.0); + + let mut new_bounds = *bounds; + + new_bounds.origin.x += left; + new_bounds.size.width -= right + left; + new_bounds.origin.y += top; + new_bounds.size.height -= top + bottom; + + new_bounds +} + +/// Returns the nearest common ancestor +fn get_nearest_relative_ancestor<'a>(start_node_id: NodeId, arena: &Arena>) -> Option { + let mut current_node = start_node_id; + while let Some(parent) = arena[current_node].parent() { + if let Some(LayoutPosition::Absolute) = arena[parent].data.layout.position { + return Some(parent); + } + current_node = parent; + } + None +} + // Empty test, for some reason codecov doesn't detect any files (and therefore // doesn't report codecov % correctly) except if they have at least one test in // the file. This is an empty test, which should be updated later on From 91ad61b061e6089c5b4c6e8d5d952680f602679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 19:52:54 +0200 Subject: [PATCH 256/868] Make scrollbars appear on the parent of the text instead of the text itself --- examples/hot_reload.css | 5 + examples/hot_reload.rs | 6 +- src/app_resources.rs | 15 ++- src/display_list.rs | 208 ++++++++++++++++++++++------------------ 4 files changed, 138 insertions(+), 96 deletions(-) diff --git a/examples/hot_reload.css b/examples/hot_reload.css index d3eeb56ac..88b171626 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -36,4 +36,9 @@ box-shadow: 2px 2px 10px black; width: 50px; position: relative; +} + +#cat { + /*background: image("Cat01");*/ + background-color: red; } \ No newline at end of file diff --git a/examples/hot_reload.rs b/examples/hot_reload.rs index 1ad1def3d..207948256 100644 --- a/examples/hot_reload.rs +++ b/examples/hot_reload.rs @@ -2,10 +2,12 @@ extern crate azul; use azul::prelude::*; +const TEST_IMAGE: &[u8] = include_bytes!("../assets/images/cat_image.jpg"); + struct MyDataModel; impl Layout for MyDataModel { - fn layout(&self, _info: WindowInfo) -> Dom { + fn layout(&self, info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") .with_child(Dom::new(NodeType::Label(format!( "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, \ @@ -24,6 +26,7 @@ impl Layout for MyDataModel { .with_child(Dom::new(NodeType::Div).with_id("below-yellow"))) .with_child(Dom::new(NodeType::Div).with_id("grey")) ) + .with_child(Dom::new(NodeType::Image(info.resources.get_image("Cat01").unwrap())).with_id("cat")) } } @@ -37,6 +40,7 @@ fn main() { let css = Css::new_from_str(include_str!(CSS_PATH!())).unwrap(); let mut app = App::new(MyDataModel, AppConfig::default()); + app.add_image("Cat01", &mut TEST_IMAGE, ImageType::Jpeg).unwrap(); app.create_window(WindowCreateOptions::default(), css).unwrap(); app.run().unwrap(); } \ No newline at end of file diff --git a/src/app_resources.rs b/src/app_resources.rs index 0fd3148cf..4ab4bdc40 100644 --- a/src/app_resources.rs +++ b/src/app_resources.rs @@ -124,15 +124,22 @@ impl AppResources { } /// See `AppState::has_image()` - pub fn has_image>(&mut self, id: S) + pub fn has_image>(&self, id: S) -> bool { - let image_id = match self.css_ids_to_image_ids.get(id.as_ref()) { + let image_id = match self.get_image(id) { None => return false, Some(s) => s, }; - self.images.get(image_id).is_some() + self.images.get(&image_id).is_some() + } + + /// Returns the image ID looked up from a string + pub fn get_image>(&self, id: S) + -> Option + { + self.css_ids_to_image_ids.get(id.as_ref()).and_then(|id| Some(*id)) } /// See `AppState::add_font()` @@ -263,7 +270,7 @@ impl AppResources { self.text_cache.clear_all_texts(); } - pub fn get_clipboard_string(&mut self) + pub fn get_clipboard_string(&self) -> Result { self.clipboard.get_string_contents() diff --git a/src/display_list.rs b/src/display_list.rs index d5dacb94f..1b5fadc8b 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -409,62 +409,74 @@ fn displaylist_handle_rect<'a, T: Layout>( let (horz_alignment, vert_alignment) = determine_text_alignment(rect_idx, arena); - // handle the special content of the node - match html_node { - Div => { /* nothing special to do */ }, - Label(text) => { + let scrollbar_style = ScrollbarInfo { + width: 17, + padding: 2, + background_color: BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 }), + triangle_color: BackgroundColor(ColorU { r: 163, g: 163, b: 163, a: 255 }), + bar_color: BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 }), + }; - // Adjust the bounds by the padding - let mut text_bounds = rect.layout.padding.as_ref().and_then(|padding| { - Some(subtract_padding(&bounds, padding)) - }).unwrap_or(bounds); + // The only thing changed between TextId and String is + //`TextInfo::Cached` vs `TextInfo::Uncached` - reduce code duplication + let push_text_wrapper = | + text_info: &TextInfo, + builder: &mut DisplayListBuilder, + app_resources: &mut AppResources, + resource_updates: &mut Vec| + { + // Adjust the bounds by the padding + let mut text_bounds = rect.layout.padding.as_ref().and_then(|padding| { + Some(subtract_padding(&bounds, padding)) + }).unwrap_or(bounds); + + text_bounds.size.width = text_bounds.size.width.max(0.0); + text_bounds.size.height = text_bounds.size.height.max(0.0); + + let text_clip_region_id = rect.layout.padding.and_then(|_| + Some(builder.define_clip(text_bounds, vec![ComplexClipRegion { + rect: text_bounds, + radii: BorderRadius::zero(), + mode: ClipMode::Clip, + }], None)) + ); + + if let Some(text_clip_id) = text_clip_region_id { + builder.push_clip_id(text_clip_id); + } - text_bounds.size.width = text_bounds.size.width.max(0.0); - text_bounds.size.height = text_bounds.size.height.max(0.0); + let overflow = push_text( + &info, + text_info, + builder, + &rect.style, + app_resources, + &render_api, + &text_bounds, + &bounds, + resource_updates, + horz_alignment, + vert_alignment, + &scrollbar_style); - let text_clip_region_id = rect.layout.padding.and_then(|_| - Some(builder.define_clip(text_bounds, vec![ComplexClipRegion { - rect: text_bounds, - radii: BorderRadius::zero(), - mode: ClipMode::Clip, - }], None)) - ); + if text_clip_region_id.is_some() { + builder.pop_clip_id(); + } - if let Some(text_clip_id) = text_clip_region_id { - builder.push_clip_id(text_clip_id); - } + overflow + }; - push_text( - &info, - &TextInfo::Uncached(text.clone()), - builder, - &rect.style, - app_resources, - &render_api, - &text_bounds, - resource_updates, - horz_alignment, - vert_alignment); - - if text_clip_region_id.is_some() { - builder.pop_clip_id(); - } + // handle the special content of the node + let overflow_result = match html_node { + Div => { None }, + Label(text) => { + push_text_wrapper(&TextInfo::Uncached(text.clone()), builder, app_resources, resource_updates) }, Text(text_id) => { - push_text( - &info, - &TextInfo::Cached(*text_id), - builder, - &rect.style, - app_resources, - &render_api, - &bounds, - resource_updates, - horz_alignment, - vert_alignment); + push_text_wrapper(&TextInfo::Cached(*text_id), builder, app_resources, resource_updates) }, Image(image_id) => { - push_image(&info, builder, &bounds, app_resources, image_id); + push_image(&info, builder, &bounds, app_resources, image_id) }, GlTexture(texture_callback) => { @@ -515,7 +527,21 @@ fn displaylist_handle_rect<'a, T: Layout>( ColorF::WHITE); } - }, + None + } + }; + + if let Some(overflow) = &overflow_result { + // push scrollbars if necessary + use text_layout::TextOverflow; + + // If the rectangle should have a scrollbar, push a scrollbar onto the display list + if let TextOverflow::IsOverflowing(amount_vert) = overflow.text_overflow.vertical { + push_scrollbar(builder, &overflow.text_overflow, &scrollbar_style, &bounds, &rect.style.border) + } + if let TextOverflow::IsOverflowing(amount_horz) = overflow.text_overflow.horizontal { + push_scrollbar(builder, &overflow.text_overflow, &scrollbar_style, &bounds, &rect.style.border) + } } if clip_region_id.is_some() { @@ -570,6 +596,12 @@ fn push_rect( builder.push_rect(&info, color.0.into()); } +struct OverflowInfo { + pub text_overflow: TextOverflowPass2, +} + +/// Note: automatically pushes the scrollbars on the parent, +/// this should be refined later #[inline] fn push_text( info: &PrimitiveInfo, @@ -579,44 +611,39 @@ fn push_text( app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, + parent_bounds: &TypedRect, resource_updates: &mut Vec, horz_alignment: TextAlignmentHorz, - vert_alignment: TextAlignmentVert) + vert_alignment: TextAlignmentVert, + scrollbar_info: &ScrollbarInfo) +-> Option { use text_layout; if text.is_empty_text(&*app_resources) { - return; + return None; } let font_family = match style.font_family { Some(ref ff) => ff, - None => return, + None => return None, }; let font_size = style.font_size.unwrap_or(DEFAULT_FONT_SIZE); let font_size_app_units = Au((font_size.0.to_pixels() as i32) * AU_PER_PX as i32); - let font_id = match font_family.fonts.get(0) { Some(s) => s, None => { error!("div @ {:?} has no font assigned!", bounds); return; }}; + let font_id = match font_family.fonts.get(0) { Some(s) => s, None => { error!("div @ {:?} has no font assigned!", bounds); return None; }}; let font_result = push_font(font_id, font_size_app_units, resource_updates, app_resources, render_api); let font_instance_key = match font_result { Some(f) => f, - None => return, + None => return None, }; let line_height = style.line_height; let overflow_behaviour = style.overflow.unwrap_or(LayoutOverflow::default()); - let scrollbar_style = ScrollbarInfo { - width: 17, - padding: 2, - background_color: BackgroundColor(ColorU { r: 241, g: 241, b: 241, a: 255 }), - triangle_color: BackgroundColor(ColorU { r: 163, g: 163, b: 163, a: 255 }), - bar_color: BackgroundColor(ColorU { r: 193, g: 193, b: 193, a: 255 }), - }; - - let (positioned_glyphs, scrollbar_info) = text_layout::get_glyphs( + let (positioned_glyphs, text_overflow) = text_layout::get_glyphs( app_resources, bounds, horz_alignment, @@ -626,7 +653,7 @@ fn push_text( line_height, text, &overflow_behaviour, - &scrollbar_style + scrollbar_info ); let font_color = style.font_color.unwrap_or(DEFAULT_FONT_COLOR).0.into(); @@ -643,23 +670,13 @@ fn push_text( builder.push_text(&info, &positioned_glyphs, font_instance_key, font_color, Some(options)); - use text_layout::TextOverflow; - - // If the rectangle should have a scrollbar, push a scrollbar onto the display list - // TODO !!! - if let TextOverflow::IsOverflowing(amount_vert) = scrollbar_info.vertical { - push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) - } - if let TextOverflow::IsOverflowing(amount_horz) = scrollbar_info.horizontal { - push_scrollbar(builder, &overflow_behaviour, &scrollbar_info, &scrollbar_style, bounds, &style.border) - } + Some(OverflowInfo { text_overflow }) } /// Adds a scrollbar to the left or bottom side of a rectangle. /// TODO: make styling configurable (like the width / style of the scrollbar) fn push_scrollbar( builder: &mut DisplayListBuilder, - display_behaviour: &LayoutOverflow, scrollbar_info: &TextOverflowPass2, scrollbar_style: &ScrollbarInfo, bounds: &TypedRect, @@ -917,23 +934,28 @@ fn push_image( bounds: &TypedRect, app_resources: &AppResources, image_id: &ImageId) +-> Option { - if let Some(image_info) = app_resources.images.get(image_id) { - use images::ImageState::*; - match image_info { - Uploaded(image_info) => { - builder.push_image( - &info, - bounds.size, - LayoutSize::zero(), - ImageRendering::Auto, - AlphaType::PremultipliedAlpha, - image_info.key, - ColorF::WHITE); - }, - _ => { }, - } + use images::ImageState::*; + + let image_info = app_resources.images.get(image_id)?; + + match image_info { + Uploaded(image_info) => { + builder.push_image( + &info, + bounds.size, + LayoutSize::zero(), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + image_info.key, + ColorF::WHITE); + }, + _ => { }, } + + // TODO: determine if image has overflown its container + None } #[inline] @@ -1225,8 +1247,12 @@ fn subtract_padding(bounds: &TypedRect, padding: &LayoutPaddin new_bounds } -/// Returns the nearest common ancestor -fn get_nearest_relative_ancestor<'a>(start_node_id: NodeId, arena: &Arena>) -> Option { +/// Returns the nearest common ancestor with a `position: relative` attribute +/// or `None` if there is no ancestor that has `position: relative`. Usually +/// used in conjunction with `position: absolute` +fn get_nearest_relative_ancestor<'a>(start_node_id: NodeId, arena: &Arena>) +-> Option +{ let mut current_node = start_node_id; while let Some(parent) = arena[current_node].parent() { if let Some(LayoutPosition::Absolute) = arena[parent].data.layout.position { From 633b54abf04a7b1e4a8d7f23ca3f4a98e55ad5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 20:21:03 +0200 Subject: [PATCH 257/868] Fix (?) text rendering and character positioning --- src/display_list.rs | 209 +++++++++++++++++++++++--------------------- src/text_layout.rs | 10 ++- 2 files changed, 118 insertions(+), 101 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index 1b5fadc8b..cb5a7a6c8 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -407,7 +407,7 @@ fn displaylist_handle_rect<'a, T: Layout>( builder, &rect.style); - let (horz_alignment, vert_alignment) = determine_text_alignment(rect_idx, arena); + let (horz_alignment, vert_alignment) = determine_text_alignment(rect); let scrollbar_style = ScrollbarInfo { width: 17, @@ -432,7 +432,7 @@ fn displaylist_handle_rect<'a, T: Layout>( text_bounds.size.width = text_bounds.size.width.max(0.0); text_bounds.size.height = text_bounds.size.height.max(0.0); - +/* let text_clip_region_id = rect.layout.padding.and_then(|_| Some(builder.define_clip(text_bounds, vec![ComplexClipRegion { rect: text_bounds, @@ -444,7 +444,7 @@ fn displaylist_handle_rect<'a, T: Layout>( if let Some(text_clip_id) = text_clip_region_id { builder.push_clip_id(text_clip_id); } - +*/ let overflow = push_text( &info, text_info, @@ -453,16 +453,15 @@ fn displaylist_handle_rect<'a, T: Layout>( app_resources, &render_api, &text_bounds, - &bounds, resource_updates, horz_alignment, vert_alignment, &scrollbar_style); - +/* if text_clip_region_id.is_some() { builder.pop_clip_id(); } - +*/ overflow }; @@ -547,44 +546,7 @@ fn displaylist_handle_rect<'a, T: Layout>( if clip_region_id.is_some() { builder.pop_clip_id(); } -} - -/// For a given rectangle, determines what text alignment should be used -fn determine_text_alignment<'a>(rect_idx: NodeId, arena: &Arena>) --> (TextAlignmentHorz, TextAlignmentVert) -{ - let mut horz_alignment = TextAlignmentHorz::default(); - let mut vert_alignment = TextAlignmentVert::default(); - - let rect = &arena[rect_idx]; - - if let Some(align_items) = rect.data.layout.align_items { - // Vertical text alignment - use css_parser::LayoutAlignItems; - match align_items { - LayoutAlignItems::Start => vert_alignment = TextAlignmentVert::Top, - LayoutAlignItems::End => vert_alignment = TextAlignmentVert::Bottom, - // technically stretch = blocktext, but we don't have that yet - _ => vert_alignment = TextAlignmentVert::Center, - } - } - if let Some(justify_content) = rect.data.layout.justify_content { - use css_parser::LayoutJustifyContent; - // Horizontal text alignment - match justify_content { - LayoutJustifyContent::Start => horz_alignment = TextAlignmentHorz::Left, - LayoutJustifyContent::End => horz_alignment = TextAlignmentHorz::Right, - _ => horz_alignment = TextAlignmentHorz::Center, - } - } - - if let Some(text_align) = rect.data.style.text_align { - // Horizontal text alignment with higher priority - horz_alignment = text_align; - } - - (horz_alignment, vert_alignment) } #[inline] @@ -611,7 +573,6 @@ fn push_text( app_resources: &mut AppResources, render_api: &RenderApi, bounds: &TypedRect, - parent_bounds: &TypedRect, resource_updates: &mut Vec, horz_alignment: TextAlignmentHorz, vert_alignment: TextAlignmentVert, @@ -692,66 +653,60 @@ fn push_scrollbar( bounds.size.height -= border_widths.bottom; } - { - // Background of scrollbar (vertical) - let scrollbar_vertical_background = TypedRect:: { - origin: TypedPoint2D::new(bounds.origin.x + bounds.size.width - scrollbar_style.width as f32, bounds.origin.y), - size: TypedSize2D::new(scrollbar_style.width as f32, bounds.size.height), - }; + // Background of scrollbar (vertical) + let scrollbar_vertical_background = TypedRect:: { + origin: TypedPoint2D::new(bounds.origin.x + bounds.size.width - scrollbar_style.width as f32, bounds.origin.y), + size: TypedSize2D::new(scrollbar_style.width as f32, bounds.size.height), + }; - let scrollbar_vertical_background_info = PrimitiveInfo { - rect: scrollbar_vertical_background, - clip_rect: bounds, - is_backface_visible: false, - tag: None, // TODO: for hit testing - }; + let scrollbar_vertical_background_info = PrimitiveInfo { + rect: scrollbar_vertical_background, + clip_rect: bounds, + is_backface_visible: false, + tag: None, // TODO: for hit testing + }; - push_rect(&scrollbar_vertical_background_info, builder, &scrollbar_style.background_color); - } + push_rect(&scrollbar_vertical_background_info, builder, &scrollbar_style.background_color); - { - // Actual scroll bar - let scrollbar_vertical_bar = TypedRect:: { - origin: TypedPoint2D::new( - bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, - bounds.origin.y + scrollbar_style.width as f32), - size: TypedSize2D::new( - (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, - bounds.size.height - (scrollbar_style.width * 2) as f32), - }; + // Actual scroll bar + let scrollbar_vertical_bar = TypedRect:: { + origin: TypedPoint2D::new( + bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, + bounds.origin.y + scrollbar_style.width as f32), + size: TypedSize2D::new( + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, + bounds.size.height - (scrollbar_style.width * 2) as f32), + }; - let scrollbar_vertical_bar_info = PrimitiveInfo { - rect: scrollbar_vertical_bar, - clip_rect: bounds, - is_backface_visible: false, - tag: None, // TODO: for hit testing - }; + let scrollbar_vertical_bar_info = PrimitiveInfo { + rect: scrollbar_vertical_bar, + clip_rect: bounds, + is_backface_visible: false, + tag: None, // TODO: for hit testing + }; - push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_color); - } + push_rect(&scrollbar_vertical_bar_info, builder, &scrollbar_style.bar_color); - { - // Triangle top - let mut scrollbar_triangle_rect = TypedRect:: { - origin: TypedPoint2D::new( - bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, - bounds.origin.y + scrollbar_style.padding as f32), - size: TypedSize2D::new( - (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, - (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32), - }; + // Triangle top + let mut scrollbar_triangle_rect = TypedRect:: { + origin: TypedPoint2D::new( + bounds.origin.x + bounds.size.width - scrollbar_style.width as f32 + scrollbar_style.padding as f32, + bounds.origin.y + scrollbar_style.padding as f32), + size: TypedSize2D::new( + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32, + (scrollbar_style.width - (scrollbar_style.padding * 2)) as f32), + }; - scrollbar_triangle_rect.origin.x += scrollbar_triangle_rect.size.width / 4.0; - scrollbar_triangle_rect.origin.y += scrollbar_triangle_rect.size.height / 4.0; - scrollbar_triangle_rect.size.width /= 2.0; - scrollbar_triangle_rect.size.height /= 2.0; + scrollbar_triangle_rect.origin.x += scrollbar_triangle_rect.size.width / 4.0; + scrollbar_triangle_rect.origin.y += scrollbar_triangle_rect.size.height / 4.0; + scrollbar_triangle_rect.size.width /= 2.0; + scrollbar_triangle_rect.size.height /= 2.0; - push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointUp); + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointUp); - // Triangle bottom - scrollbar_triangle_rect.origin.y += bounds.size.height - scrollbar_style.width as f32 + scrollbar_style.padding as f32; - push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointDown); - } + // Triangle bottom + scrollbar_triangle_rect.origin.y += bounds.size.height - scrollbar_style.width as f32 + scrollbar_style.padding as f32; + push_triangle(&scrollbar_triangle_rect, builder, &scrollbar_style.triangle_color, TriangleDirection::PointDown); } enum TriangleDirection { @@ -783,10 +738,30 @@ fn push_triangle( // make all borders but one transparent let [b_left, b_right, b_top, b_bottom] = match direction { - PointUp => [(TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid) ], - PointDown => [(TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden)], - PointLeft => [(TRANSPARENT, BorderStyle::Hidden), (background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden)], - PointRight => [(background_color.0, BorderStyle::Solid), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden), (TRANSPARENT, BorderStyle::Hidden)], + PointUp => [ + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden), + (background_color.0, BorderStyle::Solid) + ], + PointDown => [ + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden), + (background_color.0, BorderStyle::Solid), + (TRANSPARENT, BorderStyle::Hidden) + ], + PointLeft => [ + (TRANSPARENT, BorderStyle::Hidden), + (background_color.0, BorderStyle::Solid), + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden) + ], + PointRight => [ + (background_color.0, BorderStyle::Solid), + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden), + (TRANSPARENT, BorderStyle::Hidden) + ], }; let border_details = BorderDetails::Normal(NormalBorder { @@ -1026,6 +1001,42 @@ fn push_font( } } +/// For a given rectangle, determines what text alignment should be used +fn determine_text_alignment<'a>(rect: &DisplayRectangle<'a>) +-> (TextAlignmentHorz, TextAlignmentVert) +{ + let mut horz_alignment = TextAlignmentHorz::default(); + let mut vert_alignment = TextAlignmentVert::default(); + + if let Some(align_items) = rect.layout.align_items { + // Vertical text alignment + use css_parser::LayoutAlignItems; + match align_items { + LayoutAlignItems::Start => vert_alignment = TextAlignmentVert::Top, + LayoutAlignItems::End => vert_alignment = TextAlignmentVert::Bottom, + // technically stretch = blocktext, but we don't have that yet + _ => vert_alignment = TextAlignmentVert::Center, + } + } + + if let Some(justify_content) = rect.layout.justify_content { + use css_parser::LayoutJustifyContent; + // Horizontal text alignment + match justify_content { + LayoutJustifyContent::Start => horz_alignment = TextAlignmentHorz::Left, + LayoutJustifyContent::End => horz_alignment = TextAlignmentHorz::Right, + _ => horz_alignment = TextAlignmentHorz::Center, + } + } + + if let Some(text_align) = rect.style.text_align { + // Horizontal text alignment with higher priority + horz_alignment = text_align; + } + + (horz_alignment, vert_alignment) +} + /// Populate the CSS style properties of the `DisplayRectangle` fn populate_css_properties(rect: &mut DisplayRectangle, css_overrides: &FastHashMap) { diff --git a/src/text_layout.rs b/src/text_layout.rs index a04badcf7..08b2105da 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -17,6 +17,11 @@ pub use webrender::api::GlyphInstance; pub const PX_TO_PT: f32 = 72.0 / 96.0; +/// When the text is regularly layouted, the text needs to be +/// spaced out a bit vertically +pub const DEFAULT_LINE_HEIGHT_MULTIPLIER: f32 = 1.5; +pub const DEFAULT_CHARACTER_WIDTH_MULTIPLIER: f32 = 1.1; + /// Words are a collection of glyph information, i.e. how much /// horizontal space each of the words in a text block and how much /// space each individual glyph take up. @@ -457,7 +462,8 @@ pub(crate) fn split_text_into_words<'a>(text: &str, font: &Font<'a>, font_size: let h_metrics = g.scaled(v_metrics_height_unscaled).h_metrics(); let mut horiz_advance = h_metrics.advance_width * glyph_metrics.scale_for_1_pixel - * (font_size.x * (96.0 / 72.0)); + * (font_size.x * (96.0 / 72.0) + * DEFAULT_CHARACTER_WIDTH_MULTIPLIER); // horiz_advance *= 96.0 / 72.0; @@ -743,7 +749,7 @@ fn words_to_left_aligned_glyphs<'a>( for glyph in &word.glyphs { let mut new_glyph = *glyph; let push_x = word_caret; - let push_y = (current_line_num as f32 * vertical_advance) + offset_top; + let push_y = (current_line_num as f32 * vertical_advance * DEFAULT_LINE_HEIGHT_MULTIPLIER) + offset_top; new_glyph.point.x += push_x; new_glyph.point.y += push_y; left_aligned_glyphs.push(new_glyph); From 411e3ba8ceee2e5ea596ab272dea9734947f5b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 21:07:17 +0200 Subject: [PATCH 258/868] Promoted daemons from a function pointer to a proper type (prepare for animation system) --- examples/async.rs | 2 +- examples/debug.css | 2 - src/app.rs | 2 +- src/app_state.rs | 12 ++- src/daemon.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- src/task.rs | 18 +---- 7 files changed, 192 insertions(+), 26 deletions(-) create mode 100644 src/daemon.rs diff --git a/examples/async.rs b/examples/async.rs index f9ed438e8..bd77e7d96 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -65,7 +65,7 @@ fn start_connection(app_state: &mut AppState, _event: WindowEvent) let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); app_state.data.modify(|state| state.connection_status = status); app_state.add_task(connect_to_db_async, &[]); - app_state.add_daemon(timer_daemon); + app_state.add_daemon(Daemon::unique(timer_daemon)); UpdateScreen::Redraw } diff --git a/examples/debug.css b/examples/debug.css index f80c175b3..538d7d36d 100644 --- a/examples/debug.css +++ b/examples/debug.css @@ -25,8 +25,6 @@ } #child-2 { - background-color: yellow; - max-height: 600px; } * { diff --git a/src/app.rs b/src/app.rs index 7264a048d..1ae51bc62 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,7 @@ use { traits::Layout, ui_state::UiState, ui_description::UiDescription, - app_state::Daemon, + daemon::Daemon, }; /// Graphical application that maintains some kind of application state diff --git a/src/app_state.rs b/src/app_state.rs index fd3c2879f..491740201 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -18,11 +18,9 @@ use { font::FontError, css_parser::{FontId, FontSize, PixelValue}, errors::ClipboardError, - task::TerminateDaemon, + daemon::{Daemon, DaemonId, TerminateDaemon}, }; -pub type Daemon = fn(&mut T) -> (UpdateScreen, TerminateDaemon); - /// Wrapper for your application data. In order to be layout-able, /// you need to satisfy the `Layout` trait (how the application /// should be laid out) @@ -41,7 +39,7 @@ pub struct AppState { /// Fonts and images that are currently loaded into the app pub resources: AppResources, /// Currently running daemons (polling functions) - pub(crate) daemons: FastHashMap (UpdateScreen, TerminateDaemon)>, + pub(crate) daemons: FastHashMap>, /// Currently running tasks (asynchronous functions running on a different thread) pub(crate) tasks: Vec>, } @@ -190,7 +188,7 @@ impl AppState { /// /// If the daemon was inserted, returns true, otherwise false pub fn add_daemon(&mut self, daemon: Daemon) -> bool { - match self.daemons.entry(daemon as usize) { + match self.daemons.entry(daemon.id) { Occupied(_) => false, Vacant(v) => { v.insert(daemon); true }, } @@ -205,8 +203,8 @@ impl AppState { let mut lock = self.data.lock().unwrap(); let mut daemons_to_terminate = vec![]; - for (key, daemon) in self.daemons.iter() { - let (should_update, should_terminate) = (daemon.clone())(&mut lock); + for (key, daemon) in self.daemons.iter_mut() { + let (should_update, should_terminate) = daemon.invoke_callback_with_data(&mut lock); if should_update == UpdateScreen::Redraw && should_update_screen == UpdateScreen::DontRedraw { diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 000000000..34f28188c --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,178 @@ +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + time::{Duration, Instant}, + fmt, + hash::{Hash, Hasher}, +}; +use dom::UpdateScreen; + +/// Should a daemon terminate or not - used to remove active daemons +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TerminateDaemon { + Terminate, + Continue, +} + +const MAX_DAEMON_ID: AtomicUsize = AtomicUsize::new(0); + +pub fn new_daemon_id() -> DaemonId { + DaemonId(MAX_DAEMON_ID.fetch_add(1, Ordering::SeqCst)) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DaemonId(usize); + +pub struct Daemon { + created: Instant, + last_run: Instant, + run_every: Option, + max_timeout: Option, + callback: DaemonCallback, + pub(crate) id: DaemonId, +} + +pub struct DaemonCallback(pub fn(&mut T) -> (UpdateScreen, TerminateDaemon)); + +// #[derive(Debug, Clone, PartialEq, Hash, Eq)] for DaemonCallback + +impl fmt::Debug for DaemonCallback { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DaemonCallback @ 0x{:x}", self.0 as usize) + } +} + +impl Clone for DaemonCallback { + fn clone(&self) -> Self { + DaemonCallback(self.0.clone()) + } +} + +impl Hash for DaemonCallback { + fn hash(&self, state: &mut H) where H: Hasher { + state.write_usize(self.0 as usize); + } +} + +impl PartialEq for DaemonCallback { + fn eq(&self, rhs: &Self) -> bool { + self.0 as usize == rhs.0 as usize + } +} + +impl Eq for DaemonCallback { } + +impl Copy for DaemonCallback { } + +impl Daemon { + pub fn unique(callback: DaemonCallback) -> Self { + Self::with_id(callback, new_daemon_id()) + } + + pub fn with_id(callback: DaemonCallback, id: DaemonId) -> Self { + Daemon { + created: Instant::now(), + last_run: Instant::now(), + run_every: None, + max_timeout: None, + callback, + id, + } + } + + pub fn with_timeout(self, timeout: Duration) -> Self { + Self { + max_timeout: Some(timeout), + .. self + } + } + + pub fn run_every(self, every: Duration) -> Self { + Self { + run_every: Some(every), + last_run: self.last_run - every, + .. self + } + } + + pub(crate) fn invoke_callback_with_data(&mut self, data: &mut T) -> (UpdateScreen, TerminateDaemon) { + + // Check if the deamons timeout is reached + if let Some(max_timeout) = self.max_timeout { + if Instant::now() - self.created > max_timeout { + return (UpdateScreen::DontRedraw, TerminateDaemon::Terminate); + } + } + + if let Some(run_every) = self.run_every { + if Instant::now() - self.last_run < run_every { + return (UpdateScreen::DontRedraw, TerminateDaemon::Continue); + } + } + + let res = (self.callback.0)(data); + + self.last_run = Instant::now(); + + res + } +} + +// #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] for Deamon + +impl fmt::Debug for Daemon { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Daemon {{ \ + created: {:?}, \ + run_every: {:?}, \ + last_run: {:?}, \ + max_timeout: {:?}, \ + callback: {:?}, \ + id: {:?}, \ + }}", + self.created, + self.run_every, + self.last_run, + self.max_timeout, + self.callback, + self.id) + } +} + +impl Clone for Daemon { + fn clone(&self) -> Self { + Daemon { + created: self.created, + run_every: self.run_every, + last_run: self.last_run, + max_timeout: self.max_timeout, + callback: self.callback, + id: self.id, + } + } +} + +impl Hash for Daemon { + fn hash(&self, state: &mut H) where H: Hasher { + self.created.hash(state); + self.run_every.hash(state); + self.last_run.hash(state); + self.max_timeout.hash(state); + self.callback.hash(state); + self.id.hash(state); + } +} + +impl PartialEq for Daemon { + fn eq(&self, rhs: &Self) -> bool { + self.created == rhs.created && + self.run_every == rhs.run_every && + self.last_run == rhs.last_run && + self.max_timeout == rhs.max_timeout && + self.callback == rhs.callback && + self.id == rhs.id + } +} + +impl Eq for Daemon { } + +impl Copy for Daemon { } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 55fb0fdbb..5978c22ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,8 @@ pub mod dom; pub mod dialogs; /// Async IO / task system pub mod task; +/// Daemon / animation system +pub mod daemon; /// Module for caching long texts (including their layout / character positions) across multiple frames pub mod text_cache; /// Text layout helper functions - useful for text layout outside of standard containers @@ -160,7 +162,7 @@ pub mod prelude { }; pub use rusttype::Font; pub use app_resources::AppResources; - pub use task::TerminateDaemon; + pub use daemon::{TerminateDaemon, DaemonId, DaemonCallback, Daemon}; #[cfg(feature = "logging")] pub use log::LevelFilter; diff --git a/src/task.rs b/src/task.rs index 1ff95270d..56cfc2b9b 100644 --- a/src/task.rs +++ b/src/task.rs @@ -4,19 +4,9 @@ use std::{ sync::{Arc, Mutex, Weak}, thread::{spawn, JoinHandle}, }; -use { - app_state::Daemon, - traits::Layout, -}; - -/// Should a daemon terminate or not - used to remove active daemons -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TerminateDaemon { - Terminate, - Continue, -} +use daemon::Daemon; -pub struct Task { +pub struct Task { // Task is in progress join_handle: Option>, dropcheck: Weak<()>, @@ -24,7 +14,7 @@ pub struct Task { pub(crate) after_completion_daemons: Vec> } -impl Task { +impl Task { pub fn new( app_state: &Arc>, callback: fn(Arc>, Arc<()>)) @@ -58,7 +48,7 @@ impl Task { } } -impl Drop for Task { +impl Drop for Task { fn drop(&mut self) { if let Some(thread_handle) = self.join_handle.take() { let _ = thread_handle.join().unwrap(); From 5ce8790ae2aaf9e5602563f6577d498f042ae19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Thu, 30 Aug 2018 22:38:29 +0200 Subject: [PATCH 259/868] Prepared scrolling caching system --- examples/async.rs | 2 +- src/app.rs | 7 +++-- src/app_state.rs | 2 +- src/display_list.rs | 4 +++ src/text_layout.rs | 10 +++++-- src/window.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 81 insertions(+), 9 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index bd77e7d96..c57174c63 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -65,7 +65,7 @@ fn start_connection(app_state: &mut AppState, _event: WindowEvent) let status = ConnectionStatus::InProgress(Instant::now(), Duration::from_secs(0)); app_state.data.modify(|state| state.connection_status = status); app_state.add_task(connect_to_db_async, &[]); - app_state.add_daemon(Daemon::unique(timer_daemon)); + app_state.add_daemon(Daemon::unique(DaemonCallback(timer_daemon))); UpdateScreen::Redraw } diff --git a/src/app.rs b/src/app.rs index 1ae51bc62..b2cdf88eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -255,6 +255,9 @@ impl App { force_redraw_cache[idx] = 2; } + // TODO: use this! + let should_redraw_animations = window.run_all_animations(); + // Update the window state that we got from the frame event (updates window dimensions and DPI) window.update_from_external_window_state(&mut frame_event_info); // Update the window state every frame that was set by the user @@ -305,10 +308,7 @@ impl App { self.windows.remove(closed_window_id); }); - // Run daemons and remove them from the even queue if they are finished let should_redraw_daemons = self.app_state.run_all_daemons(); - - // Clean up finished tasks, remove them if possible let should_redraw_tasks = self.app_state.clean_up_finished_tasks(); if [should_redraw_daemons, should_redraw_tasks].into_iter().any(|e| *e == UpdateScreen::Redraw) { @@ -322,7 +322,6 @@ impl App { thread::sleep(FRAME_TIME - diff); } } - } Ok(()) diff --git a/src/app_state.rs b/src/app_state.rs index 491740201..f23196fd0 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -201,7 +201,7 @@ impl AppState { { let mut should_update_screen = UpdateScreen::DontRedraw; let mut lock = self.data.lock().unwrap(); - let mut daemons_to_terminate = vec![]; + let mut daemons_to_terminate = Vec::new(); for (key, daemon) in self.daemons.iter_mut() { let (should_update, should_terminate) = daemon.invoke_callback_with_data(&mut lock); diff --git a/src/display_list.rs b/src/display_list.rs index cb5a7a6c8..b149cdb6f 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1154,6 +1154,7 @@ fn create_layout_constraints<'a, T: Layout>( if let Some(min_width) = rect.layout.min_width { layout_constraints.push(self_rect.width | GE(REQUIRED) | min_width.0.to_pixels()); } + if let Some(width) = rect.layout.width { layout_constraints.push(self_rect.width | EQ(STRONG) | width.0.to_pixels()); } else { @@ -1162,6 +1163,7 @@ fn create_layout_constraints<'a, T: Layout>( layout_constraints.push(self_rect.width | EQ(STRONG) | parent.width); } } + if let Some(max_width) = rect.layout.max_width { layout_constraints.push(self_rect.width | LE(REQUIRED) | max_width.0.to_pixels()); } @@ -1169,6 +1171,7 @@ fn create_layout_constraints<'a, T: Layout>( if let Some(min_height) = rect.layout.min_height { layout_constraints.push(self_rect.height | GE(REQUIRED) | min_height.0.to_pixels()); } + if let Some(height) = rect.layout.height { layout_constraints.push(self_rect.height | EQ(STRONG) | height.0.to_pixels()); } else { @@ -1177,6 +1180,7 @@ fn create_layout_constraints<'a, T: Layout>( layout_constraints.push(self_rect.height | EQ(STRONG) | parent.height); } } + if let Some(max_height) = rect.layout.max_height { layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } diff --git a/src/text_layout.rs b/src/text_layout.rs index 08b2105da..1775ae0ac 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -738,7 +738,11 @@ fn words_to_left_aligned_glyphs<'a>( Some(s) => WordCaretMax::SomeMaxWidth(s - word_caret), None => WordCaretMax::NoMaxWidth(word_caret), }; - line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + // TODO: This is monkey-patching. The following line crashed with an + // overflow, but I don't know the reason yet. + if left_aligned_glyphs.len() > 0 { + line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + } if word_caret > max_word_caret { max_word_caret = word_caret; } @@ -767,7 +771,9 @@ fn words_to_left_aligned_glyphs<'a>( Some(s) => WordCaretMax::SomeMaxWidth(s - word_caret), None => WordCaretMax::NoMaxWidth(word_caret), }; - line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + if left_aligned_glyphs.len() > 0 { + line_break_offsets.push((left_aligned_glyphs.len() - 1, space_until_horz_return)); + } if word_caret > max_word_caret { max_word_caret = word_caret; } diff --git a/src/window.rs b/src/window.rs index 786a77d43..1008fa506 100644 --- a/src/window.rs +++ b/src/window.rs @@ -20,7 +20,10 @@ use glium::{ }; use gleam::gl::{self, Gl}; use { - dom::{Texture, Callback}, + cache::DomHash, + FastHashMap, + dom::{Texture, Callback, UpdateScreen}, + daemon::{Daemon, DaemonId}, css::{Css, FakeCss}, window_state::{WindowState, MouseState, KeyboardState}, traits::Layout, @@ -466,12 +469,36 @@ pub struct Window { pub(crate) internal: WindowInternal, /// The solver for the UI, for caching the results of the computations pub(crate) ui_solver: UiSolver, + /// Currently running animations / transitions + pub(crate) animations: FastHashMap>, + /// States of scrolling animations, updated every frame + pub(crate) scroll_states: FastHashMap, // The background thread that is running for this window. // pub(crate) background_thread: Option>, /// The css (how the current window is styled) pub css: Css, } +#[derive(Debug, Copy, Clone)] +pub struct AnimationState { } + +#[derive(Debug, Copy, Clone)] +pub struct ScrollState { + /// Amount in pixel that the current node is scrolled + scroll_amount: f32, + /// Was the scroll amount used in this frame? + used_this_frame: bool, +} + +impl Default for ScrollState { + fn default() -> Self { + ScrollState { + scroll_amount: 0.0, + used_this_frame: true, + } + } +} + pub(crate) struct WindowInternal { pub(crate) last_display_list_builder: BuiltDisplayList, pub(crate) api: RenderApi, @@ -663,6 +690,8 @@ impl Window { renderer: Some(renderer), display: Rc::new(display), css: css, + animations: FastHashMap::default(), + scroll_states: FastHashMap::default(), internal: WindowInternal { api: api, epoch: epoch, @@ -764,6 +793,40 @@ impl Window { self.state.mouse_state.scroll_x = 0.0; self.state.mouse_state.scroll_y = 0.0; } + + /// NOTE: This has to be a getter, because we need to update + #[must_use] + pub(crate) fn get_scroll_amount(&mut self, dom_hash: &DomHash) -> Option { + let entry = self.scroll_states.get_mut(&dom_hash)?; + entry.used_this_frame = true; + Some(entry.scroll_amount) + } + + /// Note: currently scrolling is only done in the vertical direction + /// + /// Updating the scroll amound does not update the `entry.used_this_frame`, + /// since that is only relevant when we are actually querying the renderer. + pub(crate) fn scroll_node(&mut self, dom_hash: &DomHash, scroll_by: f32) { + if let Some(entry) = self.scroll_states.get_mut(dom_hash) { + entry.scroll_amount += scroll_by; + } + } + + pub(crate) fn create_new_scroll(&mut self, dom_hash: DomHash) { + self.scroll_states.insert(dom_hash, ScrollState::default()); + } + + /// Removes all scroll states that weren't used in the last frame + pub(crate) fn remove_unused_scroll_states(&mut self) { + self.scroll_states.retain(|_, state| state.used_this_frame); + } + + /// Runs all animations currently registered in this DOM + #[must_use] + pub(crate) fn run_all_animations(&mut self) -> UpdateScreen { + // TODO + UpdateScreen::DontRedraw + } } pub(crate) fn get_gl_context(display: &Display) -> Result, WindowCreateError> { From d55863692bfdf29ef40e044b9f17621c8eec4587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 31 Aug 2018 13:20:25 +0200 Subject: [PATCH 260/868] Improved text rendering & fixed 2x scaling bug in SVG text --- examples/hot_reload.css | 4 +-- examples/hot_reload.rs | 13 +------- src/text_layout.rs | 30 +++++++++--------- src/widgets/svg.rs | 67 +++++++++++++++++++++++------------------ 4 files changed, 55 insertions(+), 59 deletions(-) diff --git a/examples/hot_reload.css b/examples/hot_reload.css index 88b171626..5251b9961 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -8,9 +8,9 @@ color: white; font-size: 30px; font-family: sans-serif; - padding: 150px; width: 400px; - text-align: left; + text-align: center; + align-items: start; } #sub-wrapper { diff --git a/examples/hot_reload.rs b/examples/hot_reload.rs index 207948256..bf9a3bbb0 100644 --- a/examples/hot_reload.rs +++ b/examples/hot_reload.rs @@ -9,18 +9,7 @@ struct MyDataModel; impl Layout for MyDataModel { fn layout(&self, info: WindowInfo) -> Dom { Dom::new(NodeType::Div).with_id("wrapper") - .with_child(Dom::new(NodeType::Label(format!( - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, \ - sed diam nonumy eirmod tempor invidunt ut labore et dolore \ - magna aliquyam erat, sed diam voluptua. At vero eos et accusam \ - et justo duo dolores et ea rebum. Stet clita kasd gubergren, \ - no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem \ - ipsum dolor sit amet, consetetur sadipscing elitr, sed diam \ - nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam \ - erat, sed diam voluptua. At vero eos et accusam et justo duo \ - dolores et ea rebum. Stet clita kasd gubergren, no sea takimata \ - sanctus est Lorem ipsum dolor sit amet.") - )).with_id("red")) + .with_child(Dom::new(NodeType::Label(format!("Hello123"))).with_id("red")) .with_child(Dom::new(NodeType::Div).with_id("sub-wrapper") .with_child(Dom::new(NodeType::Div).with_id("yellow") .with_child(Dom::new(NodeType::Div).with_id("below-yellow"))) diff --git a/src/text_layout.rs b/src/text_layout.rs index 1775ae0ac..666da947f 100644 --- a/src/text_layout.rs +++ b/src/text_layout.rs @@ -145,14 +145,14 @@ pub struct FontMetrics { pub tab_width: f32, /// font_size * line_height pub vertical_advance: f32, - /// Offset of the font from the top of the bounding rectangle - pub offset_top: f32, /// Font size (for rusttype) in **pt** (not px) /// Used for vertical layouting (since it includes the line height) pub font_size_with_line_height: Scale, /// Same as `font_size_with_line_height` but without the line height incorporated. /// Used for horizontal layouting pub font_size_no_line_height: Scale, + /// Some fonts have a base height of 2048 or something weird like that + pub height_for_1px: f32, } /// ## Inputs @@ -275,18 +275,19 @@ fn calculate_font_metrics<'a>(font: &Font<'a>, font_size: &FontSize, line_height let font_size_with_line_height = Scale::uniform(font_size_f32 * line_height); let font_size_no_line_height = Scale::uniform(font_size_f32); - let space_width = font.glyph(' ').scaled(font_size_no_line_height).h_metrics().advance_width; + let space_glyph = font.glyph(' ').scaled(font_size_no_line_height); + let height_for_1px = font.glyph(' ').standalone().get_data().unwrap().scale_for_1_pixel; + let space_width = space_glyph.h_metrics().advance_width; let tab_width = 4.0 * space_width; // TODO: make this configurable let v_metrics_scaled = font.v_metrics(font_size_with_line_height); let v_advance_scaled = v_metrics_scaled.ascent - v_metrics_scaled.descent + v_metrics_scaled.line_gap; - let offset_top = v_advance_scaled / 2.0; FontMetrics { vertical_advance: v_advance_scaled, space_width, tab_width, - offset_top, + height_for_1px, font_size_with_line_height, font_size_no_line_height, } @@ -517,7 +518,7 @@ fn estimate_overflow_pass_1( let words = &words.items; - let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, .. } = *font_metrics; let max_text_line_len_horizontal = 0.0; @@ -571,9 +572,7 @@ fn estimate_overflow_pass_1( max_hor_len = Some(cur_line_cursor); - let cur_vertical = (cur_line as f32 * vertical_advance) + offset_top; - - cur_vertical + cur_line as f32 * vertical_advance } }; @@ -629,7 +628,7 @@ fn estimate_overflow_pass_2( pass1: TextOverflowPass1) -> (TypedSize2D, TextOverflowPass2) { - let FontMetrics { space_width, tab_width, vertical_advance, offset_top, .. } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, .. } = *font_metrics; let mut new_size = *rect_dimensions; @@ -700,10 +699,9 @@ fn words_to_left_aligned_glyphs<'a>( { let words = &words.items; - let FontMetrics { space_width, tab_width, vertical_advance, offset_top, font_size_no_line_height, .. } = *font_metrics; + let FontMetrics { space_width, tab_width, vertical_advance, font_size_no_line_height, .. } = *font_metrics; - // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs, - // left-aligned + // left_aligned_glyphs stores the X and Y coordinates of the positioned glyphs let mut left_aligned_glyphs = Vec::::new(); enum WordCaretMax { @@ -753,7 +751,7 @@ fn words_to_left_aligned_glyphs<'a>( for glyph in &word.glyphs { let mut new_glyph = *glyph; let push_x = word_caret; - let push_y = (current_line_num as f32 * vertical_advance * DEFAULT_LINE_HEIGHT_MULTIPLIER) + offset_top; + let push_y = (current_line_num + 1) as f32 * vertical_advance * DEFAULT_LINE_HEIGHT_MULTIPLIER; new_glyph.point.x += push_x; new_glyph.point.y += push_y; left_aligned_glyphs.push(new_glyph); @@ -796,7 +794,7 @@ fn words_to_left_aligned_glyphs<'a>( } let min_enclosing_width = max_word_caret; - let min_enclosing_height = (current_line_num as f32 * vertical_advance) + (font_size_no_line_height.y / PX_TO_PT) + (offset_top / PX_TO_PT); + let min_enclosing_height = (current_line_num as f32 * vertical_advance) + (font_size_no_line_height.y / PX_TO_PT); let line_break_offsets = line_break_offsets.into_iter().map(|(line, space_r)| { let space_r = match space_r { @@ -926,7 +924,7 @@ fn align_text_vert(alignment: TextAlignmentVert, glyphs: &mut [GlyphInstance], l let space_to_add = match overflow.vertical { IsOverflowing(_) => return, - InBounds(s) => s * multiply_factor, + InBounds(s) => (s - (30.0 * DEFAULT_LINE_HEIGHT_MULTIPLIER)) * multiply_factor, }; glyphs.iter_mut().for_each(|g| g.point.y += space_to_add); diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index c80d355a0..c8ea8818f 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -720,7 +720,7 @@ impl SampledBezierCurve { let mut glyph_rotations = vec![]; // NOTE: g.point.x is the offset from the start, not the advance! - let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); + let mut current_offset = start_offset + glyphs.get(0).and_then(|g| Some(g.point.x)).unwrap_or(0.0); let mut last_offset = start_offset; for glyph_idx in 0..glyphs.len() { @@ -733,7 +733,7 @@ impl SampledBezierCurve { glyph_rotations.push(rotation); last_offset = current_offset; - current_offset = start_offset + glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x * 2.0)).unwrap_or(0.0); + current_offset = start_offset + glyphs.get(glyph_idx + 1).and_then(|g| Some(g.point.x)).unwrap_or(0.0); } (glyph_offsets, glyph_rotations) @@ -828,9 +828,9 @@ pub fn join_vertex_buffers(input: &[VertexBuffers]) -> VerticesIndicesB VerticesIndicesBuffer { vertices: vertex_buf, indices: index_buf } } -pub fn scale_vertex_buffer(input: &mut [SvgVert], scale: &FontSize) { +pub fn scale_vertex_buffer(input: &mut [SvgVert], scale: &FontSize, height_for_1px: f32) { let real_size = scale.to_pixels(); - let scale_factor = real_size / 1024.0; + let scale_factor = real_size * height_for_1px; for vert in input { vert.xy.0 *= scale_factor; vert.xy.1 *= scale_factor; @@ -1925,16 +1925,15 @@ impl SvgTextLayout { /// Get the bounding box of a layouted text pub fn get_bbox(&self, placement: &SvgTextPlacement) -> SvgBbox { use self::SvgTextPlacement::*; + use text_layout::{DEFAULT_CHARACTER_WIDTH_MULTIPLIER, DEFAULT_LINE_HEIGHT_MULTIPLIER}; - let normal_x = 0.0; - let normal_y = -self.0.font_metrics.vertical_advance / PX_TO_PT; - let normal_width = self.0.min_width * 2.0; - let normal_height = self.0.min_height; + let normal_width = self.0.min_width / DEFAULT_CHARACTER_WIDTH_MULTIPLIER; + let normal_height = self.0.min_height / DEFAULT_LINE_HEIGHT_MULTIPLIER; SvgBbox(match placement { Unmodified => { TypedRect::new( - TypedPoint2D::new(normal_x, normal_y), + TypedPoint2D::new(0.0, 0.0), TypedSize2D::new(normal_width, normal_height) ) }, @@ -1948,10 +1947,10 @@ impl SvgTextLayout { let sin = rot_radians.sin(); let cos = rot_radians.cos(); - let top_left = (normal_x, normal_y); - let top_right = (normal_x + normal_width, normal_y); - let bottom_right = (normal_x + normal_width, normal_y + normal_height); - let bottom_left = (normal_x, normal_y + normal_height); + let top_left = (0.0, 0.0); + let top_right = (0.0 + normal_width, 0.0); + let bottom_right = (0.0 + normal_width, normal_height); + let bottom_left = (0.0, normal_height); let (top_left_x, top_left_y) = rotate_point(top_left, sin, cos); let (top_right_x, top_right_y) = rotate_point(top_right, sin, cos); @@ -1994,16 +1993,16 @@ impl SvgText { { let font = resources.get_font(&self.font_id).unwrap().0; let vectorized_font = vectorized_fonts_cache.get_font(&self.font_id, resources).unwrap(); - + let font_metrics = FontMetrics::new(&font, &self.font_size, None); match self.placement { SvgTextPlacement::Unmodified => { - normal_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size) + normal_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, &font_metrics) }, SvgTextPlacement::Rotated(degrees) => { - rotated_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, degrees) + rotated_text(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, &font_metrics, degrees) }, SvgTextPlacement::OnCubicBezierCurve(curve) => { - text_on_curve(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, &curve) + text_on_curve(&self.text_layout.0, &self.position, self.style, &font, &*vectorized_font, &self.font_size, &font_metrics, &curve) } } } @@ -2021,15 +2020,16 @@ fn normal_text( text_style: SvgStyle, font: &Font, vectorized_font: &VectorizedFont, - font_size: &FontSize) + font_size: &FontSize, + font_metrics: &FontMetrics) -> SvgLayerResource { let fill_vertices = text_style.fill.and_then(|_| { - Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, get_fill_vertices)) + Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, font_metrics, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, get_stroke_vertices)) + Some(normal_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, font_metrics, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -2057,14 +2057,17 @@ fn normal_text_to_vertices( glyph_ids: &[GlyphInstance], vectorized_font: &VectorizedFont, original_font: &Font, + font_metrics: &FontMetrics, transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { + use text_layout::{DEFAULT_LINE_HEIGHT_MULTIPLIER, DEFAULT_CHARACTER_WIDTH_MULTIPLIER}; + let mut vertex_buffers = transform_func(vectorized_font, original_font, glyph_ids); vertex_buffers.iter_mut().zip(glyph_ids).for_each(|(vertex_buf, gid)| { - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0 + position.x, gid.point.y + position.y); + scale_vertex_buffer(&mut vertex_buf.vertices, font_size, font_metrics.height_for_1px); + transform_vertex_buffer(&mut vertex_buf.vertices, (gid.point.x / DEFAULT_CHARACTER_WIDTH_MULTIPLIER) + position.x, (gid.point.y / DEFAULT_LINE_HEIGHT_MULTIPLIER) + position.y); }); join_vertex_buffers(&vertex_buffers) @@ -2077,15 +2080,16 @@ fn rotated_text( font: &Font, vectorized_font: &VectorizedFont, font_size: &FontSize, + font_metrics: &FontMetrics, rotation_degrees: f32) -> SvgLayerResource { let fill_vertices = text_style.fill.and_then(|_| { - Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, get_fill_vertices)) + Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, font_metrics, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, get_stroke_vertices)) + Some(rotated_text_to_vertices(&font_size, position, &layout.layouted_glyphs, vectorized_font, font, rotation_degrees, font_metrics, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -2102,17 +2106,20 @@ fn rotated_text_to_vertices( vectorized_font: &VectorizedFont, original_font: &Font, rotation_degrees: f32, + font_metrics: &FontMetrics, transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { + use text_layout::{DEFAULT_CHARACTER_WIDTH_MULTIPLIER, DEFAULT_LINE_HEIGHT_MULTIPLIER}; + let rotation_rad = rotation_degrees.to_radians(); let (char_sin, char_cos) = (rotation_rad.sin(), rotation_rad.cos()); let mut vertex_buffers = transform_func(vectorized_font, original_font, glyph_ids); vertex_buffers.iter_mut().zip(glyph_ids).for_each(|(vertex_buf, gid)| { - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); - transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x * 2.0, gid.point.y); + scale_vertex_buffer(&mut vertex_buf.vertices, font_size, font_metrics.height_for_1px); + transform_vertex_buffer(&mut vertex_buf.vertices, gid.point.x / DEFAULT_CHARACTER_WIDTH_MULTIPLIER, gid.point.y / DEFAULT_LINE_HEIGHT_MULTIPLIER); rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); transform_vertex_buffer(&mut vertex_buf.vertices, position.x, position.y); }); @@ -2127,17 +2134,18 @@ fn text_on_curve( font: &Font, vectorized_font: &VectorizedFont, font_size: &FontSize, + font_metrics: &FontMetrics, curve: &SampledBezierCurve) -> SvgLayerResource { let (char_offsets, char_rotations) = curve.get_text_offsets_and_rotations(&layout.layouted_glyphs, 0.0); let fill_vertices = text_style.fill.and_then(|_| { - Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, get_fill_vertices)) + Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, font_metrics, get_fill_vertices)) }); let stroke_vertices = text_style.stroke.and_then(|_| { - Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, get_stroke_vertices)) + Some(curved_vector_text_to_vertices(font_size, position, &layout.layouted_glyphs, vectorized_font, font, &char_offsets, &char_rotations, font_metrics, get_stroke_vertices)) }); SvgLayerResource::Direct { @@ -2156,6 +2164,7 @@ fn curved_vector_text_to_vertices( original_font: &Font, char_offsets: &[(f32, f32)], char_rotations: &[BezierCharacterRotation], + font_metrics: &FontMetrics, transform_func: fn(&VectorizedFont, &Font, &[GlyphInstance]) -> Vec> ) -> VerticesIndicesBuffer { @@ -2167,7 +2176,7 @@ fn curved_vector_text_to_vertices( .for_each(|((vertex_buf, char_rot), char_offset)| { let (char_offset_x, char_offset_y) = char_offset; // weird borrow issue // 2. Scale characters to the final size - scale_vertex_buffer(&mut vertex_buf.vertices, font_size); + scale_vertex_buffer(&mut vertex_buf.vertices, font_size, font_metrics.height_for_1px); // 3. Rotate individual characters inside of the word let (char_sin, char_cos) = (char_rot.0.sin(), char_rot.0.cos()); rotate_vertex_buffer(&mut vertex_buf.vertices, char_sin, char_cos); From c27a0d86098eeac2c5417d808bfef1fab63630e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 31 Aug 2018 14:35:00 +0200 Subject: [PATCH 261/868] Test fixing image distortion --- src/display_list.rs | 40 ++++++++++++++++++++++++++++++++++++++-- src/widgets/svg.rs | 2 +- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/display_list.rs b/src/display_list.rs index b149cdb6f..7c6cd23d6 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -917,13 +917,49 @@ fn push_image( match image_info { Uploaded(image_info) => { + + let mut image_bounds = *bounds; + + let image_key = image_info.key; + let image_size = image_info.descriptor.size; + + // For now, adjust the width and height based on the + if image_size.width < bounds.size.width as u32 && image_size.height < bounds.size.height as u32 { + image_bounds.size.width = image_size.width as f32; + image_bounds.size.height = image_size.height as f32; + } else { + let scale_factor_w = image_size.width as f32 / bounds.size.width; + let scale_factor_h = image_size.height as f32 / bounds.size.height; + + if image_size.width < bounds.size.width as u32 { + // if the image fits horizontally + image_bounds.size.width = image_size.width as f32; + image_bounds.size.height = image_size.height as f32 * scale_factor_w; + } else if image_size.height < bounds.size.height as u32 { + // if the image fits vertically + image_bounds.size.width = image_size.width as f32 * scale_factor_h; + image_bounds.size.height = image_size.height as f32; + } else { + // image fits neither horizontally nor vertically + let scale_factor_smaller = scale_factor_w.max(scale_factor_w); + let new_width = image_size.width as f32 * scale_factor_smaller; + let new_height = image_size.height as f32 * scale_factor_smaller; + image_bounds.size.width = new_width; + image_bounds.size.height = new_height; + } + } + + // Just for testing + image_bounds.size.width /= 2.0; + image_bounds.size.height /= 2.0; + builder.push_image( &info, - bounds.size, + image_bounds.size, LayoutSize::zero(), ImageRendering::Auto, AlphaType::PremultipliedAlpha, - image_info.key, + image_key, ColorF::WHITE); }, _ => { }, diff --git a/src/widgets/svg.rs b/src/widgets/svg.rs index c8ea8818f..f3dbf5b34 100644 --- a/src/widgets/svg.rs +++ b/src/widgets/svg.rs @@ -37,7 +37,7 @@ use { window::ReadOnlyWindow, css_parser::{FontId, FontSize}, app_resources::AppResources, - text_layout::{FontMetrics, LayoutTextResult, layout_text, PX_TO_PT}, + text_layout::{FontMetrics, LayoutTextResult, layout_text}, }; pub use lyon::tessellation::VertexBuffers; From b159f5116b2b4b8a0805b0810cb0f8fa3b3c7b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Fri, 31 Aug 2018 19:41:21 +0200 Subject: [PATCH 262/868] Initial position: relative / position:absolute --- examples/async.rs | 2 + examples/hot_reload.css | 17 +++-- examples/hot_reload.rs | 2 + src/css_parser.rs | 6 ++ src/display_list.rs | 157 ++++++++++++++++++++++++++-------------- src/dom.rs | 14 +++- src/id_tree.rs | 6 ++ src/ui_state.rs | 8 +- src/window.rs | 2 +- 9 files changed, 148 insertions(+), 66 deletions(-) diff --git a/examples/async.rs b/examples/async.rs index c57174c63..f77cc2b18 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + extern crate azul; use azul::{ diff --git a/examples/hot_reload.css b/examples/hot_reload.css index 5251b9961..02ec87fa9 100644 --- a/examples/hot_reload.css +++ b/examples/hot_reload.css @@ -1,23 +1,26 @@ #wrapper { background: linear-gradient(15deg, red 0%, blue 100%); flex-direction: row; + position: absolute; + top: -50px; + left: -50px; } #red { background-color: #BF0C2B; color: white; + width: 400px; font-size: 30px; font-family: sans-serif; - width: 400px; - text-align: center; - align-items: start; } #sub-wrapper { flex-direction: column-reverse; width: 400px; box-shadow: 0px 0px 50px black; - position: absolute; + position: relative; + top: 40px; + left: 20px; } #yellow { @@ -25,17 +28,19 @@ height: 70px; flex-direction: row-reverse; box-shadow: 0px 0px 50px black; + position: absolute; + left: 50px; + top: -34px; } #grey { - background-color: transparent; + background-color: #44ccee33; } #below-yellow { background-color: #F14C13; box-shadow: 2px 2px 10px black; width: 50px; - position: relative; } #cat { diff --git a/examples/hot_reload.rs b/examples/hot_reload.rs index bf9a3bbb0..f5b667a52 100644 --- a/examples/hot_reload.rs +++ b/examples/hot_reload.rs @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + extern crate azul; use azul::prelude::*; diff --git a/src/css_parser.rs b/src/css_parser.rs index 2403812ce..7dd81b4da 100644 --- a/src/css_parser.rs +++ b/src/css_parser.rs @@ -1696,6 +1696,12 @@ pub enum LayoutPosition { Absolute, } +impl Default for LayoutPosition { + fn default() -> Self { + LayoutPosition::Static + } +} + impl Default for LayoutDirection { fn default() -> Self { LayoutDirection::Column diff --git a/src/display_list.rs b/src/display_list.rs index 7c6cd23d6..16cb25ec3 100644 --- a/src/display_list.rs +++ b/src/display_list.rs @@ -1180,98 +1180,138 @@ fn create_layout_constraints<'a, T: Layout>( let mut layout_constraints = Vec::new(); - if let Some(LayoutPosition::Relative) = rect.layout.position { - println!("nearest common absolute ancestor for node {:?} is: {:?}", node_id, get_nearest_relative_ancestor(node_id, display_rectangles)); - } + let window_constraints = ui_solver.get_window_constraints(); // Insert the max height and width constraints // - // min-width and max-width are stronger than width because the width has to be between min and max width + // min-width and max-width are stronger than width because + // the width has to be between min and max width + + // min-width, width, max-width if let Some(min_width) = rect.layout.min_width { layout_constraints.push(self_rect.width | GE(REQUIRED) | min_width.0.to_pixels()); } - if let Some(width) = rect.layout.width { layout_constraints.push(self_rect.width | EQ(STRONG) | width.0.to_pixels()); } else { if let Some(parent) = dom_node.parent { - let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.width | EQ(STRONG) | parent.width); + let parent_rect = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(self_rect.width | EQ(STRONG) | parent_rect.width); + } else { + layout_constraints.push(self_rect.width | EQ(REQUIRED) | window_constraints.width_var); } } - if let Some(max_width) = rect.layout.max_width { layout_constraints.push(self_rect.width | LE(REQUIRED) | max_width.0.to_pixels()); } + // min-height, height, max-height if let Some(min_height) = rect.layout.min_height { layout_constraints.push(self_rect.height | GE(REQUIRED) | min_height.0.to_pixels()); } - if let Some(height) = rect.layout.height { layout_constraints.push(self_rect.height | EQ(STRONG) | height.0.to_pixels()); } else { if let Some(parent) = dom_node.parent { - let parent = ui_solver.get_rect_constraints(parent).unwrap(); - layout_constraints.push(self_rect.height | EQ(STRONG) | parent.height); + let parent_rect = ui_solver.get_rect_constraints(parent).unwrap(); + layout_constraints.push(self_rect.height | EQ(STRONG) | parent_rect.height); + } else { + layout_constraints.push(self_rect.height | EQ(REQUIRED) | window_constraints.height_var); } } - if let Some(max_height) = rect.layout.max_height { layout_constraints.push(self_rect.height | LE(REQUIRED) | max_height.0.to_pixels()); } - + // root node: start at (0, 0) if dom_node.parent.is_none() { - // Root node: fill window width / height - let window_constraints = ui_solver.get_window_constraints(); layout_constraints.push(self_rect.top | EQ(REQUIRED) | 0.0); layout_constraints.push(self_rect.left | EQ(REQUIRED) | 0.0); - layout_constraints.push(self_rect.width | EQ(REQUIRED) | window_constraints.width_var); - layout_constraints.push(self_rect.height | EQ(REQUIRED) | window_constraints.height_var); } - let direction = rect.layout.direction.unwrap_or_default(); + // Node has children: Push the constraints for `flex-direction` + if dom_node.first_child.is_some() { - let mut next_child_id = dom_node.first_child; - let mut previous_child: Option = None; + let direction = rect.layout.direction.unwrap_or_default(); - while let Some(child_id) = next_child_id { - let child = ui_solver.get_rect_constraints(child_id).unwrap(); + let mut next_child_id = dom_node.first_child; + let mut previous_child: Option = None; - match direction { - LayoutDirection::Row => { - layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); - match previous_child { - None => layout_constraints.push(child.left | EQ(STRONG) | self_rect.left), - Some(prev) => layout_constraints.push(child.left | EQ(STRONG) | (prev.left + prev.width)), - } - }, - LayoutDirection::RowReverse => { - layout_constraints.push(child.top | EQ(STRONG) | self_rect.top); - match previous_child { - None => layout_constraints.push(child.left | EQ(STRONG) | (self_rect.left + (self_rect.width - child.width))), - Some(prev) => layout_constraints.push((child.left + child.width) | EQ(STRONG) | prev.left), - } - }, - LayoutDirection::Column => { - match previous_child { - None => layout_constraints.push(child.top | EQ(STRONG) | self_rect.top), - Some(prev) => layout_constraints.push(child.top | EQ(STRONG) | (prev.top + prev.height)), - } - layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); - }, - LayoutDirection::ColumnReverse => { - layout_constraints.push(child.left | EQ(STRONG) | self_rect.left); - match previous_child { - None => layout_constraints.push(child.top | EQ(STRONG) | (self_rect.top + (self_rect.height - child.height))), - Some(prev) => layout_constraints.push((child.top + child.height) | EQ(STRONG) | prev.top), - } - }, + // Iterate through children + while let Some(child_id) = next_child_id { + + let child = &display_rectangles[child_id].data; + let child_rect = ui_solver.get_rect_constraints(child_id).unwrap(); + + let should_respect_relative_positioning = child.layout.position == Some(LayoutPosition::Relative); + + let (relative_top, relative_left, relative_right, relative_bottom) = if should_respect_relative_positioning {( + child.layout.top.and_then(|top| Some(top.0.to_pixels())).unwrap_or(0.0), + child.layout.left.and_then(|left| Some(left.0.to_pixels())).unwrap_or(0.0), + child.layout.right.and_then(|right| Some(right.0.to_pixels())).unwrap_or(0.0), + child.layout.right.and_then(|bottom| Some(bottom.0.to_pixels())).unwrap_or(0.0), + )} else { + (0.0, 0.0, 0.0, 0.0) + }; + + match direction { + LayoutDirection::Row => { + match previous_child { + None => layout_constraints.push(child_rect.left | EQ(MEDIUM) | self_rect.left + relative_left), + Some(prev) => layout_constraints.push(child_rect.left | EQ(MEDIUM) | (prev.left + prev.width) + relative_left), + } + layout_constraints.push(child_rect.top | EQ(MEDIUM) | self_rect.top); + }, + LayoutDirection::RowReverse => { + match previous_child { + None => layout_constraints.push(child_rect.left | EQ(MEDIUM) | (self_rect.left + relative_left + (self_rect.width - child_rect.width))), + Some(prev) => layout_constraints.push((child_rect.left + child_rect.width) | EQ(MEDIUM) | prev.left + relative_left), + } + layout_constraints.push(child_rect.top | EQ(MEDIUM) | self_rect.top); + }, + LayoutDirection::Column => { + match previous_child { + None => layout_constraints.push(child_rect.top | EQ(MEDIUM) | self_rect.top), + Some(prev) => layout_constraints.push(child_rect.top | EQ(MEDIUM) | (prev.top + prev.height)), + } + layout_constraints.push(child_rect.left | EQ(MEDIUM) | self_rect.left + relative_left); + }, + LayoutDirection::ColumnReverse => { + match previous_child { + None => layout_constraints.push(child_rect.top | EQ(MEDIUM) | (self_rect.top + (self_rect.height - child_rect.height))), + Some(prev) => layout_constraints.push((child_rect.top + child_rect.height) | EQ(MEDIUM) | prev.top), + } + layout_constraints.push(child_rect.left | EQ(MEDIUM) | self_rect.left + relative_left); + }, + } + + previous_child = Some(child_rect); + next_child_id = dom[child_id].next_sibling; } + } - previous_child = Some(child); - next_child_id = dom[child_id].next_sibling; + // Handle position: absolute + if let Some(LayoutPosition::Absolute) = rect.layout.position { + + let top = rect.layout.top.and_then(|top| Some(top.0.to_pixels())).unwrap_or(0.0); + let left = rect.layout.left.and_then(|left| Some(left.0.to_pixels())).unwrap_or(0.0); + let right = rect.layout.right.and_then(|right| Some(right.0.to_pixels())).unwrap_or(0.0); + let bottom = rect.layout.right.and_then(|bottom| Some(bottom.0.to_pixels())).unwrap_or(0.0); + + match get_nearest_positioned_ancestor(node_id, display_rectangles) { + None => { + // window is the nearest positioned ancestor + // TODO: hacky magic that relies on having one root element + let window_id = ui_solver.get_rect_constraints(NodeId::new(0)).unwrap(); + layout_constraints.push(self_rect.top | EQ(REQUIRED) | window_id.top + top); + layout_constraints.push(self_rect.left | EQ(REQUIRED) | window_id.left + left); + }, + Some(nearest_positioned) => { + let nearest_positioned = ui_solver.get_rect_constraints(nearest_positioned).unwrap(); + layout_constraints.push(self_rect.top | GE(STRONG) | nearest_positioned.top + top); + layout_constraints.push(self_rect.left | GE(STRONG) | nearest_positioned.left + left); + } + } } layout_constraints @@ -1301,15 +1341,20 @@ fn subtract_padding(bounds: &TypedRect, padding: &LayoutPaddin /// Returns the nearest common ancestor with a `position: relative` attribute /// or `None` if there is no ancestor that has `position: relative`. Usually /// used in conjunction with `position: absolute` -fn get_nearest_relative_ancestor<'a>(start_node_id: NodeId, arena: &Arena>) +fn get_nearest_positioned_ancestor<'a>(start_node_id: NodeId, arena: &Arena>) -> Option { let mut current_node = start_node_id; while let Some(parent) = arena[current_node].parent() { - if let Some(LayoutPosition::Absolute) = arena[parent].data.layout.position { + // An element with position: absolute; is positioned relative to the nearest + // positioned ancestor (instead of positioned relative to the viewport, like fixed). + // + // A "positioned" element is one whose position is anything except static. + if let Some(LayoutPosition::Static) = arena[parent].data.layout.position { + current_node = parent; + } else { return Some(parent); } - current_node = parent; } None } diff --git a/src/dom.rs b/src/dom.rs index f9c04d027..8a9e7b29f 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -537,7 +537,19 @@ impl Dom { /// Creates an empty DOM #[inline] pub fn new(node_type: NodeType) -> Self { - let mut arena = Arena::new(); + Self::with_capacity(node_type, 0) + } + + /// Returns the number of nodes in this DOM + #[inline] + pub fn len(&self) -> usize { + self.arena.borrow().nodes_len() + } + + /// Creates an empty DOM with space reserved for `cap` nodes + #[inline] + pub fn with_capacity(node_type: NodeType, cap: usize) -> Self { + let mut arena = Arena::with_capacity(cap.saturating_add(1)); let root = arena.new_node(NodeData::new(node_type)); Self { arena: Rc::new(RefCell::new(arena)), diff --git a/src/id_tree.rs b/src/id_tree.rs index ed9a5fe95..39d05e770 100644 --- a/src/id_tree.rs +++ b/src/id_tree.rs @@ -164,6 +164,12 @@ impl Arena { } } + pub fn with_capacity(cap: usize) -> Arena { + Arena { + nodes: Vec::with_capacity(cap), + } + } + /// Return an iterator over the indices in the internal arenas Vec pub fn linear_iter(&self) -> LinearIterator { LinearIterator { diff --git a/src/ui_state.rs b/src/ui_state.rs index 57c08a05c..b1fc010ef 100644 --- a/src/ui_state.rs +++ b/src/ui_state.rs @@ -54,15 +54,19 @@ impl UiState { } }; + // Tree should have a single root element + let mut parent_dom = Dom::with_capacity(NodeType::Div, dom.len()); + parent_dom.add_child(dom); + NODE_ID.swap(0, Ordering::SeqCst); CALLBACK_ID.swap(0, Ordering::SeqCst); let mut callback_list = BTreeMap::>::new(); let mut node_ids_to_callbacks_list = BTreeMap::>::new(); - dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); + parent_dom.collect_callbacks(&mut callback_list, &mut node_ids_to_callbacks_list); UiState { - dom: dom, + dom: parent_dom, callback_list: callback_list, node_ids_to_callbacks_list: node_ids_to_callbacks_list, } diff --git a/src/window.rs b/src/window.rs index 1008fa506..34141293c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -630,7 +630,7 @@ impl Window { enable_aa: true, clear_color: clear_color, enable_render_on_scroll: true, - enable_scrollbars: true, + enable_scrollbars: false, cached_programs: Some(ProgramCache::new(None)), renderer_kind: if native { RendererKind::Native From 3a729e2d593510dc43227841037e905b3f272f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 1 Sep 2018 12:16:15 +0200 Subject: [PATCH 263/868] Updated callback model diagram --- doc/azul_callback_model.png | Bin 12189 -> 0 bytes doc/azul_callback_model_new.png | Bin 0 -> 46730 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/azul_callback_model.png create mode 100644 doc/azul_callback_model_new.png diff --git a/doc/azul_callback_model.png b/doc/azul_callback_model.png deleted file mode 100644 index 08f927b7454bdb61ea3b9ecc7708689b51f455a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12189 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 bdsoczIdxa#&&YtXdpxk3o@$ly^LPIXb>j~H diff --git a/doc/azul_callback_model_new.png b/doc/azul_callback_model_new.png new file mode 100644 index 0000000000000000000000000000000000000000..53df6aac1255a11490b92e289e1a66d26e152ef0 GIT binary patch literal 46730 zcmeFYWmH^k(=JFt0trEaJ0y(<3-0g`8n@u?u8q4S5Zv7%!QEX$pmDbb8rR_Nb~exV zesgBln)BoQn6+lsnp!~j-fX#dRozu}UAyj(uX5sOC`2d-2ncAB5@1CH1jK3tgqNsq zUIKf>RgkfO4@Ac=lFD!1yn(OEuK=5bPNM2gN)S^gR|5wV1T$NRjR~Wpk%Nhet)n@_ z=?Jky5CP!>f+Sd2*)4s4(brDtDuekx3Om(N!Zw<7WT04)-l3V5wQkUUJeS63w5#ZW zopviSCgsaU%N{rrnJhlQyHuO?=kGG{JqG2^*Q%M$1#7WprBkD2I1^Q`ud;Cu=2cww zh2f#<-L{~8MP>K21a^y0F|rPFXa3t5H$nG^6o^$UJ{|dNb$L0Cg4=&zbzDqud&vt^ zV$C($Q@jA40HM_IiqZAoZwP}JRRqtw5dJS8-`)}0!jjQT#F={>R*D&sM_engPo#qf1II(oF#v$sES zzz2@9gJ`-}T?;*~=RmyHiJhHC8U8)2JLGay@Xora1h847*{_EG2d$?+eVnrKdQx4r zeEbBdW|&kecu;4a6swl@oq8Pek!ZwH-tyTGY5jNH;>P`)1db@_RJ2&48T@8wj5lQB z*4o0~dx)?oICX@-k8mDkhAA|p!1^>)nHmkHRp(ZrQUq>vrB$==8Uf+=gh9ODoOPo7!=as6 z8(z*$Za*zXAjyq!K9~{?=$q}#S{E#zXr zG*V?#HFNMhAl```Jd~B;r|$7WkNc@KF!xfmeCn;K+2=duc^mzXc`x1A5%y9>eI9Z4 zJM%sh7CR!I!Rk`&F5*XQ41I@dL!LdkR~ghkr_QQ+>;hITAwMVio5+cl%v|`Z+u51_ z27_LZpZJ+xo;VQCc9`>1j33GS6#V?QnK7P*|C8=(Dq_{Qu0D%uNV&d?sKXN6sawGP zE1(A`uGFNL+0p)$;jWi~MpIy#;86(e?h1-}4j0}4Sv8>ZRoM}Jk6ZUl#;=-wXRU~p zu^;dd5lUUs=RSQ-`N%7r_J1aeXJT5D8nSfaXj_km(=> zGpeW;9vY@-hFI6TrB`N-$S+@H4N^uqu@fPehJNDQn*^fkw-|*F6sLTO>wB@|LUNH-x+&iaQb5VkOi=4fgB8N4-s_ z9=E98b{V1eGF{StS0xt2<@Rx<-u|sxj(%(2h)UB6D%21^o!Y(6;FFT&tC~stCI#L^ zVwu3~qMQ^eq}K1bL;9>kM4x1l%lwYN{DZT<^v~zN&ToLxeC7s$s#+EFV`KhtazOj_ z4_H?@{>^I|a><~VA14T3qhVG_ZLUWRCY+(Iy&IXm*uzYV#MKVjGwjaK*@u6CrkiSY zub_%yR|UhmZ*|#|7u-on5>4nx?dKOtp_|THJfP5~h%f&1#VmY?o$p>Wh+
Q6zi6 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 ao^j<&jlm42n0NqZuo?7mna`zp?f(z4?aQJ7 literal 0 HcmV?d00001 diff --git a/assets/images/cat_image.jpg b/assets/images/cat_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e32f906273344486bf06d76fd26b762879c52f59 GIT binary patch literal 552998 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 z z7;QIfz`@w86$=JqeUEYG^ef&1v1}q~@nXjBEuWCf;9<~dY;dDgFpTz0hSt9{HYPEI z?buoNe%$>Xdw>dBd8tqQVfNtt66=jv2tm4ag8Oriac7DwLVcxNva9K19e57!8zVZP z`QNdbyx$wKkIG9GcJgP&tAEV*2lyz_&R66a*f4|(nh9BW-Y_FxHPjQ`-o4;hngfPR6p3x6gAe19Z)3he7DOjejTmN;IAdt zf|`^Q3EmfPvNU54kfa1!fB2YeNUK(d$Z6kDLBV6LkDmCy{%Nb_4kh(U>m^~HY-z5N zc`I(uS$4YVI!}L$iMa`@a(T}G5AgHn!e+r4H|L@nVPI*^8->A{hTv`i-#Cba%fw{G zpMV*+^t?mP-n0`c`AWfZ`}%K|t^q5->N90|*uYB@#TUrxO61UEO(MqklKBYp-{YWz zfP%r|;8$n&zc@C&nA1x<0+dIExaDtRB;ekVsE{{}B5wxx$Yd`c3#!(d}OVZrdCNqDCPbu=l zC)I$?OC8DStM}ev=P?GWT#pG=ZfCd$(qm!dMdi}D_z`*1j-yo)nB|8gOj3G2F7<;W zvsIopN{9!>7fMq2-e(F&;+Wp#!3_bUB6ax--SX9LjV;?qJs17R;q zJ~#G2Qckdn1S%Zi)Y^n&8Ma_8N)rf{WVfn=BiD?EvyW zy?^^y_BA!_O@`R-n?|jPpc*y&rPO=>0n8NYOS`6>_vr=LJp}|}wq^S>FOn2|pX`}u zH>mV{n8$LR%30|7<87#iot#$q<2SK1A?_D06&uGg$zpaj0THL28aq}s6GIq3Q-6z1 zxpq-pj<*{5$mZ@fWBzhC^(EqZ={e?;`&VVs=fSGKcN2jH*9gax3aYdc=VTX2*3hAs zDvK~VK~^93`zu24cI=p-Ck2y{pLY!+rmMLc;bJxT6GPin%8>{~zqB2GgqJ*TQdK33Y+23= z3F^QqceRgp@SG-XC2$~j%C`oO?RdAsboL?id68EGGkYc?W|*3bH)>Ho(a;$zu>aa$ zLeSkWgM2PsmmuKCfcN3stbc$VYs%o#R#p^~eV)f3Xoy6`OU>)H_vo-&V(bIM8rRvY zks?-to%Z58*=H#B+_Os?xSu$`v>HFG}}o&5{*rOD5{dEj^ZUBnq{n?R@&0U>)A z2Je^Jv-^aPa(;>qbP0eJYM$?#R0y;N^uUfD37j*l=&#D=c{|Q|NBzns>MUiOF)6k9 ztZ4tU`t1SB5}xx?ebp8&CDmNk=RDTlZ(%O>)>q$hkq007*7!wD+<=HKF(hg}sd=(i zY@=n;IHimREBFuKD7d_TY>}=(&IRXV$q1|Z$n_}W%UIOPBDlD_?x3x*a{H^d+n*@* z`juwAB_)=!FNH)gbDptHH?b*?pDqeKs(|(1`wP5^b0{a-@?Xj?m7oDT1CWhVPhJ{4sUAvydd0TsG zE`SIc<%wC3?zUZYP3*_Q{N(JwXRZwhUW-oZCLpBTZVaDqDq6H+`CDV3|3X#xs^jXn ze}M8GUE_~`3=i3G7Gl?d9{L&kS+W@_4>b=qNN9hFgjL)VkS=#QZ)Hhimo%|)-yeUG zk$7xra)lAi{%k4k?(qTVtP5ZyEIXj4k|NcVqrK7C5!Y78dE3^JRR=A;*HAe>y(41= zEZ+}bvkg87w|dp^xPAJ;MEwQr8gw|X>`%tK4g-_y;=JC!NB!FYScURkz~>N5dql>4!=M8M-+fg7vAuM&4)NZLa7$mU#+hqbQ8-0AE@WE*iPs`TYUC|%fG(2Nc?Z4%h<3CTVVvIoUz@m56bgpd%%ca}+mRzgn zP3zm#jhUb21QVw*(pt9XnGNOCOGID96}-JRmU(U7I`bpALtCmM|Mm+!t?Vig(cEmS z_tBc~M8ALOAE18LCk%RNqJH1yvv8>&3)%iuX0unpvIjE7)>?>MWa{9Z;19g_0g!!a zMfiD1*$Wx-$wy_jnp(WFHu_Ny6GO5aPG9LZ?%A<_s>e?_zYy&_D{b*vQxB8Ylr5_< zzDhd9;Hs;LZ_cV*+~8R%O5jx9K98;9srqCRj_>cSsf^2SMog(a(rb7%*?FiUW)e2z z&xeouD`O592*p20LqNHG=eVH;Rw<;#zRixtKFI?^Oz@287lw_p(#W* z10EE}`VC5X8(YDt)4gnZc;y?4&oGfh%~ zHnHi)IgO+s{jB~Z${OQZf4b%tT4-|3<7sjK&zJ07fDYzt)QsTL=M<3w-~Mv>Q>RyT zTxB|}oE-F!6S-;g?7t6(Me>ob^^=m*4vd6KI+%oDHr4;ZV4kKP0U;ks0W-*>rK8X1 z34<-xOvIH?U3HLfl8%X+8}-HfBs3+07L<)<&o83cp9MA`6gK=yo`6+wF2cFn9B-Fp zsJ54538WMnWUL6G+%%yD=0@e8u>*2E-QHpY7DcYca#Ia-^~yh}6En-)T;G%`du^zz zZV6ld+I@~CNam4E4w^zf|nF^VQEAFxs3!fxSYyBSp zzCc00TAE$KG6rYAK9$W`*2yI)IGK>%=K z1#B+TwMaY?GZHGIjag*~LfqT@MmWV6ZKGXsoK6DiBOOv=ZsKUcS6e&YRhspQN9$Q53gz|jX@;OGJPtTFZ|KsBxVMA^rkkq z*hw+T?*=K}OVc@Gt(QhM1(*R~{K=1MV9n?s!b$pL)~R-v%USZMZN&Q1YkR`51kMIN zRYfMWMp2^9E~G~32Z*habW*M5BuA|xMae!{fITRYa8v;b5OK#9RU^20Lo;NH`pP>y&`jwlLoZaz~is%6+Rvl!Yb? zlCRRVZE|pd>T^=8ZfPY#05E-NiMz8^H3X!lGn{(XrGlfn2%N%=8WLPeX9Qr!t!!Eb zBiuzBeE~Wase+YxiJ6>#dWCl9Y$qA!IrPOcagf;?ktCRjs7qvmKmeWu*ETw?*rDrU zm1M|PILZG2Kdl7oB4Cwz7^kaX6cDJMd%&PB8X1rTdW!DORbqnNsD$uH%t`+MF;Ef# zjpXOjm^jdd6%spv+JbQa#@LaZccDdBR0`lJg?b)lp@F<4lOCh`)aybAl}DdX)`nWj zRwEPWYIFOAZ5t(JP=t~~Pc@C(Qvy|51&_KCahRyL?py@}86;+=pdN$ zwh{}ZK|Il+#IS(3l$>X2;*shat2WA0t~ib987DDSEkmp>uP#Vv$}(U{C*Eo=Du`A$ zEgb^?08wFbw7UoqndjQ5T}`bv