Skip to content

Commit

Permalink
Add generic image widget
Browse files Browse the repository at this point in the history
  • Loading branch information
fishrockz committed Feb 16, 2020
1 parent d7a7a6e commit 41b8ac4
Show file tree
Hide file tree
Showing 8 changed files with 602 additions and 2 deletions.
286 changes: 286 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions druid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ log = "0.4.8"
usvg = {version = "0.9.0", optional = true}
fnv = "1.0.3"
xi-unicode = "0.2.0"
image = {version = "0.22.4", optional = true}

[dependencies.simple_logger]
version = "1.3.0"
Expand Down
58 changes: 58 additions & 0 deletions druid/examples/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.

//! This example shows how to draw an png image.
//!
//! Requires the non-default "image" feature to be enabled:
//! `cargo run --example image --features "images"`
//!

#[cfg(not(feature = "image"))]
fn main() {
eprintln!("This examples requires the \"image\" feature to be enabled:");
eprintln!("cargo run --example image --features \"image\"");
}

#[cfg(feature = "image")]
fn main() {
use druid::{
widget::{FillStrat, Flex, Image, ImageData, WidgetExt},
AppLauncher, Widget, WindowDesc,
};

fn ui_builder() -> impl Widget<u32> {
let png_data = ImageData::from_file("examples/pngexample.png").unwrap();

let mut col = Flex::column();

col.add_child(Image::new(png_data.clone()).fix_width(100.0).center(), 1.0);

/*
// If you want to change the fill stratagy you can but you need the widget to be mut
let mut otherimage = Image::new(png_data);
otherimage.set_fill(FillStrat::FitWidth);
*/

let otherimage = Image::new(png_data).fill_mode(FillStrat::FitWidth);
col.add_child(otherimage, 1.0);
col
};

let main_window = WindowDesc::new(ui_builder);
let data = 0_u32;
AppLauncher::with_window(main_window)
.use_simple_logger()
.launch(data)
.expect("launch failed");
}
Binary file added druid/examples/pngexample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ impl std::fmt::Display for ArgumentError {
ArgumentError::WrongVariant => write!(
f,
"Incorrect access method for argument type; \
check Command::one_shot docs for more detail."
check Command::one_shot docs for more detail."
),
}
}
Expand Down
2 changes: 1 addition & 1 deletion druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
if self.old_data.is_none() {
log::error!(
"widget {:?} is receiving an event without having first \
recieved WidgetAdded.",
recieved WidgetAdded.",
ctx.widget_id()
);
}
Expand Down
249 changes: 249 additions & 0 deletions druid/src/widget/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// 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.

//! An Image widget.
//! Please consider using SVG and the SVG wideget as it scales much better.

use std::convert::AsRef;
use std::error::Error;
use std::marker::PhantomData;
use std::path::Path;

use image;

use crate::{
Affine, BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
PaintCtx, Point, Rect, RenderContext, Size, UpdateCtx, Widget,
};

use crate::piet::{ImageFormat, InterpolationMode};

// These are based on https://api.flutter.dev/flutter/painting/BoxFit-class.html
#[derive(Clone, Copy, PartialEq)]
pub enum FillStrat {
/// As large as posible without changing aspect ratio of image and all of image shown
Contain,
/// As large as posible with no dead space so that some of the image may be clipped
Cover,
/// Fill the widget with no dead space, aspect ratio of widget is used
Fill,
/// Fill the hight with the images aspect ratio, some of the image may be clipped
FitHeight,
/// Fill the width with the images aspect ratio, some of the image may be clipped
FitWidth,
/// Do not scale
None,
/// Scale down to fit but do not scale up
ScaleDown,
}

impl Default for FillStrat {
fn default() -> Self {
FillStrat::Contain
}
}

/// Calculate an origin and scale for an image with a given `FillStrat`.
///
/// This takes some propities of a widget and a fill Stratagy and returns a Affine matrix
/// to use to position and scale the image in the widget.
fn get_affine_from_fill(parent: Size, fit_box: Size, fit_type: FillStrat) -> Affine {
let scalex = parent.width / fit_box.width;
let scaley = parent.height / fit_box.height;

let scale: Point = match fit_type {
FillStrat::Contain => {
let scale = scalex.min(scaley);
Point { x: scale, y: scale }
}
FillStrat::Cover => {
let scale = scalex.max(scaley);
Point { x: scale, y: scale }
}
FillStrat::Fill => Point {
x: scalex,
y: scaley,
},
FillStrat::FitHeight => Point {
x: scaley,
y: scaley,
},
FillStrat::FitWidth => Point {
x: scalex,
y: scalex,
},
FillStrat::ScaleDown => {
let scale = scalex.min(scaley).min(1.0);
Point { x: scale, y: scale }
}
FillStrat::None => Point { x: 1.0, y: 1.0 },
};

let origin_x = (parent.width - (fit_box.width * scale.x)) / 2.0;
let origin_y = (parent.height - (fit_box.height * scale.y)) / 2.0;
let origin = Point::new(origin_x, origin_y);

Affine::new([scale.x, 0., 0., scale.y, origin.x, origin.y])
}

/// A widget that renders an Image
pub struct Image<T> {
image_data: ImageData,
phantom: PhantomData<T>,
fill: FillStrat,
}

impl<T: Data> Image<T> {
/// Create an image drawing widget from `ImageData`.
///
/// The Image will scale to fit its box constraints.
pub fn new(image_data: ImageData) -> Self {
Image {
image_data,
phantom: Default::default(),
fill: FillStrat::default(),
}
}

/// Change the fill stratacy of a newly constructed Image Widget
pub fn fill_mode(mut self, mode: FillStrat) -> Self {
self.fill = mode;
self
}

/// Change the fill stratagy of a Image Widgit inplace
pub fn set_fill(&mut self, newfil: FillStrat) {
self.fill = newfil;
}
}

impl<T: Data> Widget<T> for Image<T> {
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 update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _env: &Env) {}

fn layout(
&mut self,
_layout_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
_data: &T,
_env: &Env,
) -> Size {
bc.debug_check("Image");

if bc.is_width_bounded() {
bc.max()
} else {
bc.constrain(self.image_data.get_size())
}
}

fn paint(&mut self, paint_ctx: &mut PaintCtx, _data: &T, _env: &Env) {
let offset_matrix =
get_affine_from_fill(paint_ctx.size(), self.image_data.get_size(), self.fill);

// 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
if self.fill == FillStrat::Contain {
} else {
let clip_rect = Rect::ZERO.with_size(paint_ctx.size());
paint_ctx.clip(clip_rect);
}
self.image_data.to_piet(offset_matrix, paint_ctx);
}
}

/// Stored Image data.
/// Implements `FromStr` and can be converted to piet draw instructions.
#[derive(Clone)]
pub struct ImageData {
pixels: Vec<u8>,
x_pixels: u32,
y_pixels: u32,
}

impl ImageData {
/// Create an empty Image
pub fn empty() -> Self {
ImageData {
pixels: [].to_vec(),
x_pixels: 0,
y_pixels: 0,
}
}

/// This takes the raw binary contents of a file, eg png, jpg and returns a image
///
/// 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<Self, Box<dyn Error>> {
let dec = image::load_from_memory(raw_image).map_err(|e| e)?.to_rgb();

let sizeofimage = dec.dimensions();
Ok(ImageData {
pixels: dec.to_vec(),
x_pixels: sizeofimage.0,
y_pixels: sizeofimage.1,
})
}

/// Creates a Imagedata from the path to a file, eg the path to a png file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn Error>> {
let image_data = image::open(path).map_err(|e| e)?.to_rgb();

let sizeofimage = image_data.dimensions();
Ok(ImageData {
pixels: image_data.to_vec(),
x_pixels: sizeofimage.0,
y_pixels: sizeofimage.1,
})
}

/// 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, offset_matrix: Affine, paint_ctx: &mut PaintCtx) {
paint_ctx
.with_save(|ctx| {
ctx.transform(offset_matrix);

let im = ctx
.make_image(
self.x_pixels as usize,
self.y_pixels as usize,
&self.pixels,
ImageFormat::Rgb,
)
.unwrap();
let rec = Rect::from_origin_size(
(0.0, 0.0),
(self.x_pixels as f64, self.y_pixels as f64),
);
ctx.draw_image(&im, rec, InterpolationMode::Bilinear);

Ok(())
})
.unwrap();
}
}

impl Default for ImageData {
fn default() -> Self {
ImageData::empty()
}
}
6 changes: 6 additions & 0 deletions druid/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ mod either;
mod env_scope;
mod flex;
mod identity_wrapper;
#[cfg(feature = "image")]
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
mod image;
mod label;
mod list;
mod padding;
Expand All @@ -41,6 +44,9 @@ mod textbox;
mod view_switcher;
mod widget_ext;

#[cfg(feature = "image")]
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
pub use self::image::{FillStrat, Image, ImageData};
pub use align::Align;
pub use button::Button;
pub use checkbox::Checkbox;
Expand Down

0 comments on commit 41b8ac4

Please sign in to comment.