From ed591e0d3cc7c5f2694f6f7651f3f043107fb664 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 11:16:46 +0200 Subject: [PATCH 01/18] redo image size calculations --- druid/src/widget/image.rs | 70 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 59615a31d8..6a18db0bd8 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -145,10 +145,21 @@ impl Widget for Image { ) -> Size { bc.debug_check("Image"); - if bc.is_width_bounded() { - bc.max() - } else { - bc.constrain(self.image_data.get_size()) + // If either the width or height is constrained calculate a value so that the image fits + // in the size exactly. If it is unconstrained by both width and height take the size of + // the image. + if !bc.is_height_bounded() && bc.is_width_bounded() { + let mut size = bc.max(); + let ratio = size.width / self.image_data.x_pixels as f64; + size.height = ratio * self.image_data.y_pixels as f64; + size + } else if !bc.is_width_bounded() && bc.is_height_bounded(){ + let mut size = bc.max(); + let ratio = size.height / self.image_data.y_pixels as f64; + size.width = ratio * self.image_data.x_pixels as f64; + size + } else { + bc.constrain(self.image_data.get_size()) } } @@ -437,5 +448,56 @@ mod tests { target.into_png(tmp_dir.join("image.png")).unwrap(); }, ); + } + + #[test] + fn height_bound_paint() { + use crate::{tests::harness::Harness, WidgetId}; + let _id_1 = WidgetId::next(); + let image_data = ImageData { + pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + x_pixels: 2, + y_pixels: 2, + format: ImageFormat::Rgb, + }; + + let image_widget = + Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); + + Harness::create_with_render( + true, + image_widget, + Size::new(f64::INFINITY, 400.), + |harness| { + harness.send_initial_events(); + harness.just_layout(); + harness.paint(); + }, + |target| { + // the width should be calculated to be 400. + let width = 400; + let raw_pixels = target.into_raw(); + assert_eq!(raw_pixels.len(), 400 * width * 4); + + // Being a height bound widget every row will have no padding at the start and end._id_1 + + // The image starts at (0,0), so 200 black and then 200 white. + let expecting: Vec = [ + vec![0, 0, 0, 255].repeat(200), + vec![255, 255, 255, 255].repeat(200), + ] + .concat(); + assert_eq!(raw_pixels[0 * width * 4..1 * width * 4], expecting[..]); + + // The final row of 600 pixels is 100 padding 200 black, 200 white and then 100 padding. + let expecting: Vec = [ + vec![255, 255, 255, 255].repeat(200), + vec![0, 0, 0, 255].repeat(200), + ] + .concat(); + assert_eq!(raw_pixels[399 * width * 4..400 * width * 4], expecting[..]); + }, + ); } + } From 0de6d8c8e1ebad50ec89629ff0e32b91492881db Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:26:53 +0200 Subject: [PATCH 02/18] fix tests for the new image layour function --- druid/src/widget/image.rs | 69 +++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 6a18db0bd8..a34d7c1215 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -21,7 +21,7 @@ use std::{convert::AsRef, error::Error, path::Path}; use crate::{ piet::{Image as PietImage, ImageFormat, InterpolationMode}, - widget::common::FillStrat, + widget::{common::FillStrat, }, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Rect, RenderContext, Size, UpdateCtx, Widget, }; @@ -451,8 +451,8 @@ mod tests { } #[test] - fn height_bound_paint() { - use crate::{tests::harness::Harness, WidgetId}; + fn width_bound_paint() { + use crate::{tests::harness::Harness, WidgetId, widget::Scroll}; let _id_1 = WidgetId::next(); let image_data = ImageData { pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], @@ -466,8 +466,8 @@ mod tests { Harness::create_with_render( true, - image_widget, - Size::new(f64::INFINITY, 400.), + Scroll::new(image_widget).vertical(), + Size::new(400., 400.), |harness| { harness.send_initial_events(); harness.just_layout(); @@ -481,23 +481,72 @@ mod tests { // Being a height bound widget every row will have no padding at the start and end._id_1 - // The image starts at (0,0), so 200 black and then 200 white. + // The image starts at (0,0), so 200 white and then 200 black. + let expecting: Vec = [ + vec![255, 255, 255, 255].repeat(200), + vec![0, 0, 0, 255].repeat(200), + ] + .concat(); + assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); + + // The final row of 400 pixels is 200 white and then 200 black. let expecting: Vec = [ vec![0, 0, 0, 255].repeat(200), vec![255, 255, 255, 255].repeat(200), ] .concat(); - assert_eq!(raw_pixels[0 * width * 4..1 * width * 4], expecting[..]); + assert_eq!(raw_pixels[200 * width * 4..201 * width * 4], expecting[..]); + }, + ); + } + + #[test] + fn height_bound_paint() { + use crate::{tests::harness::Harness, WidgetId, widget::Scroll}; + let _id_1 = WidgetId::next(); + let image_data = ImageData { + pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + x_pixels: 2, + y_pixels: 2, + format: ImageFormat::Rgb, + }; - // The final row of 600 pixels is 100 padding 200 black, 200 white and then 100 padding. + let image_widget = + Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); + + Harness::create_with_render( + true, + Scroll::new(image_widget).horizontal(), + Size::new(400., 400.), + |harness| { + harness.send_initial_events(); + harness.just_layout(); + harness.paint(); + }, + |target| { + // the width should be calculated to be 400. + let width = 400; + let raw_pixels = target.into_raw(); + assert_eq!(raw_pixels.len(), 400 * width * 4); + + // Being a height bound widget every row will have no padding at the start and end._id_1 + + // The image starts at (0,0), so 200 black and then 200 white. let expecting: Vec = [ vec![255, 255, 255, 255].repeat(200), vec![0, 0, 0, 255].repeat(200), ] .concat(); - assert_eq!(raw_pixels[399 * width * 4..400 * width * 4], expecting[..]); + assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); + + // The final row of 400 pixels is 200 white and then 200 black. + let expecting: Vec = [ + vec![0, 0, 0, 255].repeat(200), + vec![255, 255, 255, 255].repeat(200), + ] + .concat(); + assert_eq!(raw_pixels[200 * width * 4..201 * width * 4], expecting[..]); }, ); } - } From 4f0e4fcedf2a2a8da226df372c87bb6c00ee8603 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:32:18 +0200 Subject: [PATCH 03/18] add changelog --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e02de26cac..b1bd42ff80 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ Leopold Luley Andrey Kabylin Garrett Risley Robert Wittams +Jaap Aarts From f23651652bb6e5ea4d29027886578ef8a4221030 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:34:39 +0200 Subject: [PATCH 04/18] add changelog --- CHANGELOG.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b584ef6a94..8f827e8964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ You can find its changes [documented below](#060---2020-06-01). ### Highlights ### Added + - `OPEN_PANEL_CANCELLED` and `SAVE_PANEL_CANCELLED` commands. ([#1061] by @cmyr) - Export `Image` and `ImageData` by default. ([#1011] by [@covercash2]) - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) @@ -42,6 +43,7 @@ You can find its changes [documented below](#060---2020-06-01). ### Fixed +- `widget::Imge` now computes the layout correctly when unbound in one direction. ([unknown] by [@JAicewizard]) - macOS: Timers not firing during modal loop. ([#1028] by [@xStrom]) - GTK: Directory selection now properly ignores file filters. ([#957] by [@xStrom]) - GTK: Don't crash when receiving an external command while a file dialog is visible. ([#1043] by [@jneem]) @@ -242,17 +244,22 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i Last release without a changelog :( ## [0.4.0] - 2019-12-28 + ## [0.3.2] - 2019-11-05 + ## [0.3.1] - 2019-11-04 + ## 0.3.0 - 2019-11-02 + ## 0.1.1 - 2018-11-02 + ## 0.1.0 - 2018-11-02 [@futurepaul]: https://github.com/futurepaul [@finnerale]: https://github.com/finnerale [@totsteps]: https://github.com/totsteps [@cmyr]: https://github.com/cmyr -[@xStrom]: https://github.com/xStrom +[@xstrom]: https://github.com/xStrom [@teddemunnik]: https://github.com/teddemunnik [@crsaracco]: https://github.com/crsaracco [@pyroxymat]: https://github.com/pyroxymat @@ -264,21 +271,20 @@ Last release without a changelog :( [@thecodewarrior]: https://github.com/thecodewarrior [@sjoshid]: https://github.com/sjoshid [@mastfissh]: https://github.com/mastfissh -[@Zarenor]: https://github.com/Zarenor +[@zarenor]: https://github.com/Zarenor [@yrns]: https://github.com/yrns [@jrmuizel]: https://github.com/jrmuizel [@scholtzan]: https://github.com/scholtzan [@covercash2]: https://github.com/covercash2 [@raphlinus]: https://github.com/raphlinus [@binomial0]: https://github.com/binomial0 -[@ForLoveOfCats]: https://github.com/ForLoveOfCats +[@forloveofcats]: https://github.com/ForLoveOfCats [@chris-zen]: https://github.com/chris-zen [@vkahl]: https://github.com/vkahl [@psychon]: https://github.com/psychon [@sysint64]: https://github.com/sysint64 [@justinmoon]: https://github.com/justinmoon [@rjwittams]: https://github.com/rjwittams - [#599]: https://github.com/linebender/druid/pull/599 [#611]: https://github.com/linebender/druid/pull/611 [#695]: https://github.com/linebender/druid/pull/695 @@ -412,8 +418,7 @@ Last release without a changelog :( [#1171]: https://github.com/linebender/druid/pull/1171 [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 - -[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master +[unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/linebender/druid/compare/v0.3.2...v0.4.0 From 1bc884037791b45572e548f91ac526401795cf6e Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:42:55 +0200 Subject: [PATCH 05/18] fmt --- druid/src/widget/image.rs | 56 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index a34d7c1215..1e87065008 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -21,7 +21,7 @@ use std::{convert::AsRef, error::Error, path::Path}; use crate::{ piet::{Image as PietImage, ImageFormat, InterpolationMode}, - widget::{common::FillStrat, }, + widget::common::FillStrat, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Rect, RenderContext, Size, UpdateCtx, Widget, }; @@ -145,21 +145,21 @@ impl Widget for Image { ) -> Size { bc.debug_check("Image"); - // If either the width or height is constrained calculate a value so that the image fits - // in the size exactly. If it is unconstrained by both width and height take the size of - // the image. - if !bc.is_height_bounded() && bc.is_width_bounded() { - let mut size = bc.max(); - let ratio = size.width / self.image_data.x_pixels as f64; - size.height = ratio * self.image_data.y_pixels as f64; - size - } else if !bc.is_width_bounded() && bc.is_height_bounded(){ - let mut size = bc.max(); - let ratio = size.height / self.image_data.y_pixels as f64; - size.width = ratio * self.image_data.x_pixels as f64; - size - } else { - bc.constrain(self.image_data.get_size()) + // If either the width or height is constrained calculate a value so that the image fits + // in the size exactly. If it is unconstrained by both width and height take the size of + // the image. + if !bc.is_height_bounded() && bc.is_width_bounded() { + let mut size = bc.max(); + let ratio = size.width / self.image_data.x_pixels as f64; + size.height = ratio * self.image_data.y_pixels as f64; + size + } else if !bc.is_width_bounded() && bc.is_height_bounded() { + let mut size = bc.max(); + let ratio = size.height / self.image_data.y_pixels as f64; + size.width = ratio * self.image_data.x_pixels as f64; + size + } else { + bc.constrain(self.image_data.get_size()) } } @@ -448,11 +448,11 @@ mod tests { target.into_png(tmp_dir.join("image.png")).unwrap(); }, ); - } - - #[test] + } + + #[test] fn width_bound_paint() { - use crate::{tests::harness::Harness, WidgetId, widget::Scroll}; + use crate::{tests::harness::Harness, widget::Scroll, WidgetId}; let _id_1 = WidgetId::next(); let image_data = ImageData { pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], @@ -474,8 +474,8 @@ mod tests { harness.paint(); }, |target| { - // the width should be calculated to be 400. - let width = 400; + // the width should be calculated to be 400. + let width = 400; let raw_pixels = target.into_raw(); assert_eq!(raw_pixels.len(), 400 * width * 4); @@ -498,11 +498,11 @@ mod tests { assert_eq!(raw_pixels[200 * width * 4..201 * width * 4], expecting[..]); }, ); - } - - #[test] + } + + #[test] fn height_bound_paint() { - use crate::{tests::harness::Harness, WidgetId, widget::Scroll}; + use crate::{tests::harness::Harness, widget::Scroll, WidgetId}; let _id_1 = WidgetId::next(); let image_data = ImageData { pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], @@ -524,8 +524,8 @@ mod tests { harness.paint(); }, |target| { - // the width should be calculated to be 400. - let width = 400; + // the width should be calculated to be 400. + let width = 400; let raw_pixels = target.into_raw(); assert_eq!(raw_pixels.len(), 400 * width * 4); From a7f5483bec065283f0579fd42528f5cc1c3b0472 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:46:55 +0200 Subject: [PATCH 06/18] format md propperly --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f827e8964..73e1d92a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ You can find its changes [documented below](#060---2020-06-01). ### Highlights ### Added - - `OPEN_PANEL_CANCELLED` and `SAVE_PANEL_CANCELLED` commands. ([#1061] by @cmyr) - Export `Image` and `ImageData` by default. ([#1011] by [@covercash2]) - Re-export `druid_shell::Scalable` under `druid` namespace. ([#1075] by [@ForLoveOfCats]) @@ -244,22 +243,17 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i Last release without a changelog :( ## [0.4.0] - 2019-12-28 - ## [0.3.2] - 2019-11-05 - ## [0.3.1] - 2019-11-04 - ## 0.3.0 - 2019-11-02 - ## 0.1.1 - 2018-11-02 - ## 0.1.0 - 2018-11-02 [@futurepaul]: https://github.com/futurepaul [@finnerale]: https://github.com/finnerale [@totsteps]: https://github.com/totsteps [@cmyr]: https://github.com/cmyr -[@xstrom]: https://github.com/xStrom +[@xStrom]: https://github.com/xStrom [@teddemunnik]: https://github.com/teddemunnik [@crsaracco]: https://github.com/crsaracco [@pyroxymat]: https://github.com/pyroxymat @@ -278,13 +272,14 @@ Last release without a changelog :( [@covercash2]: https://github.com/covercash2 [@raphlinus]: https://github.com/raphlinus [@binomial0]: https://github.com/binomial0 -[@forloveofcats]: https://github.com/ForLoveOfCats +[@ForLoveOfCats]: https://github.com/ForLoveOfCats [@chris-zen]: https://github.com/chris-zen [@vkahl]: https://github.com/vkahl [@psychon]: https://github.com/psychon [@sysint64]: https://github.com/sysint64 [@justinmoon]: https://github.com/justinmoon [@rjwittams]: https://github.com/rjwittams + [#599]: https://github.com/linebender/druid/pull/599 [#611]: https://github.com/linebender/druid/pull/611 [#695]: https://github.com/linebender/druid/pull/695 @@ -418,6 +413,7 @@ Last release without a changelog :( [#1171]: https://github.com/linebender/druid/pull/1171 [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 + [unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 From bda9c6d5a9ffab19975860d929b23eff122bf5ab Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:48:04 +0200 Subject: [PATCH 07/18] format md propperly --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e1d92a3e..d71719814a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -265,7 +265,7 @@ Last release without a changelog :( [@thecodewarrior]: https://github.com/thecodewarrior [@sjoshid]: https://github.com/sjoshid [@mastfissh]: https://github.com/mastfissh -[@zarenor]: https://github.com/Zarenor +[@Zarenor]: https://github.com/Zarenor [@yrns]: https://github.com/yrns [@jrmuizel]: https://github.com/jrmuizel [@scholtzan]: https://github.com/scholtzan @@ -414,7 +414,7 @@ Last release without a changelog :( [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 -[unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master +[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/linebender/druid/compare/v0.3.2...v0.4.0 From ae622b4ebb66bc3793bc1f8e7a6b74c5ba272b71 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Mon, 7 Sep 2020 20:53:45 +0200 Subject: [PATCH 08/18] add myself to changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d71719814a..fccf3f1498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ You can find its changes [documented below](#060---2020-06-01). ### Fixed -- `widget::Imge` now computes the layout correctly when unbound in one direction. ([unknown] by [@JAicewizard]) +- `widget::Imge` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) - macOS: Timers not firing during modal loop. ([#1028] by [@xStrom]) - GTK: Directory selection now properly ignores file filters. ([#957] by [@xStrom]) - GTK: Don't crash when receiving an external command while a file dialog is visible. ([#1043] by [@jneem]) @@ -279,7 +279,7 @@ Last release without a changelog :( [@sysint64]: https://github.com/sysint64 [@justinmoon]: https://github.com/justinmoon [@rjwittams]: https://github.com/rjwittams - +[@JAicewizard]: https://github.com/JAicewizard [#599]: https://github.com/linebender/druid/pull/599 [#611]: https://github.com/linebender/druid/pull/611 [#695]: https://github.com/linebender/druid/pull/695 @@ -413,7 +413,7 @@ Last release without a changelog :( [#1171]: https://github.com/linebender/druid/pull/1171 [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 - +[#1186]: https://github.com/linebender/druid/pull/1186 [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 From ba5b594c5f271f87c4e1ccbd12ace7602bc0100f Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Tue, 8 Sep 2020 20:30:28 +0200 Subject: [PATCH 09/18] applied suggested changers --- CHANGELOG.md | 4 +++- druid/src/widget/image.rs | 18 ++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fccf3f1498..65b7e713a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,6 @@ You can find its changes [documented below](#060---2020-06-01). ### Fixed -- `widget::Imge` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) - macOS: Timers not firing during modal loop. ([#1028] by [@xStrom]) - GTK: Directory selection now properly ignores file filters. ([#957] by [@xStrom]) - GTK: Don't crash when receiving an external command while a file dialog is visible. ([#1043] by [@jneem]) @@ -61,6 +60,7 @@ You can find its changes [documented below](#060---2020-06-01). - Allow derivation of lenses for generic types ([#1120]) by [@rjwittams]) - Switch widget: Toggle animation being window refresh rate dependent ([#1145] by [@ForLoveOfCats]) - Multi-click on Windows, partial fix for #859 ([#1157] by [@raphlinus]) +- `widget::Imge` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) ### Visual @@ -280,6 +280,7 @@ Last release without a changelog :( [@justinmoon]: https://github.com/justinmoon [@rjwittams]: https://github.com/rjwittams [@JAicewizard]: https://github.com/JAicewizard + [#599]: https://github.com/linebender/druid/pull/599 [#611]: https://github.com/linebender/druid/pull/611 [#695]: https://github.com/linebender/druid/pull/695 @@ -414,6 +415,7 @@ Last release without a changelog :( [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 [#1186]: https://github.com/linebender/druid/pull/1186 +++ [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 1e87065008..2312da8180 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -148,16 +148,14 @@ impl Widget for Image { // If either the width or height is constrained calculate a value so that the image fits // in the size exactly. If it is unconstrained by both width and height take the size of // the image. - if !bc.is_height_bounded() && bc.is_width_bounded() { - let mut size = bc.max(); - let ratio = size.width / self.image_data.x_pixels as f64; - size.height = ratio * self.image_data.y_pixels as f64; - size - } else if !bc.is_width_bounded() && bc.is_height_bounded() { - let mut size = bc.max(); - let ratio = size.height / self.image_data.y_pixels as f64; - size.width = ratio * self.image_data.x_pixels as f64; - size + if bc.is_width_bounded() && !bc.is_height_bounded() { + let max = bc.max(); + let ratio = max.width / self.image_data.x_pixels as f64; + Size::new(max.width, ratio * self.image_data.y_pixels as f64) + } else if bc.is_height_bounded() && !bc.is_width_bounded() { + let max = bc.max(); + let ratio = max.height / self.image_data.y_pixels as f64; + Size::new(ratio * self.image_data.x_pixels as f64, max.height) } else { bc.constrain(self.image_data.get_size()) } From 3228e359ad245aee1d18c040b2578f9cca4549d4 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 9 Sep 2020 16:45:46 +0200 Subject: [PATCH 10/18] fix formatting --- CHANGELOG.md | 2 +- druid/src/widget/image.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b7e713a1..9085ecd403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -415,7 +415,7 @@ Last release without a changelog :( [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 [#1186]: https://github.com/linebender/druid/pull/1186 -++ + [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/linebender/druid/compare/v0.4.0...v0.5.0 diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 2312da8180..98723da690 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -148,12 +148,11 @@ impl Widget for Image { // If either the width or height is constrained calculate a value so that the image fits // in the size exactly. If it is unconstrained by both width and height take the size of // the image. + let max = bc.max(); if bc.is_width_bounded() && !bc.is_height_bounded() { - let max = bc.max(); let ratio = max.width / self.image_data.x_pixels as f64; Size::new(max.width, ratio * self.image_data.y_pixels as f64) } else if bc.is_height_bounded() && !bc.is_width_bounded() { - let max = bc.max(); let ratio = max.height / self.image_data.y_pixels as f64; Size::new(ratio * self.image_data.x_pixels as f64, max.height) } else { From a247641283845dfb97f62acba41e90fbf0b6e200 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 9 Sep 2020 16:50:34 +0200 Subject: [PATCH 11/18] fix merge --- docs/book_examples/src/custom_widgets_md.rs | 4 +- druid-shell/Cargo.toml | 4 +- druid-shell/examples/perftest.rs | 4 +- druid/examples/anim.rs | 24 +- druid/examples/blocking_function.rs | 9 +- druid/examples/custom_widget.rs | 26 +- druid/examples/ext_event.rs | 6 +- druid/examples/identity.rs | 27 +- druid/examples/invalidation.rs | 21 +- druid/examples/multiwin.rs | 8 +- druid/examples/open_save.rs | 24 +- druid/examples/styled_text.rs | 27 +- druid/src/app_delegate.rs | 33 ++- druid/src/box_constraints.rs | 201 +++++++++++++++ druid/src/command.rs | 72 +++++- druid/src/contexts.rs | 40 +-- druid/src/core.rs | 72 +++--- druid/src/env.rs | 4 +- druid/src/event.rs | 21 +- druid/src/ext_event.rs | 13 +- druid/src/lib.rs | 2 +- druid/src/menu.rs | 4 +- druid/src/scroll_component.rs | 58 ++--- druid/src/tests/harness.rs | 10 +- druid/src/tests/mod.rs | 28 +- druid/src/text/editable_text.rs | 66 +++++ druid/src/text/layout.rs | 267 ++++++++++++++++++++ druid/src/text/mod.rs | 2 + druid/src/text/movement.rs | 15 +- druid/src/text/text_input.rs | 8 +- druid/src/theme.rs | 30 +-- druid/src/widget/checkbox.rs | 7 +- druid/src/widget/click.rs | 14 +- druid/src/widget/label.rs | 109 ++++---- druid/src/widget/radio.rs | 11 +- druid/src/widget/scroll.rs | 9 +- druid/src/widget/spinner.rs | 18 +- druid/src/widget/switch.rs | 129 +++++----- druid/src/widget/textbox.rs | 188 ++++++++------ druid/src/widget/widget_ext.rs | 3 +- druid/src/win_handler.rs | 75 +++--- druid/src/window.rs | 52 ++-- 42 files changed, 1178 insertions(+), 567 deletions(-) create mode 100644 druid/src/text/layout.rs diff --git a/docs/book_examples/src/custom_widgets_md.rs b/docs/book_examples/src/custom_widgets_md.rs index d12edf0400..3b9875991e 100644 --- a/docs/book_examples/src/custom_widgets_md.rs +++ b/docs/book_examples/src/custom_widgets_md.rs @@ -60,14 +60,14 @@ impl Controller for TextBoxActionController { ) { match event { Event::KeyDown(k) if k.key == Key::Enter => { - ctx.submit_command(ACTION, None); + ctx.submit_command(ACTION); } Event::KeyUp(k) if k.key == Key::Enter => { self.timer = Some(ctx.request_timer(DELAY)); child.event(ctx, event, data, env); } Event::Timer(token) if Some(*token) == self.timer => { - ctx.submit_command(ACTION, None); + ctx.submit_command(ACTION); } _ => child.event(ctx, event, data, env), } diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 892bc10ef9..2949dbfbdc 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -19,7 +19,7 @@ x11 = ["x11rb", "nix", "cairo-sys-rs"] [dependencies] # NOTE: When changing the piet or kurbo versions, ensure that # the kurbo version included in piet is compatible with the kurbo version specified here. -piet-common = "0.2.0-pre2" +piet-common = "0.2.0-pre3" kurbo = "0.6.3" log = "0.4.11" @@ -79,5 +79,5 @@ version = "0.3.44" features = ["Window", "MouseEvent", "CssStyleDeclaration", "WheelEvent", "KeyEvent", "KeyboardEvent"] [dev-dependencies] -piet-common = { version = "0.2.0-pre2", features = ["png"] } +piet-common = { version = "0.2.0-pre3", features = ["png"] } simple_logger = { version = "1.9.0", default-features = false } diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index 3e391249a7..e07ce4b7ef 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -68,7 +68,7 @@ impl WinHandler for PerfTest { self.last_time = now; let layout = piet .text() - .new_text_layout(&msg) + .new_text_layout(msg) .font(FontFamily::MONOSPACE, 15.0) .text_color(FG_COLOR) .build() @@ -80,7 +80,7 @@ impl WinHandler for PerfTest { let layout = piet .text() - .new_text_layout(&msg) + .new_text_layout(msg) .text_color(color) .font(FontFamily::MONOSPACE, 48.0) .build() diff --git a/druid/examples/anim.rs b/druid/examples/anim.rs index 323748c298..313e9325b5 100644 --- a/druid/examples/anim.rs +++ b/druid/examples/anim.rs @@ -26,22 +26,24 @@ struct AnimWidget { impl Widget for AnimWidget { fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut u32, _env: &Env) { - if let Event::MouseDown(_) = event { - self.t = 0.0; - ctx.request_anim_frame(); - } - } - - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &u32, _env: &Env) { - if let LifeCycle::AnimFrame(interval) = event { - ctx.request_paint(); - self.t += (*interval as f64) * 1e-9; - if self.t < 1.0 { + match event { + Event::MouseDown(_) => { + self.t = 0.0; ctx.request_anim_frame(); } + Event::AnimFrame(interval) => { + ctx.request_paint(); + self.t += (*interval as f64) * 1e-9; + if self.t < 1.0 { + ctx.request_anim_frame(); + } + } + _ => (), } } + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &u32, _env: &Env) {} + fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &u32, _data: &u32, _env: &Env) {} fn layout( diff --git a/druid/examples/blocking_function.rs b/druid/examples/blocking_function.rs index 427060d8db..e8e27885a7 100644 --- a/druid/examples/blocking_function.rs +++ b/druid/examples/blocking_function.rs @@ -18,10 +18,13 @@ use std::{thread, time}; use druid::{ AppDelegate, AppLauncher, Command, Data, DelegateCtx, Env, ExtEventSink, Lens, LocalizedString, - Selector, Target, Widget, WidgetExt, WindowDesc, + Selector, Widget, WidgetExt, WindowDesc, }; -use druid::widget::{Button, Either, Flex, Label, Spinner}; +use druid::{ + widget::{Button, Either, Flex, Label, Spinner}, + Target, +}; const FINISH_SLOW_FUNCTION: Selector = Selector::new("finish_slow_function"); @@ -43,7 +46,7 @@ fn slow_function(number: u32) -> u32 { fn wrapped_slow_function(sink: ExtEventSink, number: u32) { thread::spawn(move || { let number = slow_function(number); - sink.submit_command(FINISH_SLOW_FUNCTION, number, None) + sink.submit_command(FINISH_SLOW_FUNCTION, number, Target::Auto) .expect("command failed to submit"); }); } diff --git a/druid/examples/custom_widget.rs b/druid/examples/custom_widget.rs index ef503f27a0..0523231893 100644 --- a/druid/examples/custom_widget.rs +++ b/druid/examples/custom_widget.rs @@ -15,9 +15,12 @@ //! An example of a custom drawing widget. use druid::kurbo::BezPath; -use druid::piet::{FontFamily, ImageFormat, InterpolationMode, Text, TextLayoutBuilder}; +use druid::piet::{FontFamily, ImageFormat, InterpolationMode}; use druid::widget::prelude::*; -use druid::{Affine, AppLauncher, Color, LocalizedString, Point, Rect, WindowDesc}; +use druid::{ + Affine, AppLauncher, Color, FontDescriptor, LocalizedString, Point, Rect, TextLayout, + WindowDesc, +}; struct CustomWidget; @@ -54,7 +57,7 @@ impl Widget for CustomWidget { // The paint method gets called last, after an event flow. // It goes event -> update -> layout -> paint, and each method can influence the next. // Basically, anything that changes the appearance of a widget causes a paint. - fn paint(&mut self, ctx: &mut PaintCtx, data: &String, _env: &Env) { + fn paint(&mut self, ctx: &mut PaintCtx, data: &String, env: &Env) { // Let's draw a picture with Piet! // Clear the whole widget with the color of your choice @@ -81,21 +84,18 @@ impl Widget for CustomWidget { let fill_color = Color::rgba8(0x00, 0x00, 0x00, 0x7F); ctx.fill(rect, &fill_color); - // Text is easy, if you ignore all these unwraps. Just pick a font and a size. - // Here's where we actually use the UI state - let layout = ctx - .text() - .new_text_layout(data) - .font(FontFamily::SYSTEM_UI, 24.0) - .text_color(fill_color) - .build() - .unwrap(); + // Text is easy; in real use TextLayout should be stored in the widget + // and reused. + let mut layout = TextLayout::new(data.as_str()); + layout.set_font(FontDescriptor::new(FontFamily::SERIF).with_size(24.0)); + layout.set_text_color(fill_color); + layout.rebuild_if_needed(&mut ctx.text(), env); // Let's rotate our text slightly. First we save our current (default) context: ctx.with_save(|ctx| { // Now we can rotate the context (or set a clip path, for instance): ctx.transform(Affine::rotate(0.1)); - ctx.draw_text(&layout, (80.0, 40.0)); + layout.draw(ctx, (80.0, 40.0)); }); // When we exit with_save, the original context's rotation is restored diff --git a/druid/examples/ext_event.rs b/druid/examples/ext_event.rs index 86066425dd..8a7ad0ede9 100644 --- a/druid/examples/ext_event.rs +++ b/druid/examples/ext_event.rs @@ -20,7 +20,9 @@ use std::time::Duration; use druid::kurbo::RoundedRect; use druid::widget::prelude::*; -use druid::{AppLauncher, Color, Data, LocalizedString, Rect, Selector, WidgetExt, WindowDesc}; +use druid::{ + AppLauncher, Color, Data, LocalizedString, Rect, Selector, Target, WidgetExt, WindowDesc, +}; const SET_COLOR: Selector = Selector::new("event-example.set-color"); @@ -109,7 +111,7 @@ pub fn main() { // if this fails we're shutting down if event_sink - .submit_command(SET_COLOR, new_color, None) + .submit_command(SET_COLOR, new_color, Target::Auto) .is_err() { break; diff --git a/druid/examples/identity.rs b/druid/examples/identity.rs index bb3a646204..a778635dd7 100644 --- a/druid/examples/identity.rs +++ b/druid/examples/identity.rs @@ -33,9 +33,9 @@ use std::time::Duration; use druid::kurbo::RoundedRect; use druid::widget::{Button, CrossAxisAlignment, Flex, WidgetId}; use druid::{ - AppLauncher, BoxConstraints, Color, Command, Data, Env, Event, EventCtx, LayoutCtx, Lens, - LifeCycle, LifeCycleCtx, LocalizedString, PaintCtx, Rect, RenderContext, Selector, Size, - TimerToken, UpdateCtx, Widget, WidgetExt, WindowDesc, + AppLauncher, BoxConstraints, Color, Data, Env, Event, EventCtx, LayoutCtx, Lens, LifeCycle, + LifeCycleCtx, LocalizedString, PaintCtx, Rect, RenderContext, Selector, Size, TimerToken, + UpdateCtx, Widget, WidgetExt, WindowDesc, }; const CYCLE_DURATION: Duration = Duration::from_millis(100); @@ -173,13 +173,14 @@ fn make_ui() -> impl Widget { .with_spacer(10.0) .with_child( Button::::new("freeze").on_click(move |ctx, data, _env| { - ctx.submit_command(Command::new(FREEZE_COLOR, data.color.clone()), ID_ONE) + ctx.submit_command(FREEZE_COLOR.with(data.color.clone()).to(ID_ONE)) }), ) .with_spacer(10.0) .with_child( - Button::::new("unfreeze") - .on_click(move |ctx, _, _env| ctx.submit_command(UNFREEZE_COLOR, ID_ONE)), + Button::::new("unfreeze").on_click(move |ctx, _, _env| { + ctx.submit_command(UNFREEZE_COLOR.to(ID_ONE)) + }), ), 0.5, ) @@ -191,13 +192,14 @@ fn make_ui() -> impl Widget { .with_spacer(10.0) .with_child( Button::::new("freeze").on_click(move |ctx, data, _env| { - ctx.submit_command(Command::new(FREEZE_COLOR, data.color.clone()), id_two) + ctx.submit_command(FREEZE_COLOR.with(data.color.clone()).to(id_two)) }), ) .with_spacer(10.0) .with_child( - Button::::new("unfreeze") - .on_click(move |ctx, _, _env| ctx.submit_command(UNFREEZE_COLOR, id_two)), + Button::::new("unfreeze").on_click(move |ctx, _, _env| { + ctx.submit_command(UNFREEZE_COLOR.to(id_two)) + }), ), 0.5, ) @@ -209,13 +211,14 @@ fn make_ui() -> impl Widget { .with_spacer(10.0) .with_child( Button::::new("freeze").on_click(move |ctx, data, _env| { - ctx.submit_command(Command::new(FREEZE_COLOR, data.color.clone()), id_three) + ctx.submit_command(FREEZE_COLOR.with(data.color.clone()).to(id_three)) }), ) .with_spacer(10.0) .with_child( - Button::::new("unfreeze") - .on_click(move |ctx, _, _env| ctx.submit_command(UNFREEZE_COLOR, id_three)), + Button::::new("unfreeze").on_click(move |ctx, _, _env| { + ctx.submit_command(UNFREEZE_COLOR.to(id_three)) + }), ), 0.5, ) diff --git a/druid/examples/invalidation.rs b/druid/examples/invalidation.rs index 0d6c1724c9..42477ccad2 100644 --- a/druid/examples/invalidation.rs +++ b/druid/examples/invalidation.rs @@ -93,24 +93,23 @@ impl Widget> for CircleView { }); } ctx.request_anim_frame(); + } else if let Event::AnimFrame(_) = ev { + for c in &*data { + ctx.request_paint_rect(kurbo::Circle::new(c.pos, RADIUS).bounding_box()); + } + if !data.is_empty() { + ctx.request_anim_frame(); + } } } fn lifecycle( &mut self, - ctx: &mut LifeCycleCtx, - ev: &LifeCycle, - data: &Vector, + _ctx: &mut LifeCycleCtx, + _ev: &LifeCycle, + _data: &Vector, _env: &Env, ) { - if let LifeCycle::AnimFrame(_) = ev { - for c in data { - ctx.request_paint_rect(kurbo::Circle::new(c.pos, RADIUS).bounding_box()); - } - if !data.is_empty() { - ctx.request_anim_frame(); - } - } } fn update( diff --git a/druid/examples/multiwin.rs b/druid/examples/multiwin.rs index 6b7c8020e2..dbcb192857 100644 --- a/druid/examples/multiwin.rs +++ b/druid/examples/multiwin.rs @@ -57,11 +57,11 @@ fn ui_builder() -> impl Widget { .with_arg("count", |data: &State, _env| data.menu_count.into()); let label = Label::new(text); let inc_button = Button::::new("Add menu item") - .on_click(|ctx, _data, _env| ctx.submit_command(MENU_INCREMENT_ACTION, Global)); + .on_click(|ctx, _data, _env| ctx.submit_command(MENU_INCREMENT_ACTION.to(Global))); let dec_button = Button::::new("Remove menu item") - .on_click(|ctx, _data, _env| ctx.submit_command(MENU_DECREMENT_ACTION, Global)); + .on_click(|ctx, _data, _env| ctx.submit_command(MENU_DECREMENT_ACTION.to(Global))); let new_button = Button::::new("New window").on_click(|ctx, _data, _env| { - ctx.submit_command(sys_cmds::NEW_FILE, Target::Global); + ctx.submit_command(sys_cmds::NEW_FILE.to(Global)); }); let quit_button = Button::::new("Quit app").on_click(|_ctx, _data, _env| { Application::global().quit(); @@ -239,7 +239,7 @@ fn make_menu(state: &State) -> MenuDesc { MenuItem::new( LocalizedString::new("hello-counter") .with_arg("count", move |_, _| i.into()), - Command::new(MENU_COUNT_ACTION, i), + MENU_COUNT_ACTION.with(i), ) .disabled_if(|| i % 3 == 0) .selected_if(|| i == state.selected) diff --git a/druid/examples/open_save.rs b/druid/examples/open_save.rs index a2c659d1ad..73d2d9145e 100644 --- a/druid/examples/open_save.rs +++ b/druid/examples/open_save.rs @@ -42,22 +42,18 @@ fn ui_builder() -> impl Widget { let input = TextBox::new(); let save = Button::new("Save").on_click(move |ctx, _, _| { - ctx.submit_command( - Command::new( - druid::commands::SHOW_SAVE_PANEL, - save_dialog_options.clone(), - ), - None, - ) + ctx.submit_command(Command::new( + druid::commands::SHOW_SAVE_PANEL, + save_dialog_options.clone(), + Target::Auto, + )) }); let open = Button::new("Open").on_click(move |ctx, _, _| { - ctx.submit_command( - Command::new( - druid::commands::SHOW_OPEN_PANEL, - open_dialog_options.clone(), - ), - None, - ) + ctx.submit_command(Command::new( + druid::commands::SHOW_OPEN_PANEL, + open_dialog_options.clone(), + Target::Auto, + )) }); let mut col = Flex::column(); diff --git a/druid/examples/styled_text.rs b/druid/examples/styled_text.rs index 73b5103973..314f14089e 100644 --- a/druid/examples/styled_text.rs +++ b/druid/examples/styled_text.rs @@ -16,14 +16,13 @@ use druid::widget::{Checkbox, Flex, Label, MainAxisAlignment, Painter, Parse, Stepper, TextBox}; use druid::{ - theme, AppLauncher, ArcStr, Color, Data, Key, Lens, LensExt, LensWrap, LocalizedString, - PlatformError, RenderContext, Widget, WidgetExt, WindowDesc, + theme, AppLauncher, Color, Data, FontDescriptor, FontFamily, Key, Lens, LensExt, LensWrap, + LocalizedString, PlatformError, RenderContext, Widget, WidgetExt, WindowDesc, }; use std::fmt::Display; -// This is a custom key we'll use with Env to set and get our text size. -const MY_CUSTOM_TEXT_SIZE: Key = Key::new("styled_text.custom_text_size"); -const MY_CUSTOM_FONT: Key = Key::new("styled_text.custom_font"); +// This is a custom key we'll use with Env to set and get our font. +const MY_CUSTOM_FONT: Key = Key::new("styled_text.custom_font"); #[derive(Clone, Lens, Data)] struct AppData { @@ -73,7 +72,7 @@ fn ui_builder() -> impl Widget { }); // This is druid's default text style. - // It's set by theme::LABEL_COLOR, theme::TEXT_SIZE_NORMAL, and theme::FONT_NAME + // It's set by theme::LABEL_COLOR and theme::UI_FONT let label = Label::new(|data: &String, _env: &_| format!("Default: {}", data)).lens(AppData::text); @@ -88,19 +87,19 @@ fn ui_builder() -> impl Widget { // to the default font) let styled_label = Label::new(|data: &AppData, _env: &_| format!("{}", data)) .with_text_color(theme::PRIMARY_LIGHT) - .with_text_size(MY_CUSTOM_TEXT_SIZE) .with_font(MY_CUSTOM_FONT) .background(my_painter) .on_click(|_, data, _| { data.size *= 1.1; }) .env_scope(|env: &mut druid::Env, data: &AppData| { - env.set(MY_CUSTOM_TEXT_SIZE, data.size); - if data.mono { - env.set(MY_CUSTOM_FONT, "monospace"); + let new_font = if data.mono { + FontDescriptor::new(FontFamily::MONOSPACE) } else { - env.set(MY_CUSTOM_FONT, env.get(theme::FONT_NAME)); + FontDescriptor::new(FontFamily::SYSTEM_UI) } + .with_size(data.size); + env.set(MY_CUSTOM_FONT, new_font); }); let stepper = Stepper::new() @@ -118,7 +117,10 @@ fn ui_builder() -> impl Widget { let mono_checkbox = Checkbox::new("Monospace").lens(AppData::mono); - let input = TextBox::new().fix_width(200.0).lens(AppData::text); + let input = TextBox::new() + .with_text_size(38.0) + .fix_width(200.0) + .lens(AppData::text); Flex::column() .main_axis_alignment(MainAxisAlignment::Center) @@ -131,4 +133,5 @@ fn ui_builder() -> impl Widget { .with_child(mono_checkbox) .with_spacer(8.0) .with_child(input.padding(5.0)) + .debug_widget_id() } diff --git a/druid/src/app_delegate.rs b/druid/src/app_delegate.rs index 806c156949..9e193db644 100644 --- a/druid/src/app_delegate.rs +++ b/druid/src/app_delegate.rs @@ -14,20 +14,18 @@ //! Customizing application-level behaviour. -use std::{ - any::{Any, TypeId}, - collections::VecDeque, -}; +use std::any::{Any, TypeId}; use crate::{ - commands, Command, Data, Env, Event, MenuDesc, SingleUse, Target, WindowDesc, WindowId, + commands, core::CommandQueue, Command, Data, Env, Event, MenuDesc, SingleUse, Target, + WindowDesc, WindowId, }; /// A context passed in to [`AppDelegate`] functions. /// /// [`AppDelegate`]: trait.AppDelegate.html pub struct DelegateCtx<'a> { - pub(crate) command_queue: &'a mut VecDeque<(Target, Command)>, + pub(crate) command_queue: &'a mut CommandQueue, pub(crate) app_data_type: TypeId, } @@ -38,16 +36,13 @@ impl<'a> DelegateCtx<'a> { /// submitted during the handling of an event are executed before /// the [`update()`] method is called. /// + /// [`Target::Auto`] commands will be sent to every window (`Target::Global`). + /// /// [`Command`]: struct.Command.html /// [`update()`]: trait.Widget.html#tymethod.update - pub fn submit_command( - &mut self, - command: impl Into, - target: impl Into>, - ) { - let command = command.into(); - let target = target.into().unwrap_or(Target::Global); - self.command_queue.push_back((target, command)) + pub fn submit_command(&mut self, command: impl Into) { + self.command_queue + .push_back(command.into().default_to(Target::Global)) } /// Create a new window. @@ -57,8 +52,9 @@ impl<'a> DelegateCtx<'a> { pub fn new_window(&mut self, desc: WindowDesc) { if self.app_data_type == TypeId::of::() { self.submit_command( - Command::new(commands::NEW_WINDOW, SingleUse::new(Box::new(desc))), - Target::Global, + commands::NEW_WINDOW + .with(SingleUse::new(Box::new(desc))) + .to(Target::Global), ); } else { const MSG: &str = "WindowDesc - T must match the application data type."; @@ -77,8 +73,9 @@ impl<'a> DelegateCtx<'a> { pub fn set_menu(&mut self, menu: MenuDesc, window: WindowId) { if self.app_data_type == TypeId::of::() { self.submit_command( - Command::new(commands::SET_MENU, Box::new(menu)), - Target::Window(window), + commands::SET_MENU + .with(Box::new(menu)) + .to(Target::Window(window)), ); } else { const MSG: &str = "MenuDesc - T must match the application data type."; diff --git a/druid/src/box_constraints.rs b/druid/src/box_constraints.rs index c0af3f1bb5..19dce05bcf 100644 --- a/druid/src/box_constraints.rs +++ b/druid/src/box_constraints.rs @@ -163,12 +163,213 @@ impl BoxConstraints { BoxConstraints::new(min, max) } + + /// Test whether these constraints contain the given `Size`. + pub fn contains(&self, size: impl Into) -> bool { + let size = size.into(); + (self.min.width <= size.width && size.width <= self.max.width) + && (self.min.height <= size.height && size.height <= self.max.height) + } + + /// Find the `Size` within these `BoxConstraint`s that minimises the difference between the + /// returned `Size`'s aspect ratio and `aspect_ratio`, where *aspect ratio* is defined as + /// `height / width`. + /// + /// If multiple `Size`s give the optimal `aspect_ratio`, then the one with the `width` nearest + /// the supplied width will be used. Specifically, if `width == 0.0` then the smallest possible + /// `Size` will be chosen, and likewise if `width == f64::INFINITY`, then the largest `Size` + /// will be chosen. + /// + /// Use this function when maintaining an aspect ratio is more important than minimizing the + /// distance between input and output size width and height. + pub fn constrain_aspect_ratio(&self, aspect_ratio: f64, width: f64) -> Size { + // Minimizing/maximizing based on aspect ratio seems complicated, but in reality everything + // is linear, so the amount of work to do is low. + let ideal_size = Size { + width, + height: width * aspect_ratio, + }; + + // It may be possible to remove these in the future if the invariant is checked elsewhere. + let aspect_ratio = aspect_ratio.abs(); + let width = width.abs(); + + // Firstly check if we can simply return the exact requested + if self.contains(ideal_size) { + return ideal_size; + } + + // Then we check if any `Size`s with our desired aspect ratio are inside the constraints. + // TODO this currently outputs garbage when things are < 0. + let min_w_min_h = self.min.height / self.min.width; + let max_w_min_h = self.min.height / self.max.width; + let min_w_max_h = self.max.height / self.min.width; + let max_w_max_h = self.max.height / self.max.width; + + // When the aspect ratio line crosses the constraints, the closest point must be one of the + // two points where the aspect ratio enters/exits. + + // When the aspect ratio line doesn't intersect the box of possible sizes, the closest + // point must be either (max width, min height) or (max height, min width). So all we have + // to do is check which one of these has the closest aspect ratio. + + // Check each possible intersection (or not) of the aspect ratio line with the constraints + if aspect_ratio > min_w_max_h { + // outside max height min width + Size { + width: self.min.width, + height: self.max.height, + } + } else if aspect_ratio < max_w_min_h { + // outside min height max width + Size { + width: self.max.width, + height: self.min.height, + } + } else if aspect_ratio > min_w_min_h { + // hits the constraints on the min width line + if width < self.min.width { + // we take the point on the min width + Size { + width: self.min.width, + height: self.min.width * aspect_ratio, + } + } else if aspect_ratio < max_w_max_h { + // exits through max.width + Size { + width: self.max.width, + height: self.max.width * aspect_ratio, + } + } else { + // exits through max.height + Size { + width: self.max.height * aspect_ratio.recip(), + height: self.max.height, + } + } + } else { + // final case is where we hit constraints on the min height line + if width < self.min.width { + // take the point on the min height + Size { + width: self.min.height * aspect_ratio.recip(), + height: self.min.height, + } + } else if aspect_ratio > max_w_max_h { + // exit thru max height + Size { + width: self.max.height * aspect_ratio.recip(), + height: self.max.height, + } + } else { + // exit thru max width + Size { + width: self.max.width, + height: self.max.width * aspect_ratio, + } + } + } + } } #[cfg(test)] mod tests { use super::*; + fn bc(min_width: f64, min_height: f64, max_width: f64, max_height: f64) -> BoxConstraints { + BoxConstraints::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ) + } + + #[test] + fn constrain_aspect_ratio() { + for (bc, aspect_ratio, width, output) in [ + // The ideal size lies within the constraints + (bc(0.0, 0.0, 100.0, 100.0), 1.0, 50.0, Size::new(50.0, 50.0)), + (bc(0.0, 10.0, 90.0, 100.0), 1.0, 50.0, Size::new(50.0, 50.0)), + // The correct aspect ratio is available (but not width) + // min height + ( + bc(10.0, 10.0, 100.0, 100.0), + 1.0, + 5.0, + Size::new(10.0, 10.0), + ), + ( + bc(40.0, 90.0, 60.0, 100.0), + 2.0, + 30.0, + Size::new(45.0, 90.0), + ), + ( + bc(10.0, 10.0, 100.0, 100.0), + 0.5, + 5.0, + Size::new(20.0, 10.0), + ), + // min width + ( + bc(10.0, 10.0, 100.0, 100.0), + 2.0, + 5.0, + Size::new(10.0, 20.0), + ), + ( + bc(90.0, 40.0, 100.0, 60.0), + 0.5, + 60.0, + Size::new(90.0, 45.0), + ), + ( + bc(50.0, 0.0, 50.0, 100.0), + 1.0, + 100.0, + Size::new(50.0, 50.0), + ), + // max height + ( + bc(10.0, 10.0, 100.0, 100.0), + 2.0, + 105.0, + Size::new(50.0, 100.0), + ), + ( + bc(10.0, 10.0, 100.0, 100.0), + 0.5, + 105.0, + Size::new(100.0, 50.0), + ), + // The correct aspet ratio is not available + ( + bc(20.0, 20.0, 40.0, 40.0), + 10.0, + 30.0, + Size::new(20.0, 40.0), + ), + (bc(20.0, 20.0, 40.0, 40.0), 0.1, 30.0, Size::new(40.0, 20.0)), + // non-finite + ( + bc(50.0, 0.0, 50.0, f64::INFINITY), + 1.0, + 100.0, + Size::new(50.0, 50.0), + ), + ] + .iter() + { + assert_eq!( + bc.constrain_aspect_ratio(*aspect_ratio, *width), + *output, + "bc:{:?}, ar:{}, w:{}", + bc, + aspect_ratio, + width + ); + } + } + #[test] fn unbounded() { assert!(!BoxConstraints::UNBOUNDED.is_width_bounded()); diff --git a/druid/src/command.rs b/druid/src/command.rs index 9f9770025c..4dacf4351e 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -62,11 +62,11 @@ pub struct Selector(SelectorSymbol, PhantomData<*const T>); /// /// # Examples /// ``` -/// use druid::{Command, Selector}; +/// use druid::{Command, Selector, Target}; /// /// let selector = Selector::new("process_rows"); /// let rows = vec![1, 3, 10, 12]; -/// let command = Command::new(selector, rows); +/// let command = Command::new(selector, rows, Target::Auto); /// /// assert_eq!(command.get(selector), Some(&vec![1, 3, 10, 12])); /// ``` @@ -78,6 +78,7 @@ pub struct Selector(SelectorSymbol, PhantomData<*const T>); pub struct Command { symbol: SelectorSymbol, payload: Arc, + target: Target, } /// A wrapper type for [`Command`] payloads that should only be used once. @@ -87,13 +88,13 @@ pub struct Command { /// /// # Examples /// ``` -/// use druid::{Command, Selector, SingleUse}; +/// use druid::{Command, Selector, SingleUse, Target}; /// /// struct CantClone(u8); /// /// let selector = Selector::new("use-once"); /// let num = CantClone(42); -/// let command = Command::new(selector, SingleUse::new(num)); +/// let command = Command::new(selector, SingleUse::new(num), Target::Auto); /// /// let payload: &SingleUse = command.get_unchecked(selector); /// if let Some(num) = payload.take() { @@ -118,6 +119,11 @@ pub enum Target { Window(WindowId), /// The target is a specific widget. Widget(WidgetId), + /// The target will be determined automatically. + /// + /// How this behaves depends on the context used to submit the command. + /// Each `submit_command` function should have documentation about the specific behavior. + Auto, } /// Commands with special meaning, defined by druid. @@ -247,6 +253,11 @@ pub mod sys { impl Selector<()> { /// A selector that does nothing. pub const NOOP: Selector = Selector::new(""); + + /// Turns this into a command with the specified target. + pub fn to(self, target: impl Into) -> Command { + Command::from(self).to(target.into()) + } } impl Selector { @@ -267,9 +278,14 @@ impl Selector { /// If the payload is `()` there is no need to call this, /// as `Selector<()>` implements `Into`. /// + /// By default, the command will have [`Target::Auto`]. + /// The [`Command::to`] method can be used to override this. + /// /// [`Command::new`]: struct.Command.html#method.new + /// [`Command::to`]: struct.Command.html#method.to + /// [`Target::Auto`]: enum.Target.html#variant.Auto pub fn with(self, payload: T) -> Command { - Command::new(self, payload) + Command::new(self, payload, Target::Auto) } } @@ -282,21 +298,51 @@ impl Command { /// /// [`Selector`]: struct.Selector.html /// [`Selector::with`]: struct.Selector.html#method.with - pub fn new(selector: Selector, payload: T) -> Self { + pub fn new(selector: Selector, payload: T, target: impl Into) -> Self { Command { symbol: selector.symbol(), payload: Arc::new(payload), + target: target.into(), } } /// Used to create a command from the types sent via an `ExtEventSink`. - pub(crate) fn from_ext(symbol: SelectorSymbol, payload: Box) -> Self { + pub(crate) fn from_ext( + symbol: SelectorSymbol, + payload: Box, + target: impl Into, + ) -> Self { Command { symbol, payload: payload.into(), + target: target.into(), } } + /// Set the commands target. + /// + /// [`Command::target`] can be used to get the current target. + /// + /// [`Command::target`]: #method.target + pub fn to(mut self, target: impl Into) -> Self { + self.target = target.into(); + self + } + + pub(crate) fn default_to(mut self, target: impl Into) -> Self { + self.target.default(target.into()); + self + } + + /// Returns the commands target. + /// + /// [`Command::to`] can be used to change the target. + /// + /// [`Command::to`]: #method.to + pub fn target(&self) -> Target { + self.target + } + /// Returns `true` if `self` matches this `selector`. pub fn is(&self, selector: Selector) -> bool { self.symbol == selector.symbol() @@ -370,6 +416,7 @@ impl From for Command { Command { symbol: selector.symbol(), payload: Arc::new(()), + target: Target::Auto, } } } @@ -389,6 +436,15 @@ impl Clone for Selector { } } +impl Target { + /// If `self` is `Auto` it will be replaced with `target`. + pub(crate) fn default(&mut self, target: Target) { + if self == &Target::Auto { + *self = target; + } + } +} + impl From for Target { fn from(id: WindowId) -> Target { Target::Window(id) @@ -421,7 +477,7 @@ mod tests { fn get_payload() { let sel = Selector::new("my-selector"); let payload = vec![0, 1, 2]; - let command = Command::new(sel, payload); + let command = Command::new(sel, payload, Target::Auto); assert_eq!(command.get(sel), Some(&vec![0, 1, 2])); } } diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 9167e1c19f..93e188ab48 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -334,14 +334,12 @@ impl_context_method!( /// the [`update`] method is called; events submitted during [`update`] /// are handled after painting. /// + /// [`Target::Auto`] commands will be sent to the window containing the widget. + /// /// [`Command`]: struct.Command.html /// [`update`]: trait.Widget.html#tymethod.update - pub fn submit_command( - &mut self, - cmd: impl Into, - target: impl Into>, - ) { - self.state.submit_command(cmd.into(), target.into()) + pub fn submit_command(&mut self, cmd: impl Into) { + self.state.submit_command(cmd.into()) } /// Returns an [`ExtEventSink`] that can be moved between threads, @@ -387,8 +385,9 @@ impl EventCtx<'_, '_> { pub fn new_window(&mut self, desc: WindowDesc) { if self.state.root_app_data_type == TypeId::of::() { self.submit_command( - Command::new(commands::NEW_WINDOW, SingleUse::new(Box::new(desc))), - Target::Global, + commands::NEW_WINDOW + .with(SingleUse::new(Box::new(desc))) + .to(Target::Global), ); } else { const MSG: &str = "WindowDesc - T must match the application data type."; @@ -407,8 +406,9 @@ impl EventCtx<'_, '_> { pub fn show_context_menu(&mut self, menu: ContextMenu) { if self.state.root_app_data_type == TypeId::of::() { self.submit_command( - Command::new(commands::SHOW_CONTEXT_MENU, Box::new(menu)), - Target::Window(self.state.window_id), + commands::SHOW_CONTEXT_MENU + .with(Box::new(menu)) + .to(Target::Window(self.state.window_id)), ); } else { const MSG: &str = "ContextMenu - T must match the application data type."; @@ -449,6 +449,15 @@ impl EventCtx<'_, '_> { self.widget_state.request_focus = Some(FocusChange::Focus(id)); } + /// Transfer focus to the widget with the given `WidgetId`. + /// + /// See [`is_focused`] for more information about focus. + /// + /// [`is_focused`]: struct.EventCtx.html#method.is_focused + pub fn set_focus(&mut self, target: WidgetId) { + self.widget_state.request_focus = Some(FocusChange::Focus(target)); + } + /// Transfer focus to the next focusable widget. /// /// This should only be called by a widget that currently has focus. @@ -671,16 +680,17 @@ impl<'a> ContextState<'a> { } } - fn submit_command(&mut self, command: Command, target: Option) { - let target = target.unwrap_or_else(|| self.window_id.into()); - self.command_queue.push_back((target, command)) + fn submit_command(&mut self, command: Command) { + self.command_queue + .push_back(command.default_to(self.window_id)); } fn set_menu(&mut self, menu: MenuDesc) { if self.root_app_data_type == TypeId::of::() { self.submit_command( - Command::new(commands::SET_MENU, Box::new(menu)), - Some(Target::Window(self.window_id)), + commands::SET_MENU + .with(Box::new(menu)) + .to(Target::Window(self.window_id)), ); } else { const MSG: &str = "MenuDesc - T must match the application data type."; diff --git a/druid/src/core.rs b/druid/src/core.rs index 76a350e6ff..22529a87e0 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -19,16 +19,15 @@ use std::collections::{HashMap, VecDeque}; use crate::bloom::Bloom; use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; -use crate::piet::{FontFamily, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder}; use crate::util::ExtendDrain; use crate::{ BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle, - LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, Target, TimerToken, UpdateCtx, Widget, - WidgetId, + LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, RenderContext, Target, TextLayout, + TimerToken, UpdateCtx, Widget, WidgetId, }; /// Our queue type -pub(crate) type CommandQueue = VecDeque<(Target, Command)>; +pub(crate) type CommandQueue = VecDeque; /// A container for one widget in the hierarchy. /// @@ -51,7 +50,7 @@ pub struct WidgetPod { env: Option, inner: W, // stashed layout so we don't recompute this when debugging - debug_widget_text: Option, + debug_widget_text: TextLayout, } /// Generic state for all widgets in the hierarchy. @@ -145,7 +144,7 @@ impl> WidgetPod { old_data: None, env: None, inner, - debug_widget_text: None, + debug_widget_text: TextLayout::new(""), } } @@ -429,7 +428,7 @@ impl> WidgetPod { } fn make_widget_id_layout_if_needed(&mut self, id: WidgetId, ctx: &mut PaintCtx, env: &Env) { - if self.debug_widget_text.is_none() { + if self.debug_widget_text.needs_rebuild() { // switch text color based on background, this is meh and that's okay let border_color = env.get_debug_color(id.to_raw()); let (r, g, b, _) = border_color.as_rgba8(); @@ -439,37 +438,29 @@ impl> WidgetPod { } else { Color::BLACK }; - let id_string = id.to_raw().to_string(); - self.debug_widget_text = ctx - .text() - .new_text_layout(&id_string) - .font(FontFamily::SYSTEM_UI, 10.0) - .text_color(text_color) - .build() - .ok(); + self.debug_widget_text.set_text(id_string); + self.debug_widget_text.set_text_size(10.0); + self.debug_widget_text.set_text_color(text_color); + self.debug_widget_text + .rebuild_if_needed(&mut ctx.text(), env); } } fn debug_paint_widget_ids(&self, ctx: &mut PaintCtx, env: &Env) { // we clone because we need to move it for paint_with_z_index let text = self.debug_widget_text.clone(); - if let Some(text) = text { - let text_size = text.size(); - let origin = ctx.size().to_vec2() - text_size.to_vec2(); - let border_color = env.get_debug_color(ctx.widget_id().to_raw()); - self.debug_paint_layout_bounds(ctx, env); - - ctx.paint_with_z_index(ctx.depth(), move |ctx| { - let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0)); + let text_size = text.size(); + let origin = ctx.size().to_vec2() - text_size.to_vec2(); + let border_color = env.get_debug_color(ctx.widget_id().to_raw()); + self.debug_paint_layout_bounds(ctx, env); - let text_pos = origin + Vec2::new(0., 8.0); - let text_rect = Rect::from_origin_size(origin, text_size); - - ctx.fill(text_rect, &border_color); - ctx.draw_text(&text, text_pos); - }) - } + ctx.paint_with_z_index(ctx.depth(), move |ctx| { + let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0)); + let text_rect = Rect::from_origin_size(origin, text_size); + ctx.fill(text_rect, &border_color); + text.draw(ctx, origin); + }) } fn debug_paint_layout_bounds(&self, ctx: &mut PaintCtx, env: &Env) { @@ -581,21 +572,22 @@ impl> WidgetPod { ); had_active || hot_changed } - InternalEvent::TargetedCommand(target, cmd) => { - match target { - Target::Widget(id) if *id == self.id() => { + InternalEvent::TargetedCommand(cmd) => { + match cmd.target() { + Target::Widget(id) if id == self.id() => { modified_event = Some(Event::Command(cmd.clone())); true } Target::Widget(id) => { // Recurse when the target widget could be our descendant. // The bloom filter we're checking can return false positives. - self.state.children.may_contain(id) + self.state.children.may_contain(&id) } Target::Global | Target::Window(_) => { modified_event = Some(Event::Command(cmd.clone())); true } + _ => false, } } InternalEvent::RouteTimer(token, widget_id) => { @@ -691,6 +683,11 @@ impl> WidgetPod { false } } + Event::AnimFrame(_) => { + let r = self.state.request_anim; + self.state.request_anim = false; + r + } Event::KeyDown(_) => self.state.has_focus, Event::KeyUp(_) => self.state.has_focus, Event::Paste(_) => self.state.has_focus, @@ -788,11 +785,6 @@ impl> WidgetPod { true } }, - LifeCycle::AnimFrame(_) => { - let r = self.state.request_anim; - self.state.request_anim = false; - r - } LifeCycle::WidgetAdded => { assert!(self.old_data.is_none()); @@ -1010,7 +1002,7 @@ mod tests { state: &mut state, }; - let env = Env::default(); + let env = crate::theme::init(); widget.lifecycle(&mut ctx, &LifeCycle::WidgetAdded, &None, &env); assert!(ctx.widget_state.children.may_contain(&ID_1)); diff --git a/druid/src/env.rs b/druid/src/env.rs index cb494a817c..82171ad1c6 100644 --- a/druid/src/env.rs +++ b/druid/src/env.rs @@ -87,7 +87,7 @@ struct EnvImpl { /// /// [`ValueType`]: trait.ValueType.html /// [`Env`]: struct.Env.html -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Key { key: &'static str, value_type: PhantomData<*const T>, @@ -120,7 +120,7 @@ pub enum Value { /// /// [`Key`]: struct.Key.html /// [`Env`]: struct.Env.html -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum KeyOrValue { /// A concrete [`Value`] of type `T`. /// diff --git a/druid/src/event.rs b/druid/src/event.rs index 4c6f9b300e..a918a308cb 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -19,7 +19,7 @@ use crate::kurbo::{Rect, Shape, Size, Vec2}; use druid_shell::{Clipboard, KeyEvent, TimerToken}; use crate::mouse::MouseEvent; -use crate::{Command, Target, WidgetId}; +use crate::{Command, WidgetId}; /// An event, propagated downwards during event flow. /// @@ -108,6 +108,17 @@ pub enum Event { /// /// [`EventCtx::request_timer()`]: struct.EventCtx.html#method.request_timer Timer(TimerToken), + /// Called at the beginning of a new animation frame. + /// + /// On the first frame when transitioning from idle to animating, `interval` + /// will be 0. (This logic is presently per-window but might change to + /// per-widget to make it more consistent). Otherwise it is in nanoseconds. + /// + /// The `paint` method will be called shortly after this event is finished. + /// As a result, you should try to avoid doing anything computationally + /// intensive in response to an `AnimFrame` event: it might make Druid miss + /// the monitor's refresh, causing lag or jerky animation. + AnimFrame(u64), /// Called with an arbitrary [`Command`], submitted from elsewhere in /// the application. /// @@ -142,7 +153,7 @@ pub enum InternalEvent { /// but we know that we've stopped receiving the mouse events. MouseLeave, /// A command still in the process of being dispatched. - TargetedCommand(Target, Command), + TargetedCommand(Command), /// Used for routing timer events. RouteTimer(TimerToken, WidgetId), } @@ -182,12 +193,6 @@ pub enum LifeCycle { /// [`Rect`]: struct.Rect.html /// [`WidgetPod::set_layout_rect`]: struct.WidgetPod.html#method.set_layout_rect Size(Size), - /// Called at the beginning of a new animation frame. - /// - /// On the first frame when transitioning from idle to animating, `interval` - /// will be 0. (This logic is presently per-window but might change to - /// per-widget to make it more consistent). Otherwise it is in nanoseconds. - AnimFrame(u64), /// Called when the "hot" status changes. /// /// This will always be called _before_ the event that triggered it; that is, diff --git a/druid/src/ext_event.rs b/druid/src/ext_event.rs index cb1df279dd..5e175d0020 100644 --- a/druid/src/ext_event.rs +++ b/druid/src/ext_event.rs @@ -22,7 +22,7 @@ use crate::shell::IdleHandle; use crate::win_handler::EXT_EVENT_IDLE_TOKEN; use crate::{command::SelectorSymbol, Command, Selector, Target, WindowId}; -pub(crate) type ExtCommand = (SelectorSymbol, Box, Option); +pub(crate) type ExtCommand = (SelectorSymbol, Box, Target); /// A thing that can move into other threads and be used to submit commands back /// to the running application. @@ -75,12 +75,12 @@ impl ExtEventHost { !self.queue.lock().unwrap().is_empty() } - pub(crate) fn recv(&mut self) -> Option<(Option, Command)> { + pub(crate) fn recv(&mut self) -> Option { self.queue .lock() .unwrap() .pop_front() - .map(|(sel, obj, targ)| (targ, Command::from_ext(sel, obj))) + .map(|(selector, payload, target)| Command::from_ext(selector, payload, target)) } } @@ -93,20 +93,21 @@ impl ExtEventSink { /// /// The `payload` must implement `Any + Send + Sync`. /// - /// If no explicit `Target` is submitted, the `Command` will be sent to + /// If submitted with `Target::Auto`, the [`Command`] will be sent to /// the application's first window; if that window is subsequently closed, /// then the command will be sent to *an arbitrary other window*. /// /// This behavior may be changed in the future; in any case, you should - /// probably provide an explicit `Target`. + /// probably provide an explicit [`Target`]. /// /// [`Command`]: struct.Command.html /// [`Selector`]: struct.Selector.html + /// [`Target`]: struct.Target.html pub fn submit_command( &self, selector: Selector, payload: impl Into>, - target: impl Into>, + target: impl Into, ) -> Result<(), ExtEventError> { let target = target.into(); let payload = payload.into(); diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 4f1aaf8dd0..7eeb35b272 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -197,7 +197,7 @@ pub use lens::{Lens, LensExt, LensWrap}; pub use localization::LocalizedString; pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem}; pub use mouse::MouseEvent; -pub use text::FontDescriptor; +pub use text::{FontDescriptor, TextLayout}; pub use widget::{Widget, WidgetExt, WidgetId}; pub use win_handler::DruidHandler; pub use window::{Window, WindowId}; diff --git a/druid/src/menu.rs b/druid/src/menu.rs index c8a149750f..a6e6d52a38 100644 --- a/druid/src/menu.rs +++ b/druid/src/menu.rs @@ -266,7 +266,7 @@ impl MenuDesc { /// # Examples /// /// ``` - /// use druid::{Command, LocalizedString, MenuDesc, MenuItem, Selector}; + /// use druid::{Command, LocalizedString, MenuDesc, MenuItem, Selector, Target}; /// /// let num_items: usize = 4; /// const MENU_COUNT_ACTION: Selector = Selector::new("menu-count-action"); @@ -275,7 +275,7 @@ impl MenuDesc { /// .append_iter(|| (0..num_items).map(|i| { /// MenuItem::new( /// LocalizedString::new("hello-counter").with_arg("count", move |_, _| i.into()), - /// Command::new(MENU_COUNT_ACTION, i), + /// Command::new(MENU_COUNT_ACTION, i, Target::Auto), /// ) /// }) /// ); diff --git a/druid/src/scroll_component.rs b/druid/src/scroll_component.rs index aa862b8ab7..92ba7c3274 100644 --- a/druid/src/scroll_component.rs +++ b/druid/src/scroll_component.rs @@ -423,6 +423,31 @@ impl ScrollComponent { self.scrollbars.timer_id = TimerToken::INVALID; ctx.set_handled(); } + Event::AnimFrame(interval) => { + // Guard by the timer id being invalid, otherwise the scroll bars would fade + // immediately if some other widget started animating. + if self.scrollbars.timer_id == TimerToken::INVALID { + // Animate scroll bars opacity + let diff = 2.0 * (*interval as f64) * 1e-9; + self.scrollbars.opacity -= diff; + if self.scrollbars.opacity > 0.0 { + ctx.request_anim_frame(); + } + + let viewport = ctx.size().to_rect(); + if viewport.width() < self.content_size.width { + ctx.request_paint_rect( + self.calc_horizontal_bar_bounds(viewport, env) - self.scroll_offset, + ); + } + if viewport.height() < self.content_size.height { + ctx.request_paint_rect( + self.calc_vertical_bar_bounds(viewport, env) - self.scroll_offset, + ); + } + } + } + _ => (), } } @@ -445,38 +470,9 @@ impl ScrollComponent { /// /// Make sure to call on every lifecycle event pub fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, env: &Env) { - match event { - LifeCycle::AnimFrame(interval) => { - // Guard by the timer id being invalid, otherwise the scroll bars would fade - // immediately if some other widget started animating. - if self.scrollbars.timer_id == TimerToken::INVALID { - // Animate scroll bars opacity - let diff = 2.0 * (*interval as f64) * 1e-9; - self.scrollbars.opacity -= diff; - if self.scrollbars.opacity > 0.0 { - ctx.request_anim_frame(); - } - - let viewport = ctx.size().to_rect(); - if viewport.width() < self.content_size.width { - ctx.request_paint_rect( - self.calc_horizontal_bar_bounds(viewport, env) - self.scroll_offset, - ); - } - if viewport.height() < self.content_size.height { - ctx.request_paint_rect( - self.calc_vertical_bar_bounds(viewport, env) - self.scroll_offset, - ); - } - } - } - + if let LifeCycle::Size(_) = event { // Show the scrollbars any time our size changes - LifeCycle::Size(_) => { - self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env); - } - - _ => {} + self.reset_scrollbar_fade(|d| ctx.request_timer(d), &env); } } diff --git a/druid/src/tests/harness.rs b/druid/src/tests/harness.rs index 7eb21b23d2..76ca91e84e 100644 --- a/druid/src/tests/harness.rs +++ b/druid/src/tests/harness.rs @@ -214,9 +214,9 @@ impl Harness<'_, T> { } /// Send a command to a target. - pub fn submit_command(&mut self, cmd: impl Into, target: impl Into>) { - let target = target.into().unwrap_or_else(|| self.inner.window.id.into()); - let event = Event::Internal(InternalEvent::TargetedCommand(target, cmd.into())); + pub fn submit_command(&mut self, cmd: impl Into) { + let command = cmd.into().default_to(self.inner.window.id); + let event = Event::Internal(InternalEvent::TargetedCommand(command)); self.event(event); } @@ -243,9 +243,7 @@ impl Harness<'_, T> { loop { let cmd = self.inner.cmds.pop_front(); match cmd { - Some((target, cmd)) => { - self.event(Event::Internal(InternalEvent::TargetedCommand(target, cmd))) - } + Some(cmd) => self.event(Event::Internal(InternalEvent::TargetedCommand(cmd))), None => break, } } diff --git a/druid/src/tests/mod.rs b/druid/src/tests/mod.rs index 7bf36d4f8c..ffc1f9dde6 100644 --- a/druid/src/tests/mod.rs +++ b/druid/src/tests/mod.rs @@ -204,26 +204,26 @@ fn take_focus() { assert!(right_focus.get().is_none()); // this is sent to all widgets; the last widget to request focus should get it - harness.submit_command(TAKE_FOCUS, None); + harness.submit_command(TAKE_FOCUS); assert_eq!(harness.window().focus, Some(id_2)); assert_eq!(left_focus.get(), None); assert_eq!(right_focus.get(), Some(true)); // this is sent to all widgets; the last widget to request focus should still get it // NOTE: This tests siblings in particular, so careful when moving away from Split. - harness.submit_command(TAKE_FOCUS, None); + harness.submit_command(TAKE_FOCUS); assert_eq!(harness.window().focus, Some(id_2)); assert_eq!(left_focus.get(), None); assert_eq!(right_focus.get(), Some(true)); // this is sent to a specific widget; it should get focus - harness.submit_command(TAKE_FOCUS, id_1); + harness.submit_command(TAKE_FOCUS.to(id_1)); assert_eq!(harness.window().focus, Some(id_1)); assert_eq!(left_focus.get(), Some(true)); assert_eq!(right_focus.get(), Some(false)); // this is sent to a specific widget; it should get focus - harness.submit_command(TAKE_FOCUS, id_2); + harness.submit_command(TAKE_FOCUS.to(id_2)); assert_eq!(harness.window().focus, Some(id_2)); assert_eq!(left_focus.get(), Some(false)); assert_eq!(right_focus.get(), Some(true)); @@ -291,42 +291,42 @@ fn focus_changed() { harness.send_initial_events(); // focus none -> a - harness.submit_command(TAKE_FOCUS, id_a); + harness.submit_command(TAKE_FOCUS.to(id_a)); assert_eq!(harness.window().focus, Some(id_a)); assert!(changed(&a_rec, true)); assert!(no_change(&b_rec)); assert!(no_change(&c_rec)); // focus a -> b - harness.submit_command(TAKE_FOCUS, id_b); + harness.submit_command(TAKE_FOCUS.to(id_b)); assert_eq!(harness.window().focus, Some(id_b)); assert!(changed(&a_rec, false)); assert!(changed(&b_rec, true)); assert!(no_change(&c_rec)); // focus b -> c - harness.submit_command(TAKE_FOCUS, id_c); + harness.submit_command(TAKE_FOCUS.to(id_c)); assert_eq!(harness.window().focus, Some(id_c)); assert!(no_change(&a_rec)); assert!(changed(&b_rec, false)); assert!(changed(&c_rec, true)); // focus c -> a - harness.submit_command(TAKE_FOCUS, id_a); + harness.submit_command(TAKE_FOCUS.to(id_a)); assert_eq!(harness.window().focus, Some(id_a)); assert!(changed(&a_rec, true)); assert!(no_change(&b_rec)); assert!(changed(&c_rec, false)); // all focus before passing down the event - harness.submit_command(ALL_TAKE_FOCUS_BEFORE, None); + harness.submit_command(ALL_TAKE_FOCUS_BEFORE); assert_eq!(harness.window().focus, Some(id_c)); assert!(changed(&a_rec, false)); assert!(no_change(&b_rec)); assert!(changed(&c_rec, true)); // all focus after passing down the event - harness.submit_command(ALL_TAKE_FOCUS_AFTER, None); + harness.submit_command(ALL_TAKE_FOCUS_AFTER); assert_eq!(harness.window().focus, Some(id_a)); assert!(changed(&a_rec, true)); assert!(no_change(&b_rec)); @@ -370,7 +370,7 @@ fn adding_child_lifecycle() { assert!(record_new_child.is_empty()); - harness.submit_command(REPLACE_CHILD, None); + harness.submit_command(REPLACE_CHILD); assert!(matches!(record.next(), Record::E(Event::Command(_)))); @@ -410,7 +410,7 @@ fn participate_in_autofocus() { assert_eq!(harness.window().focus_chain(), &[id_1, id_2, id_3, id_4]); // tell the replacer widget to swap its children - harness.submit_command(REPLACE_CHILD, None); + harness.submit_command(REPLACE_CHILD); // verify that the two new children are registered for focus. assert_eq!( @@ -471,7 +471,7 @@ fn register_after_adding_child() { assert!(harness.get_state(id_5).children.may_contain(&id_4)); assert_eq!(harness.get_state(id_5).children.entry_count(), 3); - harness.submit_command(REPLACE_CHILD, None); + harness.submit_command(REPLACE_CHILD); assert!(harness.get_state(id_5).children.may_contain(&id_6)); assert!(harness.get_state(id_5).children.may_contain(&id_4)); @@ -501,7 +501,7 @@ fn request_update() { Harness::create_simple((), widget, |harness| { harness.send_initial_events(); assert!(!updated.get()); - harness.submit_command(REQUEST_UPDATE, None); + harness.submit_command(REQUEST_UPDATE); assert!(updated.get()); }) } diff --git a/druid/src/text/editable_text.rs b/druid/src/text/editable_text.rs index 3fe70d8089..2efbc62c65 100644 --- a/druid/src/text/editable_text.rs +++ b/druid/src/text/editable_text.rs @@ -58,6 +58,12 @@ pub trait EditableText: Sized { /// Get the next codepoint offset from the given offset, if it exists. fn next_codepoint_offset(&self, offset: usize) -> Option; + /// Get the preceding line break offset from the given offset + fn preceding_line_break(&self, offset: usize) -> usize; + + /// Get the next line break offset from the given offset + fn next_line_break(&self, offset: usize) -> usize; + /// Returns `true` if this text has 0 length. fn is_empty(&self) -> bool; @@ -156,6 +162,32 @@ impl EditableText for String { fn from_str(s: &str) -> Self { s.to_string() } + + fn preceding_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for byte in self.get(0..from).unwrap_or("").bytes().rev() { + if byte == 0x0a { + return offset; + } + offset -= 1; + } + + 0 + } + + fn next_line_break(&self, from: usize) -> usize { + let mut offset = from; + + for char in self.get(from..).unwrap_or("").bytes() { + if char == 0x0a { + return offset; + } + offset += 1; + } + + self.len() + } } /// A cursor with convenience functions for moving through EditableText. @@ -410,4 +442,38 @@ mod tests { assert_eq!(Some(35), a.next_word_offset(26)); assert_eq!(Some(35), a.next_word_offset(35)); } + + #[test] + fn preceding_line_break() { + let a = String::from("Technically\na word:\n ৬藏A\u{030a}\n\u{110b}\u{1161}"); + assert_eq!(0, a.preceding_line_break(0)); + assert_eq!(0, a.preceding_line_break(11)); + assert_eq!(12, a.preceding_line_break(12)); + assert_eq!(12, a.preceding_line_break(13)); + assert_eq!(20, a.preceding_line_break(21)); + assert_eq!(31, a.preceding_line_break(31)); + assert_eq!(31, a.preceding_line_break(34)); + + let b = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}"); + assert_eq!(0, b.preceding_line_break(0)); + assert_eq!(0, b.preceding_line_break(11)); + assert_eq!(0, b.preceding_line_break(13)); + assert_eq!(0, b.preceding_line_break(21)); + } + + #[test] + fn next_line_break() { + let a = String::from("Technically\na word:\n ৬藏A\u{030a}\n\u{110b}\u{1161}"); + assert_eq!(11, a.next_line_break(0)); + assert_eq!(11, a.next_line_break(11)); + assert_eq!(19, a.next_line_break(13)); + assert_eq!(30, a.next_line_break(21)); + assert_eq!(a.len(), a.next_line_break(31)); + + let b = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}"); + assert_eq!(b.len(), b.next_line_break(0)); + assert_eq!(b.len(), b.next_line_break(11)); + assert_eq!(b.len(), b.next_line_break(13)); + assert_eq!(b.len(), b.next_line_break(19)); + } } diff --git a/druid/src/text/layout.rs b/druid/src/text/layout.rs new file mode 100644 index 0000000000..faf6f78ab3 --- /dev/null +++ b/druid/src/text/layout.rs @@ -0,0 +1,267 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A type for laying out, drawing, and interacting with text. + +use std::ops::Range; + +use crate::kurbo::{Line, Point, Rect, Size}; +use crate::piet::{ + Color, PietText, PietTextLayout, Text as _, TextAttribute, TextLayout as _, + TextLayoutBuilder as _, +}; +use crate::{ArcStr, Data, Env, FontDescriptor, KeyOrValue, PaintCtx, RenderContext}; + +/// A component for displaying text on screen. +/// +/// This is a type intended to be used by other widgets that display text. +/// It allows for the text itself as well as font and other styling information +/// to be set and modified. It wraps an inner layout object, and handles +/// invalidating and rebuilding it as required. +/// +/// This object is not valid until the [`rebuild_if_needed`] method has been +/// called. Additionally, this method must be called anytime the text or +/// other properties have changed, or if any items in the [`Env`] that are +/// referenced in this layout change. In general, you should just call this +/// method as part of your widget's `update` method. +/// +/// [`rebuild_if_needed`]: #method.rebuild_if_needed +/// [`Env`]: struct.Env.html +#[derive(Clone)] +pub struct TextLayout { + text: ArcStr, + font: KeyOrValue, + text_size_override: Option>, + text_color: KeyOrValue, + //FIXME: all this caching stuff can go away when we have a simple way of + // checking if something has changed in the env. + cached_text_color: Color, + cached_font: FontDescriptor, + // when set, this will be used to override the size in he font descriptor. + // This provides an easy way to change only the font size, while still + // using a `FontDescriptor` in the `Env`. + cached_text_size: Option, + // the underlying layout object. This is constructed lazily. + layout: Option, +} + +impl TextLayout { + /// Create a new `TextLayout` object. + /// + /// You do not provide the actual text at creation time; instead you pass + /// it in when calling [`rebuild_if_needed`]. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn new(text: impl Into) -> Self { + TextLayout { + text: text.into(), + font: crate::theme::UI_FONT.into(), + cached_font: Default::default(), + text_color: crate::theme::LABEL_COLOR.into(), + cached_text_color: Color::BLACK, + text_size_override: None, + cached_text_size: None, + layout: None, + } + } + + /// Returns `true` if this layout needs to be rebuilt. + /// + /// This happens (for instance) after style attributes are modified. + /// + /// This does not account for things like the text changing, handling that + /// is the responsibility of the user. + pub fn needs_rebuild(&self) -> bool { + self.layout.is_none() + } + + /// Set the text to display. + pub fn set_text(&mut self, text: impl Into) { + self.text = text.into(); + self.layout = None; + } + + /// Set the default text color for this layout. + pub fn set_text_color(&mut self, color: impl Into>) { + self.text_color = color.into(); + self.layout = None; + } + + /// Set the default font. + /// + /// The argument is a [`FontDescriptor`] or a [`Key`] that + /// can be resolved from the [`Env`]. + /// + /// [`FontDescriptor`]: struct.FontDescriptor.html + /// [`Env`]: struct.Env.html + /// [`Key`]: struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.font = font.into(); + self.layout = None; + self.text_size_override = None; + } + + /// Set the font size. + /// + /// This overrides the size in the [`FontDescriptor`] provided to [`set_font`]. + /// + /// [`set_font`]: #method.set_font.html + /// [`FontDescriptor`]: struct.FontDescriptor.html + pub fn set_text_size(&mut self, size: impl Into>) { + self.text_size_override = Some(size.into()); + self.layout = None; + } + + /// The size of the laid-out text. + /// + /// This is not meaningful until [`rebuild_if_needed`] has been called. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn size(&self) -> Size { + self.layout + .as_ref() + .map(|layout| layout.size()) + .unwrap_or_default() + } + + /// For a given `Point` (relative to this object's origin), returns index + /// into the underlying text of the nearest grapheme boundary. + pub fn text_position_for_point(&self, point: Point) -> usize { + self.layout + .as_ref() + .map(|layout| layout.hit_test_point(point).idx) + .unwrap_or_default() + } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return the `Point` (relative to this object's origin) representing the + /// boundary of the containing grapheme. + /// + /// # Panics + /// + /// Panics if `text_pos` is not a character boundary. + pub fn point_for_text_position(&self, text_pos: usize) -> Point { + self.layout + .as_ref() + .map(|layout| layout.hit_test_text_position(text_pos).point) + .unwrap_or_default() + } + + /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s + /// representing the nominal bounding boxes of the text in that range. + /// + /// # Panics + /// + /// Panics if the range start or end is not a character boundary. + pub fn rects_for_range(&self, range: Range) -> Vec { + self.layout + .as_ref() + .map(|layout| layout.rects_for_range(range)) + .unwrap_or_default() + } + + /// Given the utf-8 position of a character boundary in the underlying text, + /// return a `Line` suitable for drawing a vertical cursor at that boundary. + pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line { + self.layout + .as_ref() + .map(|layout| { + let pos = layout.hit_test_text_position(text_pos); + let line_metrics = layout.line_metric(pos.line).unwrap(); + let p1 = (pos.point.x, line_metrics.y_offset); + let p2 = (pos.point.x, (line_metrics.y_offset + line_metrics.height)); + dbg!(Line::new(p1, p2)) + }) + .unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO)) + } + + /// Called during the containing widgets `update` method; this text object + /// will check to see if any used environment items have changed, + /// and invalidate itself as needed. + /// + /// Returns `true` if an item has changed, indicating that the text object + /// needs layout. + /// + /// # Note + /// + /// After calling this method, the layout may be invalid until the next call + /// to [`rebuild_layout_if_needed`], [`layout`], or [`paint`]. + /// + /// [`layout`]: #method.layout + /// [`paint`]: #method.paint + /// [`rebuild_layout_if_needed`]: #method.rebuild_layout_if_needed + pub fn rebuild_if_needed(&mut self, factory: &mut PietText, env: &Env) { + let new_font = self.font.resolve(env); + let new_color = self.text_color.resolve(env); + let new_size = self.text_size_override.as_ref().map(|key| key.resolve(env)); + + let needs_rebuild = !new_font.same(&self.cached_font) + || !new_color.same(&self.cached_text_color) + || new_size != self.cached_text_size + || self.layout.is_none(); + + self.cached_font = new_font; + self.cached_text_color = new_color; + self.cached_text_size = new_size; + + if needs_rebuild { + let descriptor = if let Some(size) = &self.cached_text_size { + self.cached_font.clone().with_size(*size) + } else { + self.cached_font.clone() + }; + let text_color = self.cached_text_color.clone(); + self.layout = Some( + factory + .new_text_layout(self.text.clone()) + .font(descriptor.family.clone(), descriptor.size) + .default_attribute(descriptor.weight) + .default_attribute(descriptor.style) + .default_attribute(TextAttribute::ForegroundColor(text_color)) + .build() + .unwrap(), + ) + } + } + + /// Draw the layout at the provided `Point`. + /// + /// The origin of the layout is the top-left corner. + /// + /// You must call [`rebuild_if_needed`] at some point before you first + /// call this method. + /// + /// [`rebuild_if_needed`]: #method.rebuild_if_needed + pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into) { + ctx.draw_text(self.layout.as_ref().unwrap(), point) + } +} + +impl std::fmt::Debug for TextLayout { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("TextLayout") + .field("font", &self.font) + .field("text_size_override", &self.text_size_override) + .field("text_color", &self.text_color) + .field( + "layout", + if self.layout.is_some() { + &"Some" + } else { + &"None" + }, + ) + .finish() + } +} diff --git a/druid/src/text/mod.rs b/druid/src/text/mod.rs index 3ff83efa29..35617ae128 100644 --- a/druid/src/text/mod.rs +++ b/druid/src/text/mod.rs @@ -17,6 +17,7 @@ pub mod backspace; mod editable_text; mod font_descriptor; +mod layout; pub mod movement; pub mod selection; mod text_input; @@ -24,6 +25,7 @@ mod text_input; pub use self::backspace::offset_for_delete_backwards; pub use self::editable_text::{EditableText, EditableTextCursor, StringCursor}; pub use self::font_descriptor::FontDescriptor; +pub use self::layout::TextLayout; pub use self::movement::{movement, Movement}; pub use self::selection::Selection; pub use self::text_input::{BasicTextInput, EditAction, MouseAction, TextInput}; diff --git a/druid/src/text/movement.rs b/druid/src/text/movement.rs index c16496d193..6e1d9da52e 100644 --- a/druid/src/text/movement.rs +++ b/druid/src/text/movement.rs @@ -28,9 +28,13 @@ pub enum Movement { /// Move to the right by one word. RightWord, /// Move to left end of visible line. - LeftOfLine, + PrecedingLineBreak, /// Move to right end of visible line. - RightOfLine, + NextLineBreak, + /// Move to the beginning of the document + StartOfDocument, + /// Move to the end of the document + EndOfDocument, } /// Compute the result of movement on a selection . @@ -51,8 +55,11 @@ pub fn movement(m: Movement, s: Selection, text: &impl EditableText, modify: boo } } - Movement::LeftOfLine => 0, - Movement::RightOfLine => text.len(), + Movement::PrecedingLineBreak => text.preceding_line_break(s.end), + Movement::NextLineBreak => text.next_line_break(s.end), + + Movement::StartOfDocument => 0, + Movement::EndOfDocument => text.len(), Movement::LeftWord => { if s.is_caret() || modify { diff --git a/druid/src/text/text_input.rs b/druid/src/text/text_input.rs index f7d5a1dcd5..523045fc75 100644 --- a/druid/src/text/text_input.rs +++ b/druid/src/text/text_input.rs @@ -80,11 +80,11 @@ impl TextInput for BasicTextInput { } // Select to home (Shift+Home) k_e if (HotKey::new(SysMods::Shift, KbKey::Home)).matches(k_e) => { - EditAction::ModifySelection(Movement::LeftOfLine) + EditAction::ModifySelection(Movement::PrecedingLineBreak) } // Select to end (Shift+End) k_e if (HotKey::new(SysMods::Shift, KbKey::End)).matches(k_e) => { - EditAction::ModifySelection(Movement::RightOfLine) + EditAction::ModifySelection(Movement::NextLineBreak) } // Select left (Shift+ArrowLeft) k_e if (HotKey::new(SysMods::Shift, KbKey::ArrowLeft)).matches(k_e) => { @@ -126,11 +126,11 @@ impl TextInput for BasicTextInput { k_e if (HotKey::new(None, KbKey::Delete)).matches(k_e) => EditAction::Delete, // Home k_e if (HotKey::new(None, KbKey::Home)).matches(k_e) => { - EditAction::Move(Movement::LeftOfLine) + EditAction::Move(Movement::PrecedingLineBreak) } // End k_e if (HotKey::new(None, KbKey::End)).matches(k_e) => { - EditAction::Move(Movement::RightOfLine) + EditAction::Move(Movement::NextLineBreak) } // Actual typing k_e if key_event_is_printable(k_e) => { diff --git a/druid/src/theme.rs b/druid/src/theme.rs index 8012eeaaa3..4510008d00 100644 --- a/druid/src/theme.rs +++ b/druid/src/theme.rs @@ -17,7 +17,7 @@ #![allow(missing_docs)] use crate::piet::Color; -use crate::{ArcStr, Env, Key}; +use crate::{Env, FontDescriptor, FontFamily, Key}; pub const WINDOW_BACKGROUND_COLOR: Key = Key::new("window_background_color"); @@ -41,11 +41,13 @@ pub const SELECTION_COLOR: Key = Key::new("selection_color"); pub const SELECTION_TEXT_COLOR: Key = Key::new("selection_text_color"); pub const CURSOR_COLOR: Key = Key::new("cursor_color"); -pub const FONT_NAME: Key = Key::new("font_name"); pub const TEXT_SIZE_NORMAL: Key = Key::new("text_size_normal"); pub const TEXT_SIZE_LARGE: Key = Key::new("text_size_large"); pub const BASIC_WIDGET_HEIGHT: Key = Key::new("basic_widget_height"); +/// The default font for labels, buttons, text boxes, and other UI elements. +pub const UI_FONT: Key = Key::new("druid.builtin.ui-font-descriptor"); + /// The default minimum width for a 'wide' widget; a textbox, slider, progress bar, etc. pub const WIDE_WIDGET_WIDTH: Key = Key::new("druid.widgets.long-widget-width"); pub const BORDERED_WIDGET_HEIGHT: Key = Key::new("bordered_widget_height"); @@ -63,7 +65,7 @@ pub const SCROLLBAR_EDGE_WIDTH: Key = Key::new("scrollbar_edge_width"); /// An initial theme. pub fn init() -> Env { - let mut env = Env::default() + Env::default() .adding(WINDOW_BACKGROUND_COLOR, Color::rgb8(0x29, 0x29, 0x29)) .adding(LABEL_COLOR, Color::rgb8(0xf0, 0xf0, 0xea)) .adding(PLACEHOLDER_COLOR, Color::rgb8(0x80, 0x80, 0x80)) @@ -96,21 +98,9 @@ pub fn init() -> Env { .adding(SCROLLBAR_WIDTH, 8.) .adding(SCROLLBAR_PAD, 2.) .adding(SCROLLBAR_RADIUS, 5.) - .adding(SCROLLBAR_EDGE_WIDTH, 1.); - - #[cfg(target_os = "windows")] - { - env = env.adding(FONT_NAME, "Segoe UI"); - } - #[cfg(target_os = "macos")] - { - // Ideally this would be a reference to San Francisco, but Cairo's - // "toy text" API doesn't seem to be able to access it easily. - env = env.adding(FONT_NAME, "Arial"); - } - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - { - env = env.adding(FONT_NAME, "sans-serif"); - } - env + .adding(SCROLLBAR_EDGE_WIDTH, 1.) + .adding( + UI_FONT, + FontDescriptor::new(FontFamily::SYSTEM_UI).with_size(15.0), + ) } diff --git a/druid/src/widget/checkbox.rs b/druid/src/widget/checkbox.rs index 0891f979a4..24ac045fcb 100644 --- a/druid/src/widget/checkbox.rs +++ b/druid/src/widget/checkbox.rs @@ -25,6 +25,7 @@ use crate::{ /// A checkbox that toggles a `bool`. pub struct Checkbox { + //FIXME: this should be a TextUi struct child_label: WidgetPod>>, } @@ -61,13 +62,15 @@ impl Widget for Checkbox { } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &bool, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &bool, env: &Env) { + self.child_label.lifecycle(ctx, event, data, env); if let LifeCycle::HotChanged(_) = event { ctx.request_paint(); } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &bool, _data: &bool, _env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &bool, data: &bool, env: &Env) { + self.child_label.update(ctx, data, env); ctx.request_paint(); } diff --git a/druid/src/widget/click.rs b/druid/src/widget/click.rs index 8f577a448d..704cc0047e 100644 --- a/druid/src/widget/click.rs +++ b/druid/src/widget/click.rs @@ -17,7 +17,7 @@ //! [`Controller`]: struct.Controller.html use crate::widget::Controller; -use crate::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, Widget}; +use crate::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, MouseButton, Widget}; /// A clickable [`Controller`] widget. Pass this and a child widget to a /// [`ControllerHost`] to make the child interactive. More conveniently, this is @@ -52,12 +52,14 @@ impl Click { impl> Controller for Click { fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { match event { - Event::MouseDown(_) => { - ctx.set_active(true); - ctx.request_paint(); + Event::MouseDown(mouse_event) => { + if mouse_event.button == MouseButton::Left { + ctx.set_active(true); + ctx.request_paint(); + } } - Event::MouseUp(_) => { - if ctx.is_active() { + Event::MouseUp(mouse_event) => { + if ctx.is_active() && mouse_event.button == MouseButton::Left { ctx.set_active(false); if ctx.is_hot() { (self.action)(ctx, data, env); diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 688d663b35..68db81125c 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -14,13 +14,10 @@ //! A label widget. -use crate::piet::{ - Color, FontFamily, PietText, PietTextLayout, RenderContext, Text, TextLayout, - TextLayoutBuilder, UnitPoint, -}; +use crate::piet::{Color, PietText, UnitPoint}; use crate::{ - theme, ArcStr, BoxConstraints, Data, Env, Event, EventCtx, KeyOrValue, LayoutCtx, LifeCycle, - LifeCycleCtx, LocalizedString, PaintCtx, Point, Size, UpdateCtx, Widget, + BoxConstraints, Data, Env, Event, EventCtx, FontDescriptor, KeyOrValue, LayoutCtx, LifeCycle, + LifeCycleCtx, LocalizedString, PaintCtx, Point, Size, TextLayout, UpdateCtx, Widget, }; // added padding between the edges of the widget and the text. @@ -53,9 +50,10 @@ pub struct Dynamic { /// A label that displays some text. pub struct Label { text: LabelText, - color: KeyOrValue, - size: KeyOrValue, - font: KeyOrValue, + layout: TextLayout, + // if our text is manually changed we need to rebuild the layout + // before using it again. + needs_update_text: bool, } impl Label { @@ -77,11 +75,11 @@ impl Label { /// ``` pub fn new(text: impl Into>) -> Self { let text = text.into(); + let layout = TextLayout::new(text.display_text()); Self { text, - color: theme::LABEL_COLOR.into(), - size: theme::TEXT_SIZE_NORMAL.into(), - font: theme::FONT_NAME.into(), + layout, + needs_update_text: true, } } @@ -122,7 +120,7 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn with_text_color(mut self, color: impl Into>) -> Self { - self.color = color.into(); + self.set_text_color(color); self } @@ -132,31 +130,27 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn with_text_size(mut self, size: impl Into>) -> Self { - self.size = size.into(); + self.set_text_size(size); self } /// Builder-style method for setting the font. /// - /// The argument can be a `&str`, `String`, or [`Key<&str>`]. + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. /// - /// [`Key<&str>`]: ../struct.Key.html - pub fn with_font(mut self, font: impl Into>) -> Self { - self.font = font.into(); + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn with_font(mut self, font: impl Into>) -> Self { + self.set_font(font); self } - /// Set a new text. - /// - /// Takes an already resolved string as input. - /// - /// If you're looking for full [`LabelText`] support, - /// then you need to create a new [`Label`]. - /// - /// [`Label`]: #method.new - /// [`LabelText`]: enum.LabelText.html - pub fn set_text(&mut self, text: impl Into) { - self.text = LabelText::Specific(text.into()); + /// Set the label's text. + pub fn set_text(&mut self, text: impl Into>) { + self.text = text.into(); + self.needs_update_text = true; } /// Returns this label's current text. @@ -170,7 +164,7 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn set_text_color(&mut self, color: impl Into>) { - self.color = color.into(); + self.layout.set_text_color(color); } /// Set the text size. @@ -179,32 +173,28 @@ impl Label { /// /// [`Key`]: ../struct.Key.html pub fn set_text_size(&mut self, size: impl Into>) { - self.size = size.into(); + self.layout.set_text_size(size); } /// Set the font. /// - /// The argument can be a `&str`, `String`, or [`Key<&str>`]. + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. /// - /// [`Key<&str>`]: ../struct.Key.html - pub fn set_font(&mut self, font: impl Into>) { - self.font = font.into(); + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.layout.set_font(font); } - fn get_layout(&mut self, t: &mut PietText, env: &Env) -> PietTextLayout { - let font_name = self.font.resolve(env); - let font_size = self.size.resolve(env); - let color = self.color.resolve(env); - - // TODO: caching of both the format and the layout - self.text.with_display_text(|text| { - let font = t.font_family(&font_name).unwrap_or(FontFamily::SYSTEM_UI); - t.new_text_layout(&text) - .font(font, font_size) - .text_color(color.clone()) - .build() - .unwrap() - }) + fn update_text_if_needed(&mut self, factory: &mut PietText, data: &T, env: &Env) { + if self.needs_update_text { + self.text.resolve(data, env); + self.layout.set_text(self.text.display_text()); + self.layout.rebuild_if_needed(factory, env); + self.needs_update_text = false; + } } } @@ -252,36 +242,35 @@ impl LabelText { impl Widget for Label { fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {} - fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { if let LifeCycle::WidgetAdded = event { - self.text.resolve(data, env); + self.update_text_if_needed(&mut ctx.text(), data, env); } } fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { - if !old_data.same(data) && self.text.resolve(data, env) { + if !old_data.same(data) | self.text.resolve(data, env) { + self.layout.set_text(self.text.display_text()); ctx.request_layout(); } + //FIXME: this should only happen if the env has changed. + self.layout.rebuild_if_needed(&mut ctx.text(), env); } - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size { + fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, _env: &Env) -> Size { bc.debug_check("Label"); - let text_layout = self.get_layout(&mut ctx.text(), env); - let text_size = text_layout.size(); + let text_size = self.layout.size(); bc.constrain(Size::new( text_size.width + 2. * LABEL_X_PADDING, text_size.height, )) } - fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) { - let text_layout = self.get_layout(&mut ctx.text(), env); - + fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) { // Find the origin for the text let origin = Point::new(LABEL_X_PADDING, 0.0); - - ctx.draw_text(&text_layout, origin); + self.layout.draw(ctx, origin) } } diff --git a/druid/src/widget/radio.rs b/druid/src/widget/radio.rs index 9ca92ba547..eb1907fda6 100644 --- a/druid/src/widget/radio.rs +++ b/druid/src/widget/radio.rs @@ -43,6 +43,7 @@ impl RadioGroup { /// A single radio button pub struct Radio { variant: T, + //FIXME: this should be using a TextUi struct child_label: WidgetPod>>, } @@ -76,14 +77,18 @@ impl Widget for Radio { } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.child_label.lifecycle(ctx, event, data, env); if let LifeCycle::HotChanged(_) = event { ctx.request_paint(); } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) { - ctx.request_paint(); + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + self.child_label.update(ctx, data, env); + if !old_data.same(data) { + ctx.request_paint(); + } } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index e1453c6c6e..6eeaa52090 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -92,6 +92,13 @@ impl> Scroll { pub fn offset(&self) -> Vec2 { self.scroll_component.scroll_offset } + + /// Scroll `delta` units. + /// + /// Returns `true` if the scroll offset has changed. + pub fn scroll(&mut self, delta: Vec2, layout_size: Size) -> bool { + self.scroll_component.scroll(delta, layout_size) + } } impl> Widget for Scroll { @@ -136,7 +143,7 @@ impl> Widget for Scroll { self.child .set_layout_rect(ctx, data, env, child_size.to_rect()); - let self_size = bc.constrain(max_bc); + let self_size = bc.constrain(child_size); let _ = self.scroll_component.scroll(Vec2::new(0.0, 0.0), self_size); self_size } diff --git a/druid/src/widget/spinner.rs b/druid/src/widget/spinner.rs index 88d98ffb41..c85396b017 100644 --- a/druid/src/widget/spinner.rs +++ b/druid/src/widget/spinner.rs @@ -67,15 +67,8 @@ impl Default for Spinner { } impl Widget for Spinner { - fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T, _env: &Env) {} - - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) { - if let LifeCycle::WidgetAdded = event { - ctx.request_anim_frame(); - ctx.request_paint(); - } - - if let LifeCycle::AnimFrame(interval) = event { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &Env) { + if let Event::AnimFrame(interval) = event { self.t += (*interval as f64) * 1e-9; if self.t >= 1.0 { self.t = 0.0; @@ -85,6 +78,13 @@ impl Widget for Spinner { } } + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &T, _env: &Env) { + if let LifeCycle::WidgetAdded = event { + ctx.request_anim_frame(); + ctx.request_paint(); + } + } + fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) {} fn layout( diff --git a/druid/src/widget/switch.rs b/druid/src/widget/switch.rs index f09cbab3d3..415a1aa581 100644 --- a/druid/src/widget/switch.rs +++ b/druid/src/widget/switch.rs @@ -17,13 +17,11 @@ use std::time::Duration; use crate::kurbo::{Circle, Point, Shape, Size}; -use crate::piet::{ - FontFamily, LinearGradient, RenderContext, Text, TextLayout, TextLayoutBuilder, UnitPoint, -}; +use crate::piet::{LinearGradient, RenderContext, UnitPoint}; use crate::theme; use crate::{ - BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, UpdateCtx, - Widget, + BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, TextLayout, + UpdateCtx, Widget, }; const SWITCH_CHANGE_TIME: f64 = 0.2; @@ -31,12 +29,28 @@ const SWITCH_PADDING: f64 = 3.; const SWITCH_WIDTH_RATIO: f64 = 2.75; /// A switch that toggles a `bool`. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Switch { knob_pos: Point, knob_hovered: bool, knob_dragged: bool, animation_in_progress: bool, + on_text: TextLayout, + off_text: TextLayout, +} + +impl Default for Switch { + fn default() -> Self { + Switch { + knob_pos: Point::ZERO, + knob_hovered: false, + knob_dragged: false, + animation_in_progress: false, + //TODO: use localized strings, also probably make these configurable? + on_text: TextLayout::new("ON"), + off_text: TextLayout::new("OFF"), + } + } } impl Switch { @@ -51,51 +65,27 @@ impl Switch { } fn paint_labels(&mut self, ctx: &mut PaintCtx, env: &Env, switch_width: f64) { - let font_name = env.get(theme::FONT_NAME); - let font_size = env.get(theme::TEXT_SIZE_NORMAL); - let text_color = env.get(theme::LABEL_COLOR); let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT); let knob_size = switch_height - 2. * SWITCH_PADDING; - let font = ctx - .text() - .font_family(&font_name) - .unwrap_or(FontFamily::SYSTEM_UI); - - // off/on labels - // TODO: use LocalizedString - let on_label_layout = ctx - .text() - .new_text_layout("ON") - .font(font.clone(), font_size) - .text_color(text_color.clone()) - .build() - .unwrap(); - - let off_label_layout = ctx - .text() - .new_text_layout("OFF") - .font(font, font_size) - .text_color(text_color) - .build() - .unwrap(); - - let label_y = (switch_height - on_label_layout.size().height).max(0.0) / 2.0; + let on_size = self.on_text.size(); + let off_size = self.off_text.size(); + + let label_y = (switch_height - on_size.height).max(0.0) / 2.0; let label_x_space = switch_width - knob_size - SWITCH_PADDING * 2.0; let off_pos = knob_size / 2. + SWITCH_PADDING; let knob_delta = self.knob_pos.x - off_pos; - let on_label_width = on_label_layout.size().width; + let on_label_width = on_size.width; let on_base_x_pos = -on_label_width - (label_x_space - on_label_width) / 2.0 + SWITCH_PADDING; let on_label_origin = Point::new(on_base_x_pos + knob_delta, label_y); - let off_base_x_pos = - knob_size + (label_x_space - off_label_layout.size().width) / 2.0 + SWITCH_PADDING; + let off_base_x_pos = knob_size + (label_x_space - off_size.width) / 2.0 + SWITCH_PADDING; let off_label_origin = Point::new(off_base_x_pos + knob_delta, label_y); - ctx.draw_text(&on_label_layout, on_label_origin); - ctx.draw_text(&off_label_layout, off_label_origin); + self.on_text.draw(ctx, on_label_origin); + self.off_text.draw(ctx, off_label_origin); } } @@ -137,37 +127,38 @@ impl Widget for Switch { } ctx.request_paint(); } + Event::AnimFrame(interval) => { + let delta = Duration::from_nanos(*interval).as_secs_f64(); + let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT); + let switch_width = switch_height * SWITCH_WIDTH_RATIO; + let knob_size = switch_height - 2. * SWITCH_PADDING; + let on_pos = switch_width - knob_size / 2. - SWITCH_PADDING; + let off_pos = knob_size / 2. + SWITCH_PADDING; + + // move knob to right position depending on the value + if self.animation_in_progress { + let change_time = if *data { + SWITCH_CHANGE_TIME + } else { + -SWITCH_CHANGE_TIME + }; + let change = (switch_width / change_time) * delta; + self.knob_pos.x = (self.knob_pos.x + change).min(on_pos).max(off_pos); + + if (self.knob_pos.x > off_pos && !*data) || (self.knob_pos.x < on_pos && *data) + { + ctx.request_anim_frame(); + } else { + self.animation_in_progress = false; + } + ctx.request_paint(); + } + } _ => (), } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &bool, env: &Env) { - if let LifeCycle::AnimFrame(interval) = event { - let delta = Duration::from_nanos(*interval).as_secs_f64(); - let switch_height = env.get(theme::BORDERED_WIDGET_HEIGHT); - let switch_width = switch_height * SWITCH_WIDTH_RATIO; - let knob_size = switch_height - 2. * SWITCH_PADDING; - let on_pos = switch_width - knob_size / 2. - SWITCH_PADDING; - let off_pos = knob_size / 2. + SWITCH_PADDING; - - // move knob to right position depending on the value - if self.animation_in_progress { - let change_time = if *data { - SWITCH_CHANGE_TIME - } else { - -SWITCH_CHANGE_TIME - }; - let change = (switch_width / change_time) * delta; - self.knob_pos.x = (self.knob_pos.x + change).min(on_pos).max(off_pos); - - if (self.knob_pos.x > off_pos && !*data) || (self.knob_pos.x < on_pos && *data) { - ctx.request_anim_frame(); - } else { - self.animation_in_progress = false; - } - ctx.request_paint(); - } - } + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &bool, _env: &Env) { } fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool, _env: &Env) { @@ -177,13 +168,7 @@ impl Widget for Switch { } } - fn layout( - &mut self, - _layout_ctx: &mut LayoutCtx, - bc: &BoxConstraints, - _data: &bool, - env: &Env, - ) -> Size { + fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints, _: &bool, env: &Env) -> Size { let width = env.get(theme::BORDERED_WIDGET_HEIGHT) * SWITCH_WIDTH_RATIO; bc.constrain(Size::new(width, env.get(theme::BORDERED_WIDGET_HEIGHT))) } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index a2b7084874..a28bfa4e82 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -16,26 +16,22 @@ use std::time::Duration; +use crate::widget::prelude::*; use crate::{ - Application, BoxConstraints, Cursor, Env, Event, EventCtx, HotKey, KbKey, LayoutCtx, LifeCycle, - LifeCycleCtx, PaintCtx, Selector, SysMods, TimerToken, UpdateCtx, Widget, + Application, BoxConstraints, Cursor, Data, Env, FontDescriptor, HotKey, KbKey, KeyOrValue, + Selector, SysMods, TimerToken, }; -use crate::kurbo::{Affine, Line, Point, Size, Vec2}; -use crate::piet::{ - FontFamily, PietText, PietTextLayout, RenderContext, Text, TextAttribute, TextLayout, - TextLayoutBuilder, -}; +use crate::kurbo::{Affine, Insets, Point, Size}; use crate::theme; use crate::text::{ movement, offset_for_delete_backwards, BasicTextInput, EditAction, EditableText, MouseAction, - Movement, Selection, TextInput, + Movement, Selection, TextInput, TextLayout, }; const BORDER_WIDTH: f64 = 1.; -const PADDING_TOP: f64 = 5.; -const PADDING_LEFT: f64 = 4.; +const TEXT_INSETS: Insets = Insets::new(4.0, 2.0, 0.0, 2.0); // we send ourselves this when we want to reset blink, which must be done in event. const RESET_BLINK: Selector = Selector::new("druid-builtin.reset-textbox-blink"); @@ -45,6 +41,7 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); #[derive(Debug, Clone)] pub struct TextBox { placeholder: String, + text: TextLayout, width: f64, hscroll_offset: f64, selection: Selection, @@ -59,9 +56,11 @@ impl TextBox { /// Create a new TextBox widget pub fn new() -> TextBox { + let text = TextLayout::new(""); Self { width: 0.0, hscroll_offset: 0., + text, selection: Selection::caret(0), cursor_timer: TimerToken::INVALID, cursor_on: false, @@ -75,44 +74,48 @@ impl TextBox { self } - #[deprecated(since = "0.5.0", note = "Use TextBox::new instead")] - #[doc(hidden)] - pub fn raw() -> TextBox { - Self::new() + /// Builder-style method for setting the text size. + /// + /// The argument can be either an `f64` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn with_text_size(mut self, size: impl Into>) -> Self { + self.set_text_size(size); + self } - /// Calculate the PietTextLayout from the given text, font, and font size - fn get_layout( - &self, - piet_text: &mut PietText, - text: &str, - env: &Env, - use_placeholder_color: bool, - ) -> PietTextLayout { - let font_name = env.get(theme::FONT_NAME); - let font_size = env.get(theme::TEXT_SIZE_NORMAL); - let default_color = if use_placeholder_color { - env.get(theme::PLACEHOLDER_COLOR) - } else { - env.get(theme::LABEL_COLOR) - }; - let selection_text_color = env.get(theme::SELECTION_TEXT_COLOR); - - // TODO: caching of both the format and the layout - let font = piet_text - .font_family(&font_name) - .unwrap_or(FontFamily::SYSTEM_UI); - - piet_text - .new_text_layout(&text.to_string()) - .font(font, font_size) - .default_attribute(TextAttribute::ForegroundColor(default_color)) - .range_attribute( - self.selection.range(), - TextAttribute::ForegroundColor(selection_text_color), - ) - .build() - .unwrap() + /// Builder-style method for setting the font. + /// + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. + /// + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn with_font(mut self, font: impl Into>) -> Self { + self.set_font(font); + self + } + + /// Set the text size. + /// + /// The argument can be either an `f64` or a [`Key`]. + /// + /// [`Key`]: ../struct.Key.html + pub fn set_text_size(&mut self, size: impl Into>) { + self.text.set_text_size(size); + } + + /// Set the font. + /// + /// The argument can be a [`FontDescriptor`] or a [`Key`] + /// that refers to a font defined in the [`Env`]. + /// + /// [`Env`]: ../struct.Env.html + /// [`FontDescriptor`]: ../struct.FontDescriptor.html + /// [`Key`]: ../struct.Key.html + pub fn set_font(&mut self, font: impl Into>) { + self.text.set_font(font); } /// Insert text at the cursor position. @@ -206,26 +209,26 @@ impl TextBox { /// For a given point, returns the corresponding offset (in bytes) of /// the grapheme cluster closest to that point. - fn offset_for_point(&self, point: Point, layout: &PietTextLayout) -> usize { + fn offset_for_point(&self, point: Point) -> usize { // Translating from screenspace to Piet's text layout representation. // We need to account for hscroll_offset state and TextBox's padding. - let translated_point = Point::new(point.x + self.hscroll_offset - PADDING_LEFT, point.y); - let hit_test = layout.hit_test_point(translated_point); - hit_test.idx + let translated_point = Point::new(point.x + self.hscroll_offset - TEXT_INSETS.x0, point.y); + self.text.text_position_for_point(translated_point) } /// Given an offset (in bytes) of a valid grapheme cluster, return /// the corresponding x coordinate of that grapheme on the screen. - fn x_for_offset(&self, layout: &PietTextLayout, offset: usize) -> f64 { - layout.hit_test_text_position(offset).point.x + fn x_pos_for_offset(&self, offset: usize) -> f64 { + self.text.point_for_text_position(offset).x } /// Calculate a stateful scroll offset - fn update_hscroll(&mut self, layout: &PietTextLayout) { - let cursor_x = self.x_for_offset(layout, self.cursor()); - let overall_text_width = layout.size().width; + fn update_hscroll(&mut self) { + let cursor_x = self.x_pos_for_offset(self.cursor()); + let overall_text_width = self.text.size().width; - let padding = PADDING_LEFT * 2.; + // when advancing the cursor, we want some additional padding + let padding = TEXT_INSETS.x0 * 2.; if overall_text_width < self.width { // There's no offset if text is smaller than text box // @@ -257,8 +260,6 @@ impl Widget for TextBox { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut String, env: &Env) { // Guard against external changes in data? self.selection = self.selection.constrain_to(data); - - let mut text_layout = self.get_layout(&mut ctx.text(), &data, env, data.is_empty()); let mut edit_action = None; match event { @@ -267,7 +268,7 @@ impl Widget for TextBox { ctx.set_active(true); if !mouse.focus { - let cursor_offset = self.offset_for_point(mouse.pos, &text_layout); + let cursor_offset = self.offset_for_point(mouse.pos); edit_action = Some(EditAction::Click(MouseAction { row: 0, column: cursor_offset, @@ -280,7 +281,7 @@ impl Widget for TextBox { Event::MouseMove(mouse) => { ctx.set_cursor(&Cursor::IBeam); if ctx.is_active() { - let cursor_offset = self.offset_for_point(mouse.pos, &text_layout); + let cursor_offset = self.offset_for_point(mouse.pos); edit_action = Some(EditAction::Drag(MouseAction { row: 0, column: cursor_offset, @@ -359,36 +360,63 @@ impl Widget for TextBox { self.do_edit_action(edit_action, data); self.reset_cursor_blink(ctx); + self.text.set_text(data.as_str()); + self.text.rebuild_if_needed(&mut ctx.text(), env); if !is_select_all { - text_layout = self.get_layout(&mut ctx.text(), &data, env, data.is_empty()); - self.update_hscroll(&text_layout); + self.update_hscroll(); } } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &String, _env: &Env) { + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &String, env: &Env) { match event { - LifeCycle::WidgetAdded => ctx.register_for_focus(), + LifeCycle::WidgetAdded => { + ctx.register_for_focus(); + self.text.set_text(data.clone()); + self.text.rebuild_if_needed(&mut ctx.text(), env); + } // an open question: should we be able to schedule timers here? - LifeCycle::FocusChanged(true) => ctx.submit_command(RESET_BLINK, ctx.widget_id()), + LifeCycle::FocusChanged(true) => ctx.submit_command(RESET_BLINK.to(ctx.widget_id())), _ => (), } } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &String, _data: &String, _env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &String, data: &String, env: &Env) { + let content = if data.is_empty() { + &self.placeholder + } else { + data + }; + + // setting text color rebuilds layout, so don't do it if we don't have to + if !old_data.same(data) { + self.selection = self.selection.constrain_to(content); + self.text.set_text(data.as_str()); + if data.is_empty() { + self.text.set_text_color(theme::PLACEHOLDER_COLOR); + } else { + self.text.set_text_color(theme::LABEL_COLOR); + } + } + + self.text.rebuild_if_needed(&mut ctx.text(), env); ctx.request_paint(); } fn layout( &mut self, - _layout_ctx: &mut LayoutCtx, + _ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &String, env: &Env, ) -> Size { let width = env.get(theme::WIDE_WIDGET_WIDTH); - let height = env.get(theme::BORDERED_WIDGET_HEIGHT); + let min_height = env.get(theme::BORDERED_WIDGET_HEIGHT); + + let text_metrics = self.text.size(); + let text_height = text_metrics.height + TEXT_INSETS.y_value(); + let height = text_height.max(min_height); let size = bc.constrain((width, height)); self.width = size.width; @@ -405,7 +433,7 @@ impl Widget for TextBox { self.selection = self.selection.constrain_to(content); - let height = env.get(theme::BORDERED_WIDGET_HEIGHT); + let height = ctx.size().height; let background_color = env.get(theme::BACKGROUND_LIGHT); let selection_color = env.get(theme::SELECTION_COLOR); let cursor_color = env.get(theme::CURSOR_COLOR); @@ -431,36 +459,30 @@ impl Widget for TextBox { rc.clip(clip_rect); // Calculate layout - let text_layout = self.get_layout(&mut rc.text(), &content, env, data.is_empty()); - let text_size = text_layout.size(); + let text_size = self.text.size(); // Shift everything inside the clip by the hscroll_offset rc.transform(Affine::translate((-self.hscroll_offset, 0.))); // Layout, measure, and draw text - let top_padding = (height - text_size.height).min(PADDING_TOP).max(0.); - let text_pos = Point::new(PADDING_LEFT, top_padding); + let extra_padding = (height - text_size.height - TEXT_INSETS.y_value()).max(0.) / 2.; + let text_pos = Point::new(TEXT_INSETS.x0, TEXT_INSETS.y0 + extra_padding); // Draw selection rect if !self.selection.is_caret() { - for sel in text_layout.rects_for_range(self.selection.range()) { - let sel = sel + Vec2::new(PADDING_LEFT, top_padding); + for sel in self.text.rects_for_range(self.selection.range()) { + let sel = sel + text_pos.to_vec2(); let rounded = sel.to_rounded_rect(1.0); rc.fill(rounded, &selection_color); } } - rc.draw_text(&text_layout, text_pos); + self.text.draw(rc, text_pos); // Paint the cursor if focused and there's no selection if is_focused && self.cursor_on { - let pos = text_layout.hit_test_text_position(self.cursor()); - let metrics = text_layout.line_metric(pos.line).unwrap(); - //let cursor_x = self.x_for_offset(&text_layout, self.cursor()); - let xy = text_pos + Vec2::new(pos.point.x, 0.0); - let x2y2 = xy + Vec2::new(0., metrics.height); - let line = Line::new(xy, x2y2); - + let line = self.text.cursor_line_for_text_position(self.cursor()); + let line = line + text_pos.to_vec2(); rc.stroke(line, &cursor_color, 1.); } }); diff --git a/druid/src/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index d458ad570e..c7d73f2505 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -160,7 +160,8 @@ pub trait WidgetExt: Widget + Sized + 'static { } /// Control the events of this widget with a [`Click`] widget. The closure - /// provided will be called when the widget is clicked. + /// provided will be called when the widget is clicked with the left mouse + /// button. /// /// The child widget will also be updated on [`LifeCycle::HotChanged`] and /// mouse down, which can be useful for painting based on `ctx.is_active()` diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index e874704b65..82631475a3 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -167,8 +167,8 @@ impl Inner { } } - fn append_command(&mut self, target: Target, cmd: Command) { - self.command_queue.push_back((target, cmd)); + fn append_command(&mut self, cmd: Command) { + self.command_queue.push_back(cmd); } /// A helper fn for setting up the `DelegateCtx`. Takes a closure with @@ -205,8 +205,8 @@ impl Inner { } } - fn delegate_cmd(&mut self, target: Target, cmd: &Command) -> bool { - self.with_delegate(|del, data, env, ctx| del.command(ctx, target, cmd, data, env)) + fn delegate_cmd(&mut self, cmd: &Command) -> bool { + self.with_delegate(|del, data, env, ctx| del.command(ctx, cmd.target(), cmd, data, env)) .unwrap_or(true) } @@ -294,9 +294,9 @@ impl Inner { fn prepare_paint(&mut self, window_id: WindowId) { if let Some(win) = self.windows.get_mut(window_id) { - win.prepare_paint(&mut self.command_queue, &self.data, &self.env); + win.prepare_paint(&mut self.command_queue, &mut self.data, &self.env); } - self.invalidate_and_finalize(); + self.do_update(); } fn paint(&mut self, window_id: WindowId, piet: &mut Piet, invalid: &Region) { @@ -312,12 +312,12 @@ impl Inner { } /// Returns `true` if the command was handled. - fn dispatch_cmd(&mut self, target: Target, cmd: Command) -> bool { - if !self.delegate_cmd(target, &cmd) { + fn dispatch_cmd(&mut self, cmd: Command) -> bool { + if !self.delegate_cmd(&cmd) { return true; } - match target { + match cmd.target() { Target::Window(id) => { // first handle special window-level events if cmd.is(sys_cmd::SET_MENU) { @@ -337,8 +337,7 @@ impl Inner { // this widget, breaking if the event is handled. Target::Widget(id) => { for w in self.windows.iter_mut().filter(|w| w.may_contain_widget(id)) { - let event = - Event::Internal(InternalEvent::TargetedCommand(id.into(), cmd.clone())); + let event = Event::Internal(InternalEvent::TargetedCommand(cmd.clone())); if w.event(&mut self.command_queue, event, &mut self.data, &self.env) { return true; } @@ -352,6 +351,9 @@ impl Inner { } } } + Target::Auto => { + log::error!("{:?} reached window handler with `Target::Auto`", cmd); + } } false } @@ -513,7 +515,7 @@ impl AppState { loop { let next_cmd = self.inner.borrow_mut().command_queue.pop_front(); match next_cmd { - Some((target, cmd)) => self.handle_cmd(target, cmd), + Some(cmd) => self.handle_cmd(cmd), None => break, } } @@ -523,7 +525,7 @@ impl AppState { loop { let ext_cmd = self.inner.borrow_mut().ext_event_host.recv(); match ext_cmd { - Some((targ, cmd)) => self.handle_cmd(targ.unwrap_or(Target::Global), cmd), + Some(cmd) => self.handle_cmd(cmd), None => break, } } @@ -537,9 +539,13 @@ impl AppState { /// is open but a menu exists, as on macOS) it will be `None`. fn handle_system_cmd(&mut self, cmd_id: u32, window_id: Option) { let cmd = self.inner.borrow().get_menu_cmd(window_id, cmd_id); - let target = window_id.map(Into::into).unwrap_or(Target::Global); match cmd { - Some(cmd) => self.inner.borrow_mut().append_command(target, cmd), + Some(cmd) => { + let default_target = window_id.map(Into::into).unwrap_or(Target::Global); + self.inner + .borrow_mut() + .append_command(cmd.default_to(default_target)) + } None => log::warn!("No command for menu id {}", cmd_id), } self.process_commands(); @@ -548,9 +554,9 @@ impl AppState { /// Handle a command. Top level commands (e.g. for creating and destroying /// windows) have their logic here; other commands are passed to the window. - fn handle_cmd(&mut self, target: Target, cmd: Command) { + fn handle_cmd(&mut self, cmd: Command) { use Target as T; - match target { + match cmd.target() { // these are handled the same no matter where they come from _ if cmd.is(sys_cmd::QUIT_APP) => self.quit(), _ if cmd.is(sys_cmd::HIDE_APPLICATION) => self.hide_app(), @@ -566,7 +572,7 @@ impl AppState { T::Window(id) if cmd.is(sys_cmd::SHOW_OPEN_PANEL) => self.show_open_panel(cmd, id), T::Window(id) if cmd.is(sys_cmd::SHOW_SAVE_PANEL) => self.show_save_panel(cmd, id), T::Window(id) if cmd.is(sys_cmd::CLOSE_WINDOW) => { - if !self.inner.borrow_mut().dispatch_cmd(target, cmd) { + if !self.inner.borrow_mut().dispatch_cmd(cmd) { self.request_close_window(id); } } @@ -579,7 +585,7 @@ impl AppState { log::warn!("SHOW_WINDOW command must target a window.") } _ => { - self.inner.borrow_mut().dispatch_cmd(target, cmd); + self.inner.borrow_mut().dispatch_cmd(cmd); } } } @@ -597,13 +603,13 @@ impl AppState { .map(|w| w.handle.clone()); let result = handle.and_then(|mut handle| handle.open_file_sync(options)); - if let Some(info) = result { - let cmd = Command::new(sys_cmd::OPEN_FILE, info); - self.inner.borrow_mut().dispatch_cmd(window_id.into(), cmd); - } else { - let cmd = sys_cmd::OPEN_PANEL_CANCELLED.into(); - self.inner.borrow_mut().dispatch_cmd(window_id.into(), cmd); - } + self.inner.borrow_mut().dispatch_cmd({ + if let Some(info) = result { + sys_cmd::OPEN_FILE.with(info).to(window_id) + } else { + sys_cmd::OPEN_PANEL_CANCELLED.to(window_id) + } + }); } fn show_save_panel(&mut self, cmd: Command, window_id: WindowId) { @@ -614,14 +620,15 @@ impl AppState { .windows .get_mut(window_id) .map(|w| w.handle.clone()); + let result = handle.and_then(|mut handle| handle.save_as_sync(options)); - if let Some(info) = result { - let cmd = Command::new(sys_cmd::SAVE_FILE, Some(info)); - self.inner.borrow_mut().dispatch_cmd(window_id.into(), cmd); - } else { - let cmd = sys_cmd::SAVE_PANEL_CANCELLED.into(); - self.inner.borrow_mut().dispatch_cmd(window_id.into(), cmd); - } + self.inner.borrow_mut().dispatch_cmd({ + if let Some(info) = result { + sys_cmd::SAVE_FILE.with(Some(info)).to(window_id) + } else { + sys_cmd::SAVE_PANEL_CANCELLED.to(window_id) + } + }); } fn new_window(&mut self, cmd: Command) -> Result<(), Box> { @@ -762,7 +769,7 @@ impl WinHandler for DruidHandler { fn request_close(&mut self) { self.app_state - .handle_cmd(self.window_id.into(), sys_cmd::CLOSE_WINDOW.into()); + .handle_cmd(sys_cmd::CLOSE_WINDOW.to(self.window_id)); self.app_state.inner.borrow_mut().do_update(); } diff --git a/druid/src/window.rs b/druid/src/window.rs index fec55b8b26..d680f7f794 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -83,9 +83,9 @@ impl Window { } impl Window { - /// `true` iff any child requested an animation frame during the last `AnimFrame` event. + /// `true` iff any child requested an animation frame since the last `AnimFrame` event. pub(crate) fn wants_animation_frame(&self) -> bool { - self.last_anim.is_some() + self.root.state().request_anim } pub(crate) fn focus_chain(&self) -> &[WidgetId] { @@ -150,9 +150,6 @@ impl Window { self.timers.extend_drain(&mut widget_state.timers); // If we need a new paint pass, make sure druid-shell knows it. - if widget_state.request_anim && self.last_anim.is_none() { - self.last_anim = Some(Instant::now()); - } if self.wants_animation_frame() { self.handle.request_anim_frame(); } @@ -263,21 +260,6 @@ impl Window { env: &Env, process_commands: bool, ) { - // for AnimFrame, the event the window receives doesn't have the correct - // elapsed time; we calculate it here. - let now = Instant::now(); - let substitute_event = if let LifeCycle::AnimFrame(_) = event { - // TODO: this calculation uses wall-clock time of the paint call, which - // potentially has jitter. - // - // See https://github.com/linebender/druid/issues/85 for discussion. - let last = self.last_anim.take(); - let elapsed_ns = last.map(|t| now.duration_since(t).as_nanos()).unwrap_or(0) as u64; - Some(LifeCycle::AnimFrame(elapsed_ns)) - } else { - None - }; - let mut widget_state = WidgetState::new(self.root.id(), Some(self.size)); let mut state = ContextState::new::(queue, &self.ext_handle, &self.handle, self.id, self.focus); @@ -285,13 +267,7 @@ impl Window { state: &mut state, widget_state: &mut widget_state, }; - let event = substitute_event.as_ref().unwrap_or(event); self.root.lifecycle(&mut ctx, event, data, env); - - if substitute_event.is_some() && ctx.widget_state.request_anim { - self.last_anim = Some(now); - } - self.post_event_processing(&mut widget_state, queue, data, env, process_commands); } @@ -322,13 +298,31 @@ impl Window { } /// Get ready for painting, by doing layout and sending an `AnimFrame` event. - pub(crate) fn prepare_paint(&mut self, queue: &mut CommandQueue, data: &T, env: &Env) { - // FIXME: only do AnimFrame if root has requested_anim? - self.lifecycle(queue, &LifeCycle::AnimFrame(0), data, env, true); + pub(crate) fn prepare_paint(&mut self, queue: &mut CommandQueue, data: &mut T, env: &Env) { + let now = Instant::now(); + // TODO: this calculation uses wall-clock time of the paint call, which + // potentially has jitter. + // + // See https://github.com/linebender/druid/issues/85 for discussion. + let last = self.last_anim.take(); + let elapsed_ns = last.map(|t| now.duration_since(t).as_nanos()).unwrap_or(0) as u64; if self.root.state().needs_layout { self.layout(queue, data, env); } + + // Here, `self.wants_animation_frame()` refers to the animation frame that is currently + // being prepared for. (This is relying on the fact that `self.layout()` can't request + // an animation frame.) + if self.wants_animation_frame() { + self.event(queue, Event::AnimFrame(elapsed_ns), data, env); + } + + // Here, `self.wants_animation_frame()` is true if we want *another* animation frame after + // the current one. (It got modified in the call to `self.event` above.) + if self.wants_animation_frame() { + self.last_anim = Some(now); + } } pub(crate) fn do_paint( From f3e5b9856b228bac19cf486d43e435bc51ccfbbe Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 9 Sep 2020 16:52:14 +0200 Subject: [PATCH 12/18] fix fmt --- druid/src/widget/image.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 98723da690..a8dad96718 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -148,7 +148,7 @@ impl Widget for Image { // If either the width or height is constrained calculate a value so that the image fits // in the size exactly. If it is unconstrained by both width and height take the size of // the image. - let max = bc.max(); + let max = bc.max(); if bc.is_width_bounded() && !bc.is_height_bounded() { let ratio = max.width / self.image_data.x_pixels as f64; Size::new(max.width, ratio * self.image_data.y_pixels as f64) From 7c0e4ebbe2d888affe3287aa7ee8ec9712d039e4 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Sun, 20 Sep 2020 16:16:59 +0200 Subject: [PATCH 13/18] fix changelow and comments --- CHANGELOG.md | 2 +- druid/src/widget/image.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6d0045cd..0e65562dc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ You can find its changes [documented below](#060---2020-06-01). - Allow derivation of lenses for generic types ([#1120]) by [@rjwittams]) - Switch widget: Toggle animation being window refresh rate dependent ([#1145] by [@ForLoveOfCats]) - Multi-click on Windows, partial fix for #859 ([#1157] by [@raphlinus]) -- `widget::Imge` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) +- `widget::Image` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) ### Visual diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index a8dad96718..50b051509f 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -478,7 +478,7 @@ mod tests { // Being a height bound widget every row will have no padding at the start and end._id_1 - // The image starts at (0,0), so 200 white and then 200 black. + // the last white-black line starts at (199,0), so 200 white and then 200 black. let expecting: Vec = [ vec![255, 255, 255, 255].repeat(200), vec![0, 0, 0, 255].repeat(200), @@ -486,7 +486,7 @@ mod tests { .concat(); assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); - // The final row of 400 pixels is 200 white and then 200 black. + // The first row of black-white line starts at (200,0) so 200 black and then 200 white. let expecting: Vec = [ vec![0, 0, 0, 255].repeat(200), vec![255, 255, 255, 255].repeat(200), @@ -528,7 +528,7 @@ mod tests { // Being a height bound widget every row will have no padding at the start and end._id_1 - // The image starts at (0,0), so 200 black and then 200 white. + // the last white-black line starts at (199,0), so 200 white and then 200 black. let expecting: Vec = [ vec![255, 255, 255, 255].repeat(200), vec![0, 0, 0, 255].repeat(200), @@ -536,7 +536,7 @@ mod tests { .concat(); assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); - // The final row of 400 pixels is 200 white and then 200 black. + // The first row of black-white line starts at (200,0) so 200 black and then 200 white. let expecting: Vec = [ vec![0, 0, 0, 255].repeat(200), vec![255, 255, 255, 255].repeat(200), From 004e8794d0f500f65b58e65845700513fb41bbf3 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Sun, 20 Sep 2020 16:53:42 +0200 Subject: [PATCH 14/18] make tests more direct --- druid/src/widget/image.rs | 107 +++++++++++--------------------------- 1 file changed, 31 insertions(+), 76 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 50b051509f..78203971cb 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -449,8 +449,14 @@ mod tests { #[test] fn width_bound_paint() { - use crate::{tests::harness::Harness, widget::Scroll, WidgetId}; - let _id_1 = WidgetId::next(); + use crate::{ + tests::harness::Harness, + widget::{Container, Scroll}, + WidgetExt, WidgetId, + }; + use float_cmp::approx_eq; + + let id_1 = WidgetId::next(); let image_data = ImageData { pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], x_pixels: 2, @@ -458,49 +464,26 @@ mod tests { format: ImageFormat::Rgb, }; - let image_widget = - Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); - - Harness::create_with_render( - true, - Scroll::new(image_widget).vertical(), - Size::new(400., 400.), - |harness| { - harness.send_initial_events(); - harness.just_layout(); - harness.paint(); - }, - |target| { - // the width should be calculated to be 400. - let width = 400; - let raw_pixels = target.into_raw(); - assert_eq!(raw_pixels.len(), 400 * width * 4); - - // Being a height bound widget every row will have no padding at the start and end._id_1 - - // the last white-black line starts at (199,0), so 200 white and then 200 black. - let expecting: Vec = [ - vec![255, 255, 255, 255].repeat(200), - vec![0, 0, 0, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); + let image_widget = Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)); - // The first row of black-white line starts at (200,0) so 200 black and then 200 white. - let expecting: Vec = [ - vec![0, 0, 0, 255].repeat(200), - vec![255, 255, 255, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[200 * width * 4..201 * width * 4], expecting[..]); - }, - ); + Harness::create_simple(true, image_widget, |harness| { + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id_1); + assert!(approx_eq!(f64, state.layout_rect().x1, 400.0)); + }) } #[test] fn height_bound_paint() { - use crate::{tests::harness::Harness, widget::Scroll, WidgetId}; - let _id_1 = WidgetId::next(); + use crate::{ + tests::harness::Harness, + widget::{Container, Scroll}, + WidgetExt, WidgetId, + }; + use float_cmp::approx_eq; + + let id_1 = WidgetId::next(); let image_data = ImageData { pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], x_pixels: 2, @@ -509,41 +492,13 @@ mod tests { }; let image_widget = - Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); - - Harness::create_with_render( - true, - Scroll::new(image_widget).horizontal(), - Size::new(400., 400.), - |harness| { - harness.send_initial_events(); - harness.just_layout(); - harness.paint(); - }, - |target| { - // the width should be calculated to be 400. - let width = 400; - let raw_pixels = target.into_raw(); - assert_eq!(raw_pixels.len(), 400 * width * 4); - - // Being a height bound widget every row will have no padding at the start and end._id_1 - - // the last white-black line starts at (199,0), so 200 white and then 200 black. - let expecting: Vec = [ - vec![255, 255, 255, 255].repeat(200), - vec![0, 0, 0, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[199 * width * 4..200 * width * 4], expecting[..]); - - // The first row of black-white line starts at (200,0) so 200 black and then 200 white. - let expecting: Vec = [ - vec![0, 0, 0, 255].repeat(200), - vec![255, 255, 255, 255].repeat(200), - ] - .concat(); - assert_eq!(raw_pixels[200 * width * 4..201 * width * 4], expecting[..]); - }, - ); + Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)).horizontal(); + + Harness::create_simple(true, image_widget, |harness| { + harness.send_initial_events(); + harness.just_layout(); + let state = harness.get_state(id_1); + assert!(approx_eq!(f64, state.layout_rect().x1, 400.0)); + }) } } From 5516707c960ce2f671cf4af4da69906eaca03574 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 30 Sep 2020 19:35:24 +0200 Subject: [PATCH 15/18] use the new type ImageBuf instead of old ImageData --- druid/src/widget/image.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 0c5493c67a..89ad5e9a11 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -149,12 +149,13 @@ impl Widget for Image { // in the size exactly. If it is unconstrained by both width and height take the size of // the image. let max = bc.max(); + let image_size = self.image_data.size(); if bc.is_width_bounded() && !bc.is_height_bounded() { - let ratio = max.width / self.image_data.x_pixels as f64; - Size::new(max.width, ratio * self.image_data.y_pixels as f64) + let ratio = max.width / image_size.width; + Size::new(max.width, ratio * image_size.height) } else if bc.is_height_bounded() && !bc.is_width_bounded() { - let ratio = max.height / self.image_data.y_pixels as f64; - Size::new(ratio * self.image_data.x_pixels as f64, max.height) + let ratio = max.height / image_size.height; + Size::new(ratio * image_size.width, max.height) } else { bc.constrain(self.image_data.size()) } @@ -336,12 +337,12 @@ mod tests { use float_cmp::approx_eq; let id_1 = WidgetId::next(); - let image_data = ImageData { - pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], - x_pixels: 2, - y_pixels: 2, - format: ImageFormat::Rgb, - }; + let image_data = ImageBuf::from_raw( + vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + ImageFormat::Rgb, + 2, + 2, + ); let image_widget = Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)); @@ -363,12 +364,12 @@ mod tests { use float_cmp::approx_eq; let id_1 = WidgetId::next(); - let image_data = ImageData { - pixels: vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], - x_pixels: 2, - y_pixels: 2, - format: ImageFormat::Rgb, - }; + let image_data = ImageBuf::from_raw( + vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + ImageFormat::Rgb, + 2, + 2, + ); let image_widget = Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)).horizontal(); From b6f9c4386a7d81fc76d5d3a5c5cb899a21e49ab2 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 30 Sep 2020 19:59:50 +0200 Subject: [PATCH 16/18] explicitly set .vertical on the scroll --- druid/src/widget/image.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 89ad5e9a11..1e91c72a92 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -328,7 +328,7 @@ mod tests { } #[test] - fn width_bound_paint() { + fn width_bound_layout() { use crate::{ tests::harness::Harness, widget::{Container, Scroll}, @@ -344,7 +344,8 @@ mod tests { 2, ); - let image_widget = Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)); + let image_widget = + Scroll::new(Container::new(Image::new(image_data)).with_id(id_1)).vertical(); Harness::create_simple(true, image_widget, |harness| { harness.send_initial_events(); @@ -355,7 +356,7 @@ mod tests { } #[test] - fn height_bound_paint() { + fn height_bound_layout() { use crate::{ tests::harness::Harness, widget::{Container, Scroll}, From 211421f176fe8d4d046c58733a1dbf2e06dd9d24 Mon Sep 17 00:00:00 2001 From: jaap aarts Date: Wed, 30 Sep 2020 22:28:45 +0200 Subject: [PATCH 17/18] fixed merge --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 822b833f42..f308406331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,12 +95,9 @@ You can find its changes [documented below](#060---2020-06-01). - Allow derivation of lenses for generic types ([#1120]) by [@rjwittams]) - Switch widget: Toggle animation being window refresh rate dependent ([#1145] by [@ForLoveOfCats]) - Multi-click on Windows, partial fix for #859 ([#1157] by [@raphlinus]) -<<<<<<< HEAD -- `widget::Image` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) -======= - Windows: fix crash on resize from incompatible resources ([#1191 by [@raphlinus]]) - GTK: Related dependencies are now optional, facilitating a pure X11 build. ([#1241] by [@finnerale]) ->>>>>>> 59f6750122095033fb368c29609afbc7e0ce6371 +- `widget::Image` now computes the layout correctly when unbound in one direction. ([#1189] by [@JAicewizard]) ### Visual From e1964b6000956e48a6dee0a2bffcd75c100e4238 Mon Sep 17 00:00:00 2001 From: Jaap Aarts Date: Thu, 1 Oct 2020 17:09:23 +0200 Subject: [PATCH 18/18] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f308406331..425f03a991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -467,7 +467,7 @@ Last release without a changelog :( [#1185]: https://github.com/linebender/druid/pull/1185 [#1191]: https://github.com/linebender/druid/pull/1191 [#1092]: https://github.com/linebender/druid/pull/1092 -[#1186]: https://github.com/linebender/druid/pull/1186 +[#1189]: https://github.com/linebender/druid/pull/1189 [#1195]: https://github.com/linebender/druid/pull/1195 [#1204]: https://github.com/linebender/druid/pull/1204 [#1205]: https://github.com/linebender/druid/pull/1205