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

Svg and icon support #111

Merged
merged 11 commits into from
Dec 16, 2019
3 changes: 3 additions & 0 deletions native/src/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod button;
pub mod checkbox;
pub mod column;
pub mod container;
pub mod icon;
pub mod image;
pub mod radio;
pub mod row;
Expand All @@ -41,6 +42,8 @@ pub use column::Column;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
pub use icon::Icon;
#[doc(no_inline)]
pub use image::Image;
#[doc(no_inline)]
pub use radio::Radio;
Expand Down
100 changes: 100 additions & 0 deletions native/src/widget/icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Display an icon.
use crate::{
layout, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};

use std::{
hash::Hash,
path::{Path, PathBuf},
};

/// A simple icon_loader widget.
#[derive(Debug, Clone)]
pub struct Icon {
path: PathBuf,
size: Length,
}
Copy link
Member

Choose a reason for hiding this comment

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

I would name this widget Svg and let it be configurable both in width and height.

Copy link
Member

@hecrj hecrj Dec 11, 2019

Choose a reason for hiding this comment

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

After thinking a bit about it, it may make sense to merge this with the Image widget. We will probably want to keep the SVG aspect ratio after all.

We can check the extension as you were doing and have an additional Memory variant in image to deal with caching aware of DPI and viewport dimensions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I said, that widget can handle svg as well as png or other files since some icons still come in png format. So I think naming it Svg would be rather misleading about its capabilities.
I also thought that since icons are usually squares, giving it width and height would be unnecessary and a single size attribute simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking about the image widget too. I would like to give the user more control about aspect ratio of the widget. Maybe an enum with KeepAspectRatio, Stretch and KeepAspectRatioCrop at least. But icons don't need that.
Also having a separate widget makes integrating themed icons easier in the future.

Copy link
Member

Choose a reason for hiding this comment

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

What an icon is depends on each particular application.

To keep things simple, I was proposing that we should instead expose a widget that is able to render only SVG with configurable dimensions. One that can be used to render SVG directly, not just icons.

This way, you could easily create a function icon in your application that chooses Svg or an Image widget based on the path, sets square dimensions, and returns an Element.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now the Icon widget does all that for you. You don't have to create an icon function yourself.
It's very easy to use.

Copy link
Member

@hecrj hecrj Dec 11, 2019

Choose a reason for hiding this comment

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

Convenient, but not simple.

If our renderer supports SVG, we need a widget that can leverage it to its full extent, not just squares.

For instance, I may want to draw a logo using SVG. I won't be able to use Icon for that unless the logo is a square.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I could easily merge the svg capability into Image. If simplicity is the main goal here, I could try to merge the two pipelines too. That shouldn't be too hard.

Copy link
Member

Choose a reason for hiding this comment

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

I am not completely sure about it.

I like the idea of having a dedicated Svg widget. It makes everything more explicit and forces users to keep in mind Svg and Image have different performance implications. It may also help us in the long run to feature-gate some particular widgets and reduce binary size. I'd say let's keep it as different widgets for now.

However, I believe we can reuse the image pipeline by adding a new variant to the Memory enum to handle the Svg primitive. This should reduce the scope of the changes too.

For instance:

enum Memory {
    Host(HostMemory),
    Device {
        bind_group: Rc<wgpu::BindGroup>,
        width: u32,
        height: u32,
    },
    NotFound,
    Invalid,
}

enum HostMemory {
    Image(image::ImageBuffer<image::Bgra<u8>, Vec<u8>>),
    Svg { /* ... */ },
}


impl Icon {
/// Create a new [`Icon`] from the file at `path`.
///
/// [`Icon`]: struct.Icon.html
pub fn new(path: impl Into<PathBuf>) -> Self {
Icon {
path: path.into(),
size: Length::Fill,
}
}

/// Sets the size of the [`Icon`].
///
/// [`Icon`]: struct.Icon.html
pub fn size(mut self, size: Length) -> Self {
self.size = size;
self
}
}

impl<Message, Renderer> Widget<Message, Renderer> for Icon
where
Renderer: self::Renderer,
{
fn width(&self) -> Length {
self.size
}

fn height(&self) -> Length {
self.size
}

fn layout(&self, _: &Renderer, limits: &layout::Limits) -> layout::Node {
let mut size = limits.width(self.size).height(self.size).max();

if size.width > size.height {
size.width = size.height;
} else if size.width < size.height {
size.height = size.width;
}

layout::Node::new(size)
}

fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> Renderer::Output {
let bounds = layout.bounds();

renderer.draw(bounds, self.path.as_path())
}

fn hash_layout(&self, state: &mut Hasher) {
self.size.hash(state);
}
}

/// The renderer of an [`Icon`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use [`Icon`] in your [`UserInterface`].
///
/// [`Icon`]: struct.Icon.html
/// [renderer]: ../../renderer/index.html
/// [`UserInterface`]: ../../struct.UserInterface.html
pub trait Renderer: crate::Renderer {
/// Draws an [`Icon`].
///
/// [`Icon`]: struct.Icon.html
fn draw(&mut self, bounds: Rectangle, path: &Path) -> Self::Output;
}

impl<'a, Message, Renderer> From<Icon> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
{
fn from(icon: Icon) -> Element<'a, Message, Renderer> {
Element::new(icon)
}
}
9 changes: 7 additions & 2 deletions src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ pub mod widget {
pub use iced_winit::image::{Handle, Image};
}

pub mod icon {
//! Display icons in your user interface.
pub use iced_winit::icon::Icon;
}

pub use iced_winit::{Checkbox, Radio, Text};

#[doc(no_inline)]
pub use {
button::Button, image::Image, scrollable::Scrollable, slider::Slider,
text_input::TextInput,
button::Button, icon::Icon, image::Image, scrollable::Scrollable,
slider::Slider, text_input::TextInput,
};

/// A container that distributes its contents vertically.
Expand Down
1 change: 1 addition & 0 deletions wgpu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ image = "0.22"
glam = "0.8"
font-kit = "0.4"
log = "0.4"
resvg = { version = "0.8", features = ["raqote-backend"] }
2 changes: 2 additions & 0 deletions wgpu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ mod image;
mod primitive;
mod quad;
mod renderer;
mod svg;
mod text;
mod transformation;

pub(crate) use crate::image::Image;
pub(crate) use quad::Quad;
pub(crate) use svg::Svg;
pub(crate) use transformation::Transformation;

pub use primitive::Primitive;
Expand Down
7 changes: 7 additions & 0 deletions wgpu/src/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ pub enum Primitive {
/// The bounds of the image
bounds: Rectangle,
},
/// A svg icon primitive
Svg {
/// The path of the icon
handle: crate::svg::Handle,
/// The bounds of the icon
bounds: Rectangle,
},
/// A clip primitive
Clip {
/// The bounds of the clip
Expand Down
32 changes: 31 additions & 1 deletion wgpu/src/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{quad, text, Image, Primitive, Quad, Transformation};
use crate::{quad, text, Image, Primitive, Quad, Svg, Transformation};
use iced_native::{
renderer::{Debugger, Windowed},
Background, Color, Layout, MouseCursor, Point, Rectangle, Vector, Widget,
Expand All @@ -23,6 +23,7 @@ pub struct Renderer {
queue: Queue,
quad_pipeline: quad::Pipeline,
image_pipeline: crate::image::Pipeline,
svg_pipeline: crate::svg::Pipeline,
text_pipeline: text::Pipeline,
}

Expand All @@ -31,6 +32,7 @@ struct Layer<'a> {
offset: Vector<u32>,
quads: Vec<Quad>,
images: Vec<Image>,
svgs: Vec<Svg>,
text: Vec<wgpu_glyph::Section<'a>>,
}

Expand All @@ -41,6 +43,7 @@ impl<'a> Layer<'a> {
offset,
quads: Vec::new(),
images: Vec::new(),
svgs: Vec::new(),
text: Vec::new(),
}
}
Expand All @@ -64,12 +67,14 @@ impl Renderer {
let text_pipeline = text::Pipeline::new(&mut device);
let quad_pipeline = quad::Pipeline::new(&mut device);
let image_pipeline = crate::image::Pipeline::new(&mut device);
let svg_pipeline = crate::svg::Pipeline::new(&mut device);

Self {
device,
queue,
quad_pipeline,
image_pipeline,
svg_pipeline,
text_pipeline,
}
}
Expand Down Expand Up @@ -128,6 +133,7 @@ impl Renderer {

self.queue.submit(&[encoder.finish()]);
self.image_pipeline.trim_cache();
self.svg_pipeline.trim_cache();

*mouse_cursor
}
Expand Down Expand Up @@ -237,6 +243,11 @@ impl Renderer {
scale: [bounds.width, bounds.height],
});
}
Primitive::Svg { handle, bounds } => layer.svgs.push(Svg {
handle: handle.clone(),
position: [bounds.x, bounds.y],
scale: [bounds.width, bounds.height],
}),
Primitive::Clip {
bounds,
offset,
Expand Down Expand Up @@ -345,6 +356,25 @@ impl Renderer {
);
}

if layer.svgs.len() > 0 {
let translated = transformation
* Transformation::translate(
-(layer.offset.x as f32),
-(layer.offset.y as f32),
);

self.svg_pipeline.draw(
&mut self.device,
encoder,
&layer.svgs,
translated,
bounds,
target,
(dpi * 96.0) as u16,
(dpi * 20.0) as u16,
);
}

if layer.text.len() > 0 {
for text in layer.text.iter() {
// Target physical coordinates directly to avoid blurry text
Expand Down
1 change: 1 addition & 0 deletions wgpu/src/renderer/widget.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod button;
mod checkbox;
mod column;
mod icon;
mod image;
mod radio;
mod row;
Expand Down
15 changes: 15 additions & 0 deletions wgpu/src/renderer/widget/icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use crate::{svg::Handle, Primitive, Renderer};
use iced_native::{icon, MouseCursor, Rectangle};
use std::path::Path;

impl icon::Renderer for Renderer {
fn draw(&mut self, bounds: Rectangle, path: &Path) -> Self::Output {
(
Primitive::Svg {
handle: Handle::from_path(path),
bounds,
},
MouseCursor::OutOfBounds,
)
}
}
Loading