Skip to content

Commit

Permalink
Custom cursor (#1183)
Browse files Browse the repository at this point in the history
* Adds an ImageBuf type to druid-shell, replacing ImageData in druid.

* Add custom cursors, implemented on GTK and windows.
  • Loading branch information
jneem authored Sep 30, 2020
1 parent b320ec2 commit 59f6750
Show file tree
Hide file tree
Showing 19 changed files with 598 additions and 201 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -61,6 +62,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])
Expand Down Expand Up @@ -459,6 +461,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
Expand Down
9 changes: 7 additions & 2 deletions druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +32,11 @@ 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]
scopeguard = "1.1.0"
wio = "0.2.2"

[target.'cfg(target_os="windows")'.dependencies.winapi]
Expand All @@ -55,6 +59,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 }
Expand Down
219 changes: 219 additions & 0 deletions druid-shell/src/image.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<[u8]>>,
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<Item = impl Iterator<Item = Color> + '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<Ctx: RenderContext>(&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<ImageBuf, Box<dyn Error>> {
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<P: AsRef<Path>>(path: P) -> Result<ImageBuf, Box<dyn Error>> {
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()
}
}
4 changes: 3 additions & 1 deletion druid-shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod common_util;
mod dialog;
mod error;
mod hotkey;
mod image;
mod keyboard;
mod menu;
mod mouse;
Expand All @@ -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;
Expand All @@ -65,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};
Expand Down
26 changes: 25 additions & 1 deletion druid-shell/src/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<Point>) -> CursorDesc {
CursorDesc {
image,
hot: hot.into(),
}
}
}
Loading

0 comments on commit 59f6750

Please sign in to comment.