From da8fbc86ca79e7ec9f5f2705ac6e1679ad0023cc Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Sun, 20 Sep 2020 16:51:32 -0500 Subject: [PATCH 1/2] Adds an ImageBuf type to druid-shell, replacing ImageData in druid. --- CHANGELOG.md | 2 + druid-shell/Cargo.toml | 8 +- druid-shell/src/image.rs | 219 +++++++++++++++++++++++++++++++ druid-shell/src/lib.rs | 2 + druid/Cargo.toml | 6 +- druid/examples/image.rs | 6 +- druid/examples/widget_gallery.rs | 8 +- druid/src/lib.rs | 6 +- druid/src/widget/image.rs | 195 ++++++--------------------- druid/src/widget/mod.rs | 2 +- 10 files changed, 280 insertions(+), 174 deletions(-) create mode 100644 druid-shell/src/image.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0334b3cbad..59d13413e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ You can find its changes [documented below](#060---2020-06-01). - Moved `Target` parameter from `submit_command` to `Command::new` and `Command::to`. ([#1185] by [@finnerale]) - `Movement::RightOfLine` to `Movement::NextLineBreak`, and `Movement::LeftOfLine` to `Movement::PrecedingLineBreak`. ([#1092] by [@sysint64]) - `AnimFrame` was moved from `lifecycle` to `event` ([#1155] by [@jneem]) +- Renamed `ImageData` to `ImageBuf` and moved it to `druid_shell` ([#1183] by [@jneem]) - Contexts' `text()` methods return `&mut PietText` instead of cloning ([#1205] by [@cmyr]) - Window construction: WindowDesc decomposed to PendingWindow and WindowConfig to allow for sub-windows and reconfiguration. ([#1235] by [@rjwittams]) - `LocalizedString` and `LabelText` use `ArcStr` instead of String ([#1245] by [@cmyr]) @@ -456,6 +457,7 @@ Last release without a changelog :( [#1172]: https://github.com/linebender/druid/pull/1172 [#1173]: https://github.com/linebender/druid/pull/1173 [#1182]: https://github.com/linebender/druid/pull/1182 +[#1183]: https://github.com/linebender/druid/pull/1183 [#1185]: https://github.com/linebender/druid/pull/1185 [#1191]: https://github.com/linebender/druid/pull/1191 [#1092]: https://github.com/linebender/druid/pull/1092 diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 2b39a152a8..2c53eab552 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -14,9 +14,9 @@ rustdoc-args = ["--cfg", "docsrs"] default-target = "x86_64-pc-windows-msvc" [features] -x11 = ["x11rb", "nix", "cairo-sys-rs"] -gtk = ["gio", "gdk", "gdk-sys", "glib", "glib-sys", "gtk-sys", "gtk-rs"] default = ["gtk"] +gtk = ["gio", "gdk", "gdk-sys", "glib", "glib-sys", "gtk-sys", "gtk-rs", "gdk-pixbuf"] +x11 = ["x11rb", "nix", "cairo-sys-rs"] [dependencies] # NOTE: When changing the piet or kurbo versions, ensure that @@ -32,6 +32,9 @@ instant = { version = "0.1.6", features = ["wasm-bindgen"] } anyhow = "1.0.32" keyboard-types = { version = "0.5.0", default_features = false } +# Optional dependencies +image = { version = "0.23.10", optional = true } + [target.'cfg(target_os="windows")'.dependencies] wio = "0.2.2" @@ -55,6 +58,7 @@ cairo-rs = { version = "0.9.1", default_features = false, features = ["xcb"] } cairo-sys-rs = { version = "0.10.0", default_features = false, optional = true } gio = { version = "0.9.1", optional = true } gdk = { version = "0.13.2", optional = true } +gdk-pixbuf = { version = "0.9.0", optional = true } gdk-sys = { version = "0.10.0", optional = true } # `gtk` gets renamed to `gtk-rs` so that we can use `gtk` as the feature name. gtk-rs = { version = "0.9.2", features = ["v3_22"], package = "gtk", optional = true } diff --git a/druid-shell/src/image.rs b/druid-shell/src/image.rs new file mode 100644 index 0000000000..f537087fd5 --- /dev/null +++ b/druid-shell/src/image.rs @@ -0,0 +1,219 @@ +// Copyright 2020 The Druid 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. + +#[cfg(feature = "image")] +use std::error::Error; +#[cfg(feature = "image")] +use std::path::Path; +use std::sync::Arc; + +use kurbo::Size; +use piet_common::{Color, ImageFormat, RenderContext}; + +/// An in-memory pixel buffer. +/// +/// Contains raw bytes, dimensions, and image format ([`piet::ImageFormat`]). +/// +/// [`piet::ImageFormat`]: ../piet/enum.ImageFormat.html +#[derive(Clone)] +pub struct ImageBuf { + pixels: Arc<[u8]>, + width: usize, + height: usize, + format: ImageFormat, +} + +impl ImageBuf { + /// Create an empty image buffer. + pub fn empty() -> Self { + ImageBuf { + pixels: Arc::new([]), + width: 0, + height: 0, + format: ImageFormat::RgbaSeparate, + } + } + + /// Creates a new image buffer from an array of bytes. + /// + /// `format` specifies the pixel format of the pixel data, which must have length + /// `width * height * format.bytes_per_pixel()`. + /// + /// # Panics + /// + /// Panics if the pixel data has the wrong length. + pub fn from_raw( + pixels: impl Into>, + format: ImageFormat, + width: usize, + height: usize, + ) -> ImageBuf { + let pixels = pixels.into(); + assert_eq!(pixels.len(), width * height * format.bytes_per_pixel()); + ImageBuf { + pixels, + format, + width, + height, + } + } + + /// Returns the raw pixel data of this image buffer. + pub fn raw_pixels(&self) -> &[u8] { + &self.pixels[..] + } + + /// Returns a shared reference to the raw pixel data of this image buffer. + pub fn raw_pixels_shared(&self) -> Arc<[u8]> { + Arc::clone(&self.pixels) + } + + /// Returns the format of the raw pixel data. + pub fn format(&self) -> ImageFormat { + self.format + } + + /// The width, in pixels, of this image. + pub fn width(&self) -> usize { + self.width + } + + /// The height, in pixels, of this image. + pub fn height(&self) -> usize { + self.height + } + + /// The size of this image, in pixels. + pub fn size(&self) -> Size { + Size::new(self.width() as f64, self.height() as f64) + } + + /// Returns an iterator over the pixels in this image. + /// + /// The return value is an iterator over "rows", where each "row" is an iterator + /// over the color of the pixels in that row. + pub fn pixel_colors<'a>( + &'a self, + ) -> impl Iterator + 'a> + 'a { + // TODO: a version of this exists in piet-web and piet-coregraphics. Maybe put it somewhere + // common? + fn unpremul(x: u8, a: u8) -> u8 { + if a == 0 { + 0 + } else { + let y = (x as u32 * 255 + (a as u32 / 2)) / (a as u32); + y.min(255) as u8 + } + } + let format = self.format; + let bytes_per_pixel = format.bytes_per_pixel(); + self.pixels + .chunks_exact(self.width * bytes_per_pixel) + .map(move |row| { + row.chunks_exact(bytes_per_pixel) + .map(move |p| match format { + ImageFormat::Rgb => Color::rgb8(p[0], p[1], p[2]), + ImageFormat::RgbaSeparate => Color::rgba8(p[0], p[1], p[2], p[3]), + ImageFormat::RgbaPremul => { + let a = p[3]; + Color::rgba8(unpremul(p[0], a), unpremul(p[1], a), unpremul(p[2], a), a) + } + // TODO: is there a better way to handle unsupported formats? + _ => Color::WHITE, + }) + }) + } + + /// Converts this buffer a Piet image, which is optimized for drawing into a [`RenderContext`]. + /// + /// [`RenderContext`]: ../piet/trait.RenderContext.html + pub fn to_piet_image(&self, ctx: &mut Ctx) -> Ctx::Image { + ctx.make_image(self.width(), self.height(), &self.pixels, self.format) + .unwrap() + } +} + +impl Default for ImageBuf { + fn default() -> Self { + ImageBuf::empty() + } +} + +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +impl ImageBuf { + /// Load an image from a DynamicImage from the image crate + pub fn from_dynamic_image(image_data: image::DynamicImage) -> ImageBuf { + use image::ColorType::*; + let has_alpha_channel = match image_data.color() { + La8 | Rgba8 | La16 | Rgba16 | Bgra8 => true, + _ => false, + }; + + if has_alpha_channel { + ImageBuf::from_dynamic_image_with_alpha(image_data) + } else { + ImageBuf::from_dynamic_image_without_alpha(image_data) + } + } + + /// Load an image from a DynamicImage with alpha + pub fn from_dynamic_image_with_alpha(image_data: image::DynamicImage) -> ImageBuf { + let rgba_image = image_data.to_rgba(); + let sizeofimage = rgba_image.dimensions(); + ImageBuf::from_raw( + rgba_image.to_vec(), + ImageFormat::RgbaSeparate, + sizeofimage.0 as usize, + sizeofimage.1 as usize, + ) + } + + /// Load an image from a DynamicImage without alpha + pub fn from_dynamic_image_without_alpha(image_data: image::DynamicImage) -> ImageBuf { + let rgb_image = image_data.to_rgb(); + let sizeofimage = rgb_image.dimensions(); + ImageBuf::from_raw( + rgb_image.to_vec(), + ImageFormat::Rgb, + sizeofimage.0 as usize, + sizeofimage.1 as usize, + ) + } + + /// Attempt to load an image from raw bytes. + /// + /// If the image crate can't decode an image from the data an error will be returned. + pub fn from_data(raw_image: &[u8]) -> Result> { + let image_data = image::load_from_memory(raw_image).map_err(|e| e)?; + Ok(ImageBuf::from_dynamic_image(image_data)) + } + + /// Attempt to load an image from the file at the provided path. + pub fn from_file>(path: P) -> Result> { + let image_data = image::open(path).map_err(|e| e)?; + Ok(ImageBuf::from_dynamic_image(image_data)) + } +} + +impl std::fmt::Debug for ImageBuf { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("ImageBuf") + .field("size", &self.pixels.len()) + .field("width", &self.width) + .field("height", &self.height) + .field("format", &format_args!("{:?}", self.format)) + .finish() + } +} diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index 5d3b62425f..9d297e876d 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -48,6 +48,7 @@ mod common_util; mod dialog; mod error; mod hotkey; +mod image; mod keyboard; mod menu; mod mouse; @@ -57,6 +58,7 @@ mod scale; mod screen; mod window; +pub use crate::image::ImageBuf; pub use application::{AppHandler, Application}; pub use clipboard::{Clipboard, ClipboardFormat, FormatId}; pub use common_util::Counter; diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 72e92e6618..899a5e8796 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -20,10 +20,11 @@ rustdoc-args = ["--cfg", "docsrs"] default-target = "x86_64-pc-windows-msvc" [features] -x11 = ["druid-shell/x11"] +default = ["gtk"] gtk = ["druid-shell/gtk"] +image = ["druid-shell/image"] svg = ["usvg", "harfbuzz-sys"] -default = ["gtk"] +x11 = ["druid-shell/x11"] [dependencies] druid-shell = { version = "0.6.0", default-features = false, path = "../druid-shell" } @@ -46,7 +47,6 @@ usvg = { version = "0.11.0", optional = true } # We don't depend directly on harfbuzz-sys, it comes through usvg. But we need to pin # the version, because the latest release of harfbuzz_rs is broken with harfbuzz-sys 0.5. harfbuzz-sys = { version = "0.5.0", optional = true } -image = { version = "0.23.9", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] console_log = "0.2.0" diff --git a/druid/examples/image.rs b/druid/examples/image.rs index 3da38e94bb..c4b526f2dd 100644 --- a/druid/examples/image.rs +++ b/druid/examples/image.rs @@ -15,8 +15,8 @@ //! This example shows how to draw an png image. use druid::{ - widget::{FillStrat, Flex, Image, ImageData, WidgetExt}, - AppLauncher, Color, Widget, WindowDesc, + widget::{FillStrat, Flex, Image, WidgetExt}, + AppLauncher, Color, ImageBuf, Widget, WindowDesc, }; pub fn main() { @@ -29,7 +29,7 @@ pub fn main() { } fn ui_builder() -> impl Widget { - let png_data = ImageData::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(); + let png_data = ImageBuf::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(); let mut col = Flex::column(); diff --git a/druid/examples/widget_gallery.rs b/druid/examples/widget_gallery.rs index 4cd4e0c119..cd3a977ca5 100644 --- a/druid/examples/widget_gallery.rs +++ b/druid/examples/widget_gallery.rs @@ -17,10 +17,10 @@ use druid::{ kurbo::{Affine, BezPath, Circle}, piet::{FixedLinearGradient, GradientStop, InterpolationMode}, widget::{ - prelude::*, Button, Checkbox, FillStrat, Flex, Image, ImageData, Label, List, Painter, - ProgressBar, RadioGroup, Scroll, Slider, Spinner, Stepper, Switch, TextBox, + prelude::*, Button, Checkbox, FillStrat, Flex, Image, Label, List, Painter, ProgressBar, + RadioGroup, Scroll, Slider, Spinner, Stepper, Switch, TextBox, }, - AppLauncher, Color, Data, Lens, Rect, Widget, WidgetExt, WidgetPod, WindowDesc, + AppLauncher, Color, Data, ImageBuf, Lens, Rect, Widget, WidgetExt, WidgetPod, WindowDesc, }; #[cfg(not(target_arch = "wasm32"))] @@ -182,7 +182,7 @@ fn ui_builder() -> impl Widget { )) .with_child(label_widget( Image::new( - ImageData::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(), + ImageBuf::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(), ) .fill_mode(FillStrat::Fill) .interpolation_mode(InterpolationMode::Bilinear), diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 41835f67a8..540813325a 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -178,9 +178,9 @@ pub use piet::{ pub use shell::keyboard_types; pub use shell::{ Application, Clipboard, ClipboardFormat, Code, Cursor, Error as PlatformError, - FileDialogOptions, FileInfo, FileSpec, FormatId, HotKey, KbKey, KeyEvent, Location, Modifiers, - Monitor, MouseButton, MouseButtons, RawMods, Region, Scalable, Scale, Screen, SysMods, - TimerToken, WindowHandle, WindowState, + FileDialogOptions, FileInfo, FileSpec, FormatId, HotKey, ImageBuf, KbKey, KeyEvent, Location, + Modifiers, Monitor, MouseButton, MouseButtons, RawMods, Region, Scalable, Scale, Screen, + SysMods, TimerToken, WindowHandle, WindowState, }; pub use crate::core::WidgetPod; diff --git a/druid/src/widget/image.rs b/druid/src/widget/image.rs index 59615a31d8..758ae9fe1a 100644 --- a/druid/src/widget/image.rs +++ b/druid/src/widget/image.rs @@ -15,12 +15,10 @@ //! An Image widget. //! Please consider using SVG and the SVG widget as it scales much better. -use std::fmt; -#[cfg(feature = "image")] -use std::{convert::AsRef, error::Error, path::Path}; +use druid_shell::ImageBuf; use crate::{ - piet::{Image as PietImage, ImageFormat, InterpolationMode}, + piet::{Image as PietImage, InterpolationMode}, widget::common::FillStrat, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Rect, RenderContext, Size, UpdateCtx, Widget, @@ -37,7 +35,7 @@ use crate::{ /// Instead consider using [SVG files] and enabling the `svg` feature with `cargo`. /// /// (See also: -/// [`ImageData`], +/// [`ImageBuf`], /// [`FillStrat`], /// [`InterpolationMode`] /// ) @@ -47,11 +45,12 @@ use crate::{ /// Create an image widget and configure it using builder methods /// ``` /// use druid::{ -/// widget::{Image, ImageData, FillStrat}, +/// widget::{Image, FillStrat}, /// piet::InterpolationMode, /// }; +/// use druid_shell::ImageBuf; /// -/// let image_data = ImageData::empty(); +/// let image_data = ImageBuf::empty(); /// let image_widget = Image::new(image_data) /// // set the fill strategy /// .fill_mode(FillStrat::Fill) @@ -61,11 +60,12 @@ use crate::{ /// Create an image widget and configure it using setters /// ``` /// use druid::{ -/// widget::{Image, ImageData, FillStrat}, +/// widget::{Image, FillStrat}, /// piet::InterpolationMode, /// }; +/// use druid_shell::ImageBuf; /// -/// let image_data = ImageData::empty(); +/// let image_data = ImageBuf::empty(); /// let mut image_widget = Image::new(image_data); /// // set the fill strategy /// image_widget.set_fill_mode(FillStrat::FitWidth); @@ -75,18 +75,18 @@ use crate::{ /// /// [scaling a bitmap image]: ../struct.Scale.html#pixels-and-display-points /// [SVG files]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -/// [`ImageData`]: struct.ImageData.html +/// [`ImageBuf`]: ../druid_shell/struct.ImageBuf.html /// [`FillStrat`]: ../widget/enum.FillStrat.html /// [`InterpolationMode`]: ../piet/enum.InterpolationMode.html pub struct Image { - image_data: ImageData, + image_data: ImageBuf, paint_data: Option, fill: FillStrat, interpolation: InterpolationMode, } impl Image { - /// Create an image drawing widget from `ImageData`. + /// Create an image drawing widget from an image buffer. /// /// By default, the Image will scale to fit its box constraints /// ([`FillStrat::Fill`]) @@ -95,7 +95,7 @@ impl Image { /// /// [`FillStrat::Fill`]: ../widget/enum.FillStrat.html#variant.Fill /// [`InterpolationMode::Bilinear`]: ../piet/enum.InterpolationMode.html#variant.Bilinear - pub fn new(image_data: ImageData) -> Self { + pub fn new(image_data: ImageBuf) -> Self { Image { image_data, paint_data: None, @@ -148,14 +148,12 @@ impl Widget for Image { if bc.is_width_bounded() { bc.max() } else { - bc.constrain(self.image_data.get_size()) + bc.constrain(self.image_data.size()) } } fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, _env: &Env) { - let offset_matrix = self - .fill - .affine_to_fill(ctx.size(), self.image_data.get_size()); + let offset_matrix = self.fill.affine_to_fill(ctx.size(), self.image_data.size()); // The ImageData's to_piet function does not clip to the image's size // CairoRenderContext is very like druids but with some extra goodies like clip @@ -168,142 +166,23 @@ impl Widget for Image { let piet_image = { let image_data = &self.image_data; self.paint_data - .get_or_insert_with(|| image_data.to_piet(ctx)) + .get_or_insert_with(|| image_data.to_piet_image(ctx.render_ctx)) }; ctx.transform(offset_matrix); ctx.draw_image( piet_image, - self.image_data.get_size().to_rect(), + self.image_data.size().to_rect(), self.interpolation, ); }); } } -/// Processed image data. -/// -/// By default, Druid does not parse image data. -/// However, enabling [the `image` feature] -/// provides several -/// methods by which you can load image files. -/// -/// Contains raw bytes, dimensions, and image format ([`piet::ImageFormat`]). -/// -/// [the `image` feature]: ../index.html#optional-features -/// [`piet::ImageFormat`]: ../piet/enum.ImageFormat.html -#[derive(Clone)] -pub struct ImageData { - pixels: Vec, - x_pixels: u32, - y_pixels: u32, - format: ImageFormat, -} - -impl ImageData { - /// Create an empty Image - pub fn empty() -> Self { - ImageData { - pixels: [].to_vec(), - x_pixels: 0, - y_pixels: 0, - format: ImageFormat::RgbaSeparate, - } - } - - /// Get the size in pixels of the contained image. - fn get_size(&self) -> Size { - Size::new(self.x_pixels as f64, self.y_pixels as f64) - } - - /// Convert ImageData into Piet draw instructions. - fn to_piet(&self, ctx: &mut PaintCtx) -> PietImage { - ctx.make_image( - self.get_size().width as usize, - self.get_size().height as usize, - &self.pixels, - self.format, - ) - .unwrap() - } -} - -#[cfg(feature = "image")] -#[cfg_attr(docsrs, doc(cfg(feature = "image")))] -impl ImageData { - /// Load an image from a DynamicImage from the image crate - pub fn from_dynamic_image(image_data: image::DynamicImage) -> ImageData { - use image::ColorType::*; - let has_alpha_channel = match image_data.color() { - La8 | Rgba8 | La16 | Rgba16 | Bgra8 => true, - _ => false, - }; - - if has_alpha_channel { - Self::from_dynamic_image_with_alpha(image_data) - } else { - Self::from_dynamic_image_without_alpha(image_data) - } - } - - /// Load an image from a DynamicImage with alpha - pub fn from_dynamic_image_with_alpha(image_data: image::DynamicImage) -> ImageData { - let rgba_image = image_data.to_rgba(); - let sizeofimage = rgba_image.dimensions(); - ImageData { - pixels: rgba_image.to_vec(), - x_pixels: sizeofimage.0, - y_pixels: sizeofimage.1, - format: ImageFormat::RgbaSeparate, - } - } - - /// Load an image from a DynamicImage without alpha - pub fn from_dynamic_image_without_alpha(image_data: image::DynamicImage) -> ImageData { - let rgb_image = image_data.to_rgb(); - let sizeofimage = rgb_image.dimensions(); - ImageData { - pixels: rgb_image.to_vec(), - x_pixels: sizeofimage.0, - y_pixels: sizeofimage.1, - format: ImageFormat::Rgb, - } - } - - /// Attempt to load an image from raw bytes. - /// - /// If the image crate can't decode an image from the data an error will be returned. - pub fn from_data(raw_image: &[u8]) -> Result> { - let image_data = image::load_from_memory(raw_image).map_err(|e| e)?; - Ok(ImageData::from_dynamic_image(image_data)) - } - - /// Attempt to load an image from the file at the provided path. - pub fn from_file>(path: P) -> Result> { - let image_data = image::open(path).map_err(|e| e)?; - Ok(ImageData::from_dynamic_image(image_data)) - } -} - -impl Default for ImageData { - fn default() -> Self { - ImageData::empty() - } -} - -impl fmt::Debug for ImageData { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("ImageData") - .field("size", &self.pixels.len()) - .field("width", &self.x_pixels) - .field("height", &self.y_pixels) - .field("format", &format_args!("{:?}", self.format)) - .finish() - } -} - #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod tests { + use crate::piet::ImageFormat; + use super::*; #[test] @@ -311,12 +190,12 @@ mod tests { 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_data = ImageBuf::from_raw( + vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + ImageFormat::Rgb, + 2, + 2, + ); let image_widget = Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); @@ -358,12 +237,12 @@ mod tests { fn wide_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_data = ImageBuf::from_raw( + vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + ImageFormat::Rgb, + 2, + 2, + ); let image_widget = Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); @@ -413,12 +292,12 @@ mod tests { 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_data = ImageBuf::from_raw( + vec![255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255], + ImageFormat::Rgb, + 2, + 2, + ); let image_widget = Image::new(image_data).interpolation_mode(InterpolationMode::NearestNeighbor); diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 96f466d0bd..026e74addf 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -52,7 +52,7 @@ mod view_switcher; mod widget; mod widget_ext; -pub use self::image::{Image, ImageData}; +pub use self::image::Image; pub use align::Align; pub use button::Button; pub use checkbox::Checkbox; From 651074d0d2dd327616b75d7ff49768405fcf841f Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Sun, 20 Sep 2020 16:54:28 -0500 Subject: [PATCH 2/2] Add custom cursors, implemented on GTK and windows. --- CHANGELOG.md | 1 + druid-shell/Cargo.toml | 1 + druid-shell/src/lib.rs | 2 +- druid-shell/src/mouse.rs | 26 +++++- druid-shell/src/platform/gtk/window.rs | 69 ++++++++++++--- druid-shell/src/platform/mac/window.rs | 13 ++- druid-shell/src/platform/web/window.rs | 13 ++- druid-shell/src/platform/windows/window.rs | 99 ++++++++++++++++++++-- druid-shell/src/platform/x11/window.rs | 10 ++- druid-shell/src/window.rs | 6 +- druid/Cargo.toml | 4 + druid/examples/cursor.rs | 98 +++++++++++++++++++++ druid/examples/web/src/lib.rs | 1 + druid/src/lib.rs | 2 +- 14 files changed, 318 insertions(+), 27 deletions(-) create mode 100644 druid/examples/cursor.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d13413e0..1b48c25e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ You can find its changes [documented below](#060---2020-06-01). - `Menu` commands can now choose a custom target. ([#1185] by [@finnerale]) - `Movement::StartOfDocument`, `Movement::EndOfDocument`. ([#1092] by [@sysint64]) - `TextLayout` type simplifies drawing text ([#1182] by [@cmyr]) +- Added support for custom mouse cursors ([#1183] by [@jneem]) - Implementation of `Data` trait for `i128` and `u128` primitive data types. ([#1214] by [@koutoftimer]) - `LineBreaking` enum allows configuration of label line-breaking ([#1195] by [@cmyr]) - `TextAlignment` support in `TextLayout` and `Label` ([#1210] by [@cmyr]) diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 2c53eab552..936d750631 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -36,6 +36,7 @@ keyboard-types = { version = "0.5.0", default_features = false } image = { version = "0.23.10", optional = true } [target.'cfg(target_os="windows")'.dependencies] +scopeguard = "1.1.0" wio = "0.2.2" [target.'cfg(target_os="windows")'.dependencies.winapi] diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index 9d297e876d..1a559dc266 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -67,7 +67,7 @@ pub use error::Error; pub use hotkey::{HotKey, RawMods, SysMods}; pub use keyboard::{Code, IntoKey, KbKey, KeyEvent, KeyState, Location, Modifiers}; pub use menu::Menu; -pub use mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +pub use mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; pub use region::Region; pub use scale::{Scalable, Scale, ScaledArea}; pub use screen::{Monitor, Screen}; diff --git a/druid-shell/src/mouse.rs b/druid-shell/src/mouse.rs index 2b5d4a2e6d..938e2b8af1 100644 --- a/druid-shell/src/mouse.rs +++ b/druid-shell/src/mouse.rs @@ -15,8 +15,9 @@ //! Common types for representing mouse events and state use crate::kurbo::{Point, Vec2}; +use crate::platform; -use crate::Modifiers; +use crate::{ImageBuf, Modifiers}; /// Information about the mouse event. /// @@ -253,4 +254,27 @@ pub enum Cursor { NotAllowed, ResizeLeftRight, ResizeUpDown, + Custom(platform::window::CustomCursor), +} + +/// A platform-independent description of a custom cursor. +#[derive(Clone)] +pub struct CursorDesc { + pub(crate) image: ImageBuf, + pub(crate) hot: Point, +} + +impl CursorDesc { + /// Creates a new `CursorDesc`. + /// + /// `hot` is the "hot spot" of the cursor, measured in terms of the pixels in `image` with + /// `(0, 0)` at the top left. The hot spot is the logical position of the mouse cursor within + /// the image. For example, if the image is a picture of a arrow, the hot spot might be the + /// coordinates of the arrow's tip. + pub fn new(image: ImageBuf, hot: impl Into) -> CursorDesc { + CursorDesc { + image, + hot: hot.into(), + } + } } diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index 04f3dfdeae..bce147470e 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -39,7 +39,8 @@ use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; use crate::error::Error as ShellError; use crate::keyboard::{KbKey, KeyEvent, KeyState, Modifiers}; -use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; +use crate::piet::ImageFormat; use crate::region::Region; use crate::scale::{Scalable, Scale, ScaledArea}; use crate::window; @@ -146,6 +147,9 @@ pub(crate) struct WindowState { current_keycode: RefCell>, } +#[derive(Clone)] +pub struct CustomCursor(gdk::Cursor); + impl WindowBuilder { pub fn new(app: Application) -> WindowBuilder { WindowBuilder { @@ -852,6 +856,38 @@ impl WindowHandle { } } + pub fn make_cursor(&self, desc: &CursorDesc) -> Option { + if let Some(state) = self.state.upgrade() { + if let Some(gdk_window) = state.window.get_window() { + // TODO: gtk::Pixbuf expects unpremultiplied alpha. We should convert. + let has_alpha = !matches!(desc.image.format(), ImageFormat::Rgb); + let bytes_per_pixel = desc.image.format().bytes_per_pixel(); + let pixbuf = gdk_pixbuf::Pixbuf::from_mut_slice( + desc.image.raw_pixels().to_owned(), + gdk_pixbuf::Colorspace::Rgb, + has_alpha, + // bits_per_sample + 8, + desc.image.width() as i32, + desc.image.height() as i32, + // row stride (in bytes) + (desc.image.width() * bytes_per_pixel) as i32, + ); + let c = gdk::Cursor::from_pixbuf( + &gdk_window.get_display(), + &pixbuf, + desc.hot.x.round() as i32, + desc.hot.y.round() as i32, + ); + Some(Cursor::Custom(CustomCursor(c))) + } else { + None + } + } else { + None + } + } + pub fn open_file_sync(&mut self, options: FileDialogOptions) -> Option { self.file_dialog(FileDialogType::Open, options) .ok() @@ -1000,19 +1036,24 @@ fn run_idle(state: &Arc) -> glib::source::Continue { } fn make_gdk_cursor(cursor: &Cursor, gdk_window: &gdk::Window) -> Option { - gdk::Cursor::from_name( - &gdk_window.get_display(), - match cursor { - // cursor name values from https://www.w3.org/TR/css-ui-3/#cursor - Cursor::Arrow => "default", - Cursor::IBeam => "text", - Cursor::Crosshair => "crosshair", - Cursor::OpenHand => "grab", - Cursor::NotAllowed => "not-allowed", - Cursor::ResizeLeftRight => "ew-resize", - Cursor::ResizeUpDown => "ns-resize", - }, - ) + if let Cursor::Custom(custom) = cursor { + Some(custom.0.clone()) + } else { + gdk::Cursor::from_name( + &gdk_window.get_display(), + match cursor { + // cursor name values from https://www.w3.org/TR/css-ui-3/#cursor + Cursor::Arrow => "default", + Cursor::IBeam => "text", + Cursor::Crosshair => "crosshair", + Cursor::OpenHand => "grab", + Cursor::NotAllowed => "not-allowed", + Cursor::ResizeLeftRight => "ew-resize", + Cursor::ResizeUpDown => "ns-resize", + Cursor::Custom(_) => unreachable!(), + }, + ) + } } fn get_mouse_button(button: u32) -> Option { diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 47d7eb975a..1e6346e085 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -53,7 +53,7 @@ use super::util::{assert_main_thread, make_nsstring}; use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; use crate::keyboard_types::KeyState; -use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::scale::Scale; use crate::window::{IdleToken, TimerToken, WinHandler, WindowLevel, WindowState}; @@ -145,6 +145,10 @@ struct ViewState { text: PietText, } +#[derive(Clone)] +// TODO: support custom cursors +pub struct CustomCursor; + impl WindowBuilder { pub fn new(_app: Application) -> WindowBuilder { WindowBuilder { @@ -888,11 +892,18 @@ impl WindowHandle { Cursor::NotAllowed => msg_send![nscursor, operationNotAllowedCursor], Cursor::ResizeLeftRight => msg_send![nscursor, resizeLeftRightCursor], Cursor::ResizeUpDown => msg_send![nscursor, resizeUpDownCursor], + // TODO: support custom cursors + Cursor::Custom(_) => msg_send![nscursor, arrowCursor], }; let () = msg_send![cursor, set]; } } + pub fn make_cursor(&self, _cursor_desc: &CursorDesc) -> Option { + log::warn!("Custom cursors are not yet supported in the macOS backend"); + None + } + pub fn request_timer(&self, deadline: std::time::Instant) -> TimerToken { let ti = time_interval_from_deadline(deadline); let token = TimerToken::next(); diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index d5f9e293cc..497f8b955c 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -39,7 +39,7 @@ use crate::error::Error as ShellError; use crate::scale::{Scale, ScaledArea}; use crate::keyboard::{KbKey, KeyState, Modifiers}; -use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::window; use crate::window::{IdleToken, TimerToken, WinHandler, WindowLevel}; @@ -99,6 +99,10 @@ struct WindowState { invalid: RefCell, } +// TODO: support custom cursors +#[derive(Clone)] +pub struct CustomCursor; + impl WindowState { fn render(&self) { self.handler.borrow_mut().prepare_paint(); @@ -555,6 +559,11 @@ impl WindowHandle { } } + pub fn make_cursor(&self, _cursor_desc: &CursorDesc) -> Option { + log::warn!("Custom cursors are not yet supported in the web backend"); + None + } + pub fn open_file_sync(&mut self, options: FileDialogOptions) -> Option { log::warn!("open_file_sync is currently unimplemented for web."); self.file_dialog(FileDialogType::Open, options) @@ -704,6 +713,8 @@ fn set_cursor(canvas: &web_sys::HtmlCanvasElement, cursor: &Cursor) { Cursor::NotAllowed => "not-allowed", Cursor::ResizeLeftRight => "ew-resize", Cursor::ResizeUpDown => "ns-resize", + // TODO: support custom cursors + Cursor::Custom(_) => "default", }, ) .unwrap_or_else(|_| log::warn!("Failed to set cursor")); diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index dae0cceff7..29fccfaf4f 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -25,6 +25,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use log::{debug, error, warn}; +use scopeguard::defer; use winapi::ctypes::{c_int, c_void}; use winapi::shared::dxgi::*; use winapi::shared::dxgi1_2::*; @@ -36,6 +37,7 @@ use winapi::shared::winerror::*; use winapi::um::errhandlingapi::GetLastError; use winapi::um::shellscalingapi::MDT_EFFECTIVE_DPI; use winapi::um::unknwnbase::*; +use winapi::um::wingdi::*; use winapi::um::winnt::*; use winapi::um::winuser::*; @@ -60,7 +62,7 @@ use crate::common_util::IdleCallback; use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo}; use crate::error::Error as ShellError; use crate::keyboard::{KbKey, KeyState}; -use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; use crate::region::Region; use crate::scale::{Scalable, Scale, ScaledArea}; use crate::window; @@ -335,6 +337,19 @@ struct DxgiState { swap_chain: *mut IDXGISwapChain1, } +#[derive(Clone)] +pub struct CustomCursor(Arc); + +struct HCursor(HCURSOR); + +impl Drop for HCursor { + fn drop(&mut self) { + unsafe { + DestroyIcon(self.0); + } + } +} + /// Message indicating there are idle tasks to run. const DS_RUN_IDLE: UINT = WM_USER; @@ -1476,8 +1491,8 @@ unsafe fn create_window( } impl Cursor { - fn get_lpcwstr(&self) -> LPCWSTR { - match self { + fn get_hcursor(&self) -> HCURSOR { + let name = match self { Cursor::Arrow => IDC_ARROW, Cursor::IBeam => IDC_IBEAM, Cursor::Crosshair => IDC_CROSS, @@ -1485,7 +1500,11 @@ impl Cursor { Cursor::NotAllowed => IDC_NO, Cursor::ResizeLeftRight => IDC_SIZEWE, Cursor::ResizeUpDown => IDC_SIZENS, - } + Cursor::Custom(c) => { + return (c.0).0; + } + }; + unsafe { LoadCursorW(0 as HINSTANCE, name) } } } @@ -1743,8 +1762,76 @@ impl WindowHandle { /// Set the cursor icon. pub fn set_cursor(&mut self, cursor: &Cursor) { unsafe { - let cursor = LoadCursorW(0 as HINSTANCE, cursor.get_lpcwstr()); - SetCursor(cursor); + SetCursor(cursor.get_hcursor()); + } + } + + pub fn make_cursor(&self, cursor_desc: &CursorDesc) -> Option { + if let Some(hwnd) = self.get_hwnd() { + unsafe { + let hdc = GetDC(hwnd); + if hdc.is_null() { + return None; + } + defer!(ReleaseDC(null_mut(), hdc);); + + let mask_dc = CreateCompatibleDC(hdc); + if mask_dc.is_null() { + return None; + } + defer!(DeleteDC(mask_dc);); + + let bmp_dc = CreateCompatibleDC(hdc); + if bmp_dc.is_null() { + return None; + } + defer!(DeleteDC(bmp_dc);); + + let width = cursor_desc.image.width(); + let height = cursor_desc.image.height(); + let mask = CreateCompatibleBitmap(hdc, width as c_int, height as c_int); + if mask.is_null() { + return None; + } + defer!(DeleteObject(mask as _);); + + let bmp = CreateCompatibleBitmap(hdc, width as c_int, height as c_int); + if bmp.is_null() { + return None; + } + defer!(DeleteObject(bmp as _);); + + let old_mask = SelectObject(mask_dc, mask as *mut c_void); + let old_bmp = SelectObject(bmp_dc, bmp as *mut c_void); + + for (row_idx, row) in cursor_desc.image.pixel_colors().enumerate() { + for (col_idx, p) in row.enumerate() { + let (r, g, b, a) = p.as_rgba8(); + // TODO: what's the story on partial transparency? I couldn't find documentation. + let mask_px = RGB(255 - a, 255 - a, 255 - a); + let bmp_px = RGB(r, g, b); + SetPixel(mask_dc, col_idx as i32, row_idx as i32, mask_px); + SetPixel(bmp_dc, col_idx as i32, row_idx as i32, bmp_px); + } + } + + SelectObject(mask_dc, old_mask); + SelectObject(bmp_dc, old_bmp); + + let mut icon_info = ICONINFO { + // 0 means it's a cursor, not an icon. + fIcon: 0, + xHotspot: cursor_desc.hot.x as DWORD, + yHotspot: cursor_desc.hot.y as DWORD, + hbmMask: mask, + hbmColor: bmp, + }; + let icon = CreateIconIndirect(&mut icon_info); + + Some(Cursor::Custom(CustomCursor(Arc::new(HCursor(icon))))) + } + } else { + None } } diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 7d1b89efb2..24e1410240 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -41,7 +41,7 @@ use crate::dialog::{FileDialogOptions, FileInfo}; use crate::error::Error as ShellError; use crate::keyboard::{KeyEvent, KeyState, Modifiers}; use crate::kurbo::{Point, Rect, Size, Vec2}; -use crate::mouse::{Cursor, MouseButton, MouseButtons, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; use crate::piet::{Piet, PietText, RenderContext}; use crate::region::Region; use crate::scale::Scale; @@ -515,6 +515,9 @@ struct PresentData { last_ust: Option, } +#[derive(Clone)] +pub struct CustomCursor(xproto::Cursor); + impl Window { fn connect(&self, handle: WindowHandle) -> Result<(), Error> { let mut handler = borrow_mut!(self.handler)?; @@ -1464,6 +1467,11 @@ impl WindowHandle { // TODO(x11/cursors): implement WindowHandle::set_cursor } + pub fn make_cursor(&self, _cursor_desc: &CursorDesc) -> Option { + log::warn!("Custom cursors are not yet supported in the X11 backend"); + None + } + pub fn open_file_sync(&mut self, _options: FileDialogOptions) -> Option { // TODO(x11/file_dialogs): implement WindowHandle::open_file_sync log::warn!("WindowHandle::open_file_sync is currently unimplemented for X11 platforms."); diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index f19afef691..e83f02df31 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -24,7 +24,7 @@ use crate::error::Error; use crate::keyboard::KeyEvent; use crate::kurbo::{Point, Rect, Size}; use crate::menu::Menu; -use crate::mouse::{Cursor, MouseEvent}; +use crate::mouse::{Cursor, CursorDesc, MouseEvent}; use crate::platform::window as platform; use crate::region::Region; use crate::scale::Scale; @@ -286,6 +286,10 @@ impl WindowHandle { self.0.set_cursor(cursor) } + pub fn make_cursor(&self, desc: &CursorDesc) -> Option { + self.0.make_cursor(desc) + } + /// Prompt the user to chose a file to open. /// /// Blocks while the user picks the file. diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 899a5e8796..484f6a0e83 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -56,6 +56,10 @@ float-cmp = { version = "0.8.0", features = ["std"], default-features = false } tempfile = "3.1.0" piet-common = { version = "0.2.0-pre4", features = ["png"] } +[[example]] +name = "cursor" +required-features = ["image"] + [[example]] name = "image" required-features = ["image"] diff --git a/druid/examples/cursor.rs b/druid/examples/cursor.rs new file mode 100644 index 0000000000..b8aaf3d7d1 --- /dev/null +++ b/druid/examples/cursor.rs @@ -0,0 +1,98 @@ +// Copyright 2020 The Druid 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. + +//! An example showing how to change the mouse cursor. + +use druid::{ + AppLauncher, Color, Cursor, CursorDesc, Data, Env, ImageBuf, Lens, LocalizedString, Widget, + WidgetExt, WindowDesc, +}; + +use druid::widget::prelude::*; +use druid::widget::{Button, Controller}; + +use std::sync::Arc; + +#[derive(Clone, Data, Lens)] +struct AppState { + cursor: Arc, + custom_desc: Arc, + custom: Option>, +} + +fn next_cursor(c: &Cursor, custom: &Option>) -> Cursor { + match c { + Cursor::Arrow => Cursor::IBeam, + Cursor::IBeam => Cursor::Crosshair, + Cursor::Crosshair => Cursor::OpenHand, + Cursor::OpenHand => Cursor::NotAllowed, + Cursor::NotAllowed => Cursor::ResizeLeftRight, + Cursor::ResizeLeftRight => Cursor::ResizeUpDown, + Cursor::ResizeUpDown => { + if let Some(custom) = custom { + Cursor::clone(&custom) + } else { + Cursor::Arrow + } + } + Cursor::Custom(_) => Cursor::Arrow, + } +} + +struct CursorArea {} + +impl> Controller for CursorArea { + fn event( + &mut self, + child: &mut W, + ctx: &mut EventCtx, + event: &Event, + data: &mut AppState, + env: &Env, + ) { + if data.custom.is_none() { + data.custom = ctx.window().make_cursor(&data.custom_desc).map(Arc::new); + } + if matches!(event, Event::MouseMove(_)) { + ctx.set_cursor(&data.cursor); + } + child.event(ctx, event, data, env); + } +} + +fn ui_builder() -> impl Widget { + Button::new("Change cursor") + .on_click(|ctx, data: &mut AppState, _env| { + data.cursor = Arc::new(next_cursor(&data.cursor, &data.custom)); + ctx.set_cursor(&data.cursor); + }) + .padding(50.0) + .controller(CursorArea {}) + .border(Color::WHITE, 1.0) + .padding(50.0) +} + +pub fn main() { + let main_window = WindowDesc::new(ui_builder).title(LocalizedString::new("Blocking functions")); + let cursor_image = ImageBuf::from_data(include_bytes!("./assets/PicWithAlpha.png")).unwrap(); + let custom_desc = CursorDesc::new(cursor_image, (50.0, 50.0)); + let data = AppState { + cursor: Arc::new(Cursor::Arrow), + custom: None, + custom_desc: Arc::new(custom_desc), + }; + + let app = AppLauncher::with_window(main_window); + app.use_simple_logger().launch(data).expect("launch failed"); +} diff --git a/druid/examples/web/src/lib.rs b/druid/examples/web/src/lib.rs index 862dad51ac..accaa025aa 100644 --- a/druid/examples/web/src/lib.rs +++ b/druid/examples/web/src/lib.rs @@ -56,6 +56,7 @@ macro_rules! impl_example { // Please add the examples that cannot be built to the EXCEPTIONS list in build.rs. impl_example!(anim); impl_example!(calc); +impl_example!(cursor); impl_example!(custom_widget); impl_example!(either); impl_example!(flex.unwrap()); diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 540813325a..448a4b3173 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -177,7 +177,7 @@ pub use piet::{ // these are the types from shell that we expose; others we only use internally. pub use shell::keyboard_types; pub use shell::{ - Application, Clipboard, ClipboardFormat, Code, Cursor, Error as PlatformError, + Application, Clipboard, ClipboardFormat, Code, Cursor, CursorDesc, Error as PlatformError, FileDialogOptions, FileInfo, FileSpec, FormatId, HotKey, ImageBuf, KbKey, KeyEvent, Location, Modifiers, Monitor, MouseButton, MouseButtons, RawMods, Region, Scalable, Scale, Screen, SysMods, TimerToken, WindowHandle, WindowState,