Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom cursor #1183

Merged
merged 2 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -59,6 +60,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 @@ -456,6 +458,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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see this in the piet util module?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll make a piet PR

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`].
jneem marked this conversation as resolved.
Show resolved Hide resolved
///
/// [`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