Merge pull request #17 from hecrj/web

Basic web support (core, native, and web crates)
This commit is contained in:
Héctor Ramón 2019-09-24 15:39:33 +02:00 committed by GitHub
commit 68c4752e99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 3448 additions and 2791 deletions

View file

@ -7,10 +7,15 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
rust: [stable, beta]
include:
- os: ubuntu-latest
rust: stable
targets: 'wasm32-unknown-unknown'
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: ${{ matrix.rust }}
targets: ${{ matrix.targets }}
- name: Install libinput
if: matrix.os == 'ubuntu-latest'
run: |
@ -18,4 +23,7 @@ jobs:
sudo apt-get install -y libasound2-dev libudev-dev
- uses: actions/checkout@master
- name: Run tests
run: cargo test --verbose --all-features
run: cargo test --verbose --all --all-features
- name: Build tour for WebAssembly
if: matrix.targets == 'wasm32-unknown-unknown'
run: cargo build --verbose --package iced_tour --lib --target wasm32-unknown-unknown

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
pkg/
**/*.rs.bk
Cargo.lock

View file

@ -3,7 +3,7 @@ name = "iced"
version = "0.1.0-alpha"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A GUI runtime, heavily inspired by Elm."
description = "A cross-platform GUI library inspired by Elm"
license = "MIT"
repository = "https://github.com/hecrj/iced"
documentation = "https://docs.rs/iced"
@ -14,18 +14,10 @@ categories = ["gui"]
[badges]
maintenance = { status = "actively-developed" }
[package.metadata.docs.rs]
features = ["winit"]
[dependencies]
stretch = "0.2"
twox-hash = "1.5"
# Enable to obtain conversion traits
winit = { version = "0.20.0-alpha3", optional = true }
[dev-dependencies]
# A personal `ggez` fork that introduces a `FontCache` type to measure text
# efficiently and fixes HiDPI issues.
ggez = { version = "0.5", git = "https://github.com/hecrj/ggez.git" }
env_logger = "0.6"
[workspace]
members = [
"core",
"native",
"web",
"examples/tour",
]

View file

@ -156,11 +156,10 @@ its own standalone crate, as it could potentially benefit other engines and
applications. I thought it was a great idea, and after a bit of work... Iced is
here!
As an interesting note, Iced does not rely on reference counting and interior
mutability. There is not a single `Rc`, `RefCell`, or similar used
directly in the library. As a consequence, compiler guarantees stay intact and
many kinds of pesky bugs and runtime errors are banished. No spooky action at
a distance!
As an interesting note, the core of Iced does not rely on interior mutability.
Usage of types like `RefCell` is restricted to runtime boundaries. As a
consequence, compiler guarantees stay intact and many kinds of pesky bugs and
runtime errors are banished. No spooky action at a distance!
[this pull request]: https://github.com/hecrj/coffee/pull/35
[`stretch`]: https://github.com/vislyhq/stretch

8
core/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "iced_core"
version = "0.1.0-alpha"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "The essential concepts of Iced"
license = "MIT"
repository = "https://github.com/hecrj/iced"

21
core/src/align.rs Normal file
View file

@ -0,0 +1,21 @@
/// Alignment on the cross axis of a container.
///
/// * On a [`Column`], it describes __horizontal__ alignment.
/// * On a [`Row`], it describes __vertical__ alignment.
///
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Align {
/// Align at the start of the cross axis.
Start,
/// Align at the center of the cross axis.
Center,
/// Align at the end of the cross axis.
End,
/// Stretch over the cross axis.
Stretch,
}

19
core/src/color.rs Normal file
View file

@ -0,0 +1,19 @@
/// A color in the sRGB color space.
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(missing_docs)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
/// The black color.
pub const BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
}

27
core/src/justify.rs Normal file
View file

@ -0,0 +1,27 @@
/// Distribution on the main axis of a container.
///
/// * On a [`Column`], it describes __vertical__ distribution.
/// * On a [`Row`], it describes __horizontal__ distribution.
///
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Justify {
/// Place items at the start of the main axis.
Start,
/// Place items at the center of the main axis.
Center,
/// Place items at the end of the main axis.
End,
/// Place items with space between.
SpaceBetween,
/// Place items with space around.
SpaceAround,
/// Place items with evenly distributed space.
SpaceEvenly,
}

7
core/src/length.rs Normal file
View file

@ -0,0 +1,7 @@
/// The strategy used to fill space in a specific dimension.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Length {
Fill,
Shrink,
Units(u16),
}

18
core/src/lib.rs Normal file
View file

@ -0,0 +1,18 @@
pub mod widget;
mod align;
mod color;
mod justify;
mod length;
mod point;
mod rectangle;
mod vector;
pub use align::Align;
pub use color::Color;
pub use justify::Justify;
pub use length::Length;
pub use point::Point;
pub use rectangle::Rectangle;
pub use vector::Vector;
pub use widget::*;

33
core/src/widget.rs Normal file
View file

@ -0,0 +1,33 @@
//! Use the essential widgets.
//!
//! # Re-exports
//! For convenience, the contents of this module are available at the root
//! module. Therefore, you can directly type:
//!
//! ```
//! use iced_core::{button, Button};
//! ```
mod checkbox;
mod column;
mod image;
mod radio;
mod row;
pub mod button;
pub mod slider;
pub mod text;
#[doc(no_inline)]
pub use button::Button;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
pub use checkbox::Checkbox;
pub use column::Column;
pub use image::Image;
pub use radio::Radio;
pub use row::Row;

158
core/src/widget/button.rs Normal file
View file

@ -0,0 +1,158 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
//!
//! [`Button`]: struct.Button.html
//! [`State`]: struct.State.html
use crate::{Align, Length};
/// A generic widget that produces a message when clicked.
///
/// # Example
///
/// ```
/// use iced_core::{button, Button};
///
/// pub enum Message {
/// ButtonClicked,
/// }
///
/// let state = &mut button::State::new();
///
/// Button::new(state, "Click me!")
/// .on_press(Message::ButtonClicked);
/// ```
///
/// ![Button drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/button.png?raw=true)
pub struct Button<'a, Message> {
/// The current state of the button
pub state: &'a mut State,
/// The label of the button
pub label: String,
/// The message to produce when the button is pressed
pub on_press: Option<Message>,
pub class: Class,
pub width: Length,
pub align_self: Option<Align>,
}
impl<'a, Message> std::fmt::Debug for Button<'a, Message>
where
Message: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Button")
.field("state", &self.state)
.field("label", &self.label)
.field("on_press", &self.on_press)
.finish()
}
}
impl<'a, Message> Button<'a, Message> {
/// Creates a new [`Button`] with some local [`State`] and the given label.
///
/// [`Button`]: struct.Button.html
/// [`State`]: struct.State.html
pub fn new(state: &'a mut State, label: &str) -> Self {
Button {
state,
label: String::from(label),
on_press: None,
class: Class::Primary,
width: Length::Shrink,
align_self: None,
}
}
/// Sets the width of the [`Button`].
///
/// [`Button`]: struct.Button.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the alignment of the [`Button`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Button`]: struct.Button.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
/// Sets the [`Class`] of the [`Button`].
///
///
/// [`Button`]: struct.Button.html
/// [`Class`]: enum.Class.html
pub fn class(mut self, class: Class) -> Self {
self.class = class;
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// [`Button`]: struct.Button.html
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
}
}
/// The local state of a [`Button`].
///
/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
pub is_pressed: bool,
}
impl State {
/// Creates a new [`State`].
///
/// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
/// Returns whether the associated [`Button`] is currently being pressed or
/// not.
///
/// [`Button`]: struct.Button.html
pub fn is_pressed(&self) -> bool {
self.is_pressed
}
}
/// The type of a [`Button`].
///
/// ![Different buttons drawn by the built-in renderer in Coffee](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/button_classes.png?raw=true)
///
/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
/// The [`Button`] performs the main action.
///
/// [`Button`]: struct.Button.html
Primary,
/// The [`Button`] performs an alternative action.
///
/// [`Button`]: struct.Button.html
Secondary,
/// The [`Button`] performs a productive action.
///
/// [`Button`]: struct.Button.html
Positive,
}

View file

@ -0,0 +1,78 @@
//! Show toggle controls using checkboxes.
use crate::Color;
/// A box that can be checked.
///
/// # Example
///
/// ```
/// use iced_core::Checkbox;
///
/// pub enum Message {
/// CheckboxToggled(bool),
/// }
///
/// let is_checked = true;
///
/// Checkbox::new(is_checked, "Toggle me!", Message::CheckboxToggled);
/// ```
///
/// ![Checkbox drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/checkbox.png?raw=true)
pub struct Checkbox<Message> {
/// Whether the checkbox is checked or not
pub is_checked: bool,
/// Function to call when checkbox is toggled to produce a __message__.
///
/// The function should be provided `true` when the checkbox is checked
/// and `false` otherwise.
pub on_toggle: Box<dyn Fn(bool) -> Message>,
/// The label of the checkbox
pub label: String,
/// The color of the label
pub label_color: Option<Color>,
}
impl<Message> std::fmt::Debug for Checkbox<Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Checkbox")
.field("is_checked", &self.is_checked)
.field("label", &self.label)
.field("label_color", &self.label_color)
.finish()
}
}
impl<Message> Checkbox<Message> {
/// Creates a new [`Checkbox`].
///
/// It expects:
/// * a boolean describing whether the [`Checkbox`] is checked or not
/// * the label of the [`Checkbox`]
/// * a function that will be called when the [`Checkbox`] is toggled.
/// It will receive the new state of the [`Checkbox`] and must produce
/// a `Message`.
///
/// [`Checkbox`]: struct.Checkbox.html
pub fn new<F>(is_checked: bool, label: &str, f: F) -> Self
where
F: 'static + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Box::new(f),
label: String::from(label),
label_color: None,
}
}
/// Sets the color of the label of the [`Checkbox`].
///
/// [`Checkbox`]: struct.Checkbox.html
pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self {
self.label_color = Some(color.into());
self
}
}

147
core/src/widget/column.rs Normal file
View file

@ -0,0 +1,147 @@
use crate::{Align, Justify, Length};
/// A container that distributes its contents vertically.
///
/// A [`Column`] will try to fill the horizontal space of its container.
///
/// [`Column`]: struct.Column.html
pub struct Column<Element> {
pub spacing: u16,
pub padding: u16,
pub width: Length,
pub height: Length,
pub max_width: Length,
pub max_height: Length,
pub align_self: Option<Align>,
pub align_items: Align,
pub justify_content: Justify,
pub children: Vec<Element>,
}
impl<Element> Column<Element> {
/// Creates an empty [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn new() -> Self {
Column {
spacing: 0,
padding: 0,
width: Length::Fill,
height: Length::Shrink,
max_width: Length::Shrink,
max_height: Length::Shrink,
align_self: None,
align_items: Align::Start,
justify_content: Justify::Start,
children: Vec::new(),
}
}
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, units: u16) -> Self {
self.spacing = units;
self
}
/// Sets the padding of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: Length) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: Length) -> Self {
self.max_height = max_height;
self
}
/// Sets the alignment of the [`Column`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Column`]: struct.Column.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Sets the vertical distribution strategy for the contents of the
/// [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn justify_content(mut self, justify: Justify) -> Self {
self.justify_content = justify;
self
}
/// Adds an element to the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Column<Element>
where
E: Into<Element>,
{
self.children.push(child.into());
self
}
}
impl<Element> Default for Column<Element> {
fn default() -> Self {
Self::new()
}
}
impl<Element> std::fmt::Debug for Column<Element>
where
Element: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// TODO: Complete once stabilized
f.debug_struct("Column")
.field("spacing", &self.spacing)
.field("children", &self.children)
.finish()
}
}

89
core/src/widget/image.rs Normal file
View file

@ -0,0 +1,89 @@
//! Display images in your user interface.
use crate::{Align, Length, Rectangle};
/// A frame that displays an image while keeping aspect ratio.
///
/// # Example
///
/// ```
/// use iced_core::Image;
///
/// # let my_handle = String::from("some_handle");
/// let image = Image::new(my_handle);
/// ```
pub struct Image<I> {
/// The image handle
pub handle: I,
/// The part of the image to show
pub clip: Option<Rectangle<u16>>,
/// The width of the image
pub width: Length,
/// The height of the image
pub height: Length,
pub align_self: Option<Align>,
}
impl<I> std::fmt::Debug for Image<I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Image")
.field("clip", &self.clip)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
impl<I> Image<I> {
/// Creates a new [`Image`] with given image handle.
///
/// [`Image`]: struct.Image.html
pub fn new(handle: I) -> Self {
Image {
handle,
clip: None,
width: Length::Shrink,
height: Length::Shrink,
align_self: None,
}
}
/// Sets the portion of the [`Image`] to draw.
///
/// [`Image`]: struct.Image.html
pub fn clip(mut self, clip: Rectangle<u16>) -> Self {
self.clip = Some(clip);
self
}
/// Sets the width of the [`Image`] boundaries.
///
/// [`Image`]: struct.Image.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Image`] boundaries.
///
/// [`Image`]: struct.Image.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the alignment of the [`Image`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Image`]: struct.Image.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
}

88
core/src/widget/radio.rs Normal file
View file

@ -0,0 +1,88 @@
//! Create choices using radio buttons.
use crate::Color;
/// A circular button representing a choice.
///
/// # Example
/// ```
/// use iced_core::Radio;
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice {
/// A,
/// B,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Message {
/// RadioSelected(Choice),
/// }
///
/// let selected_choice = Some(Choice::A);
///
/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected);
///
/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected);
/// ```
///
/// ![Radio buttons drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/radio.png?raw=true)
pub struct Radio<Message> {
/// Whether the radio button is selected or not
pub is_selected: bool,
/// The message to produce when the radio button is clicked
pub on_click: Message,
/// The label of the radio button
pub label: String,
/// The color of the label
pub label_color: Option<Color>,
}
impl<Message> std::fmt::Debug for Radio<Message>
where
Message: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Radio")
.field("is_selected", &self.is_selected)
.field("on_click", &self.on_click)
.field("label", &self.label)
.field("label_color", &self.label_color)
.finish()
}
}
impl<Message> Radio<Message> {
/// Creates a new [`Radio`] button.
///
/// It expects:
/// * the value related to the [`Radio`] button
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
///
/// [`Radio`]: struct.Radio.html
pub fn new<F, V>(value: V, label: &str, selected: Option<V>, f: F) -> Self
where
V: Eq + Copy,
F: 'static + Fn(V) -> Message,
{
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
label: String::from(label),
label_color: None,
}
}
/// Sets the `Color` of the label of the [`Radio`].
///
/// [`Radio`]: struct.Radio.html
pub fn label_color<C: Into<Color>>(mut self, color: C) -> Self {
self.label_color = Some(color.into());
self
}
}

142
core/src/widget/row.rs Normal file
View file

@ -0,0 +1,142 @@
use crate::{Align, Justify, Length};
/// A container that distributes its contents horizontally.
///
/// A [`Row`] will try to fill the horizontal space of its container.
///
/// [`Row`]: struct.Row.html
pub struct Row<Element> {
pub spacing: u16,
pub padding: u16,
pub width: Length,
pub height: Length,
pub max_width: Length,
pub max_height: Length,
pub align_self: Option<Align>,
pub align_items: Align,
pub justify_content: Justify,
pub children: Vec<Element>,
}
impl<Element> Row<Element> {
/// Creates an empty [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn new() -> Self {
Row {
spacing: 0,
padding: 0,
width: Length::Fill,
height: Length::Shrink,
max_width: Length::Shrink,
max_height: Length::Shrink,
align_self: None,
align_items: Align::Start,
justify_content: Justify::Start,
children: Vec::new(),
}
}
/// Sets the horizontal spacing _between_ elements.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, units: u16) -> Self {
self.spacing = units;
self
}
/// Sets the padding of the [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn max_width(mut self, max_width: Length) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn max_height(mut self, max_height: Length) -> Self {
self.max_height = max_height;
self
}
/// Sets the alignment of the [`Row`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Row`]: struct.Row.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
///
/// [`Row`]: struct.Row.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Sets the horizontal distribution strategy for the contents of the
/// [`Row`] .
///
/// [`Row`]: struct.Row.html
pub fn justify_content(mut self, justify: Justify) -> Self {
self.justify_content = justify;
self
}
/// Adds an [`Element`] to the [`Row`].
///
/// [`Element`]: ../struct.Element.html
/// [`Row`]: struct.Row.html
pub fn push<E>(mut self, child: E) -> Row<Element>
where
E: Into<Element>,
{
self.children.push(child.into());
self
}
}
impl<Element> std::fmt::Debug for Row<Element>
where
Element: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// TODO: Complete once stabilized
f.debug_struct("Row")
.field("spacing", &self.spacing)
.field("children", &self.children)
.finish()
}
}

123
core/src/widget/slider.rs Normal file
View file

@ -0,0 +1,123 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
//!
//! [`Slider`]: struct.Slider.html
//! [`State`]: struct.State.html
use crate::Length;
use std::ops::RangeInclusive;
use std::rc::Rc;
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
/// [`Slider`]: struct.Slider.html
///
/// # Example
/// ```
/// use iced_core::{slider, Slider};
///
/// pub enum Message {
/// SliderChanged(f32),
/// }
///
/// let state = &mut slider::State::new();
/// let value = 50.0;
///
/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged);
/// ```
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
pub struct Slider<'a, Message> {
/// The state of the slider
pub state: &'a mut State,
/// The range of the slider
pub range: RangeInclusive<f32>,
/// The current value of the slider
pub value: f32,
/// The function to produce messages on change
pub on_change: Rc<Box<dyn Fn(f32) -> Message>>,
pub width: Length,
}
impl<'a, Message> std::fmt::Debug for Slider<'a, Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Slider")
.field("state", &self.state)
.field("range", &self.range)
.field("value", &self.value)
.finish()
}
}
impl<'a, Message> Slider<'a, Message> {
/// Creates a new [`Slider`].
///
/// It expects:
/// * the local [`State`] of the [`Slider`]
/// * an inclusive range of possible values
/// * the current value of the [`Slider`]
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
///
/// [`Slider`]: struct.Slider.html
/// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
range: RangeInclusive<f32>,
value: f32,
on_change: F,
) -> Self
where
F: 'static + Fn(f32) -> Message,
{
Slider {
state,
value: value.max(*range.start()).min(*range.end()),
range,
on_change: Rc::new(Box::new(on_change)),
width: Length::Fill,
}
}
/// Sets the width of the [`Slider`].
///
/// [`Slider`]: struct.Slider.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
}
/// The local state of a [`Slider`].
///
/// [`Slider`]: struct.Slider.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
pub is_dragging: bool,
}
impl State {
/// Creates a new [`State`].
///
/// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
/// Returns whether the associated [`Slider`] is currently being dragged or
/// not.
///
/// [`Slider`]: struct.Slider.html
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
}

119
core/src/widget/text.rs Normal file
View file

@ -0,0 +1,119 @@
//! Write some text for your users to read.
use crate::{Color, Length};
/// A paragraph of text.
///
/// # Example
///
/// ```
/// use iced_core::Text;
///
/// Text::new("I <3 iced!")
/// .size(40);
/// ```
#[derive(Debug, Clone)]
pub struct Text {
pub content: String,
pub size: Option<u16>,
pub color: Option<Color>,
pub width: Length,
pub height: Length,
pub horizontal_alignment: HorizontalAlignment,
pub vertical_alignment: VerticalAlignment,
}
impl Text {
/// Create a new fragment of [`Text`] with the given contents.
///
/// [`Text`]: struct.Text.html
pub fn new(label: &str) -> Self {
Text {
content: String::from(label),
size: None,
color: None,
width: Length::Fill,
height: Length::Shrink,
horizontal_alignment: HorizontalAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
/// Sets the size of the [`Text`].
///
/// [`Text`]: struct.Text.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
/// Sets the `Color` of the [`Text`].
///
/// [`Text`]: struct.Text.html
pub fn color<C: Into<Color>>(mut self, color: C) -> Self {
self.color = Some(color.into());
self
}
/// Sets the width of the [`Text`] boundaries.
///
/// [`Text`]: struct.Text.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Text`] boundaries.
///
/// [`Text`]: struct.Text.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the [`HorizontalAlignment`] of the [`Text`].
///
/// [`Text`]: struct.Text.html
/// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
pub fn horizontal_alignment(
mut self,
alignment: HorizontalAlignment,
) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the [`VerticalAlignment`] of the [`Text`].
///
/// [`Text`]: struct.Text.html
/// [`VerticalAlignment`]: enum.VerticalAlignment.html
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
}
}
/// The horizontal alignment of some resource.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizontalAlignment {
/// Align left
Left,
/// Horizontally centered
Center,
/// Align right
Right,
}
/// The vertical alignment of some resource.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerticalAlignment {
/// Align top
Top,
/// Vertically centered
Center,
/// Align bottom
Bottom,
}

View file

@ -11,31 +11,62 @@ you want to learn about a specific release, check out [the release list].
A simple UI tour showcasing different widgets that can be built using Iced. It
also shows how the library can be integrated into an existing system.
The example is built on top of [`ggez`], a game library for Rust. Currently, it
is using a [personal fork] to [add a `FontCache` type] and
[fix some issues with HiDPI].
The example can run both on native and web platforms, using the same GUI code!
The native renderer of the example is built on top of [`ggez`], a game library
for Rust. Currently, it is using a [personal fork] to [add a `FontCache` type]
and [fix some issues with HiDPI].
The web version uses `iced_web` directly. This crate is still a work in
progress. In particular, the styling of elements is not finished yet
(text color, alignment, sizing, etc).
The implementation consists of different modules:
- __[`tour`]__ contains the actual GUI code: __state__, __messages__,
__update logic__ and __view logic__.
- __[`renderer`]__ implements a simple renderer for each of the used widgets on
top of the graphics module of [`ggez`].
- __[`widget`]__ re-exposes Iced's built-in widgets with the renderer type parameter
replaced with the implemented [`renderer`], for convenience.
- __[`tour`]__ contains the actual cross-platform GUI code: __state__,
__messages__, __update logic__ and __view logic__.
- __[`iced_ggez`]__ implements a simple renderer for each of the used widgets
on top of the graphics module of [`ggez`].
- __[`widget`]__ conditionally re-exposes the correct platform widgets based
on the target architecture.
- __[`main`]__ integrates Iced with [`ggez`] and connects the [`tour`] with
the [`renderer`].
the native [`renderer`].
- __[`lib`]__ exposes the [`tour`] types and conditionally implements the
WebAssembly entrypoint in the [`web`] module.
The conditional compilation awkwardness from targetting both native and web
platforms should be handled seamlessly by the `iced` crate in the near future!
If you want to run it as a native app:
```
cargo run --example tour
cd examples/tour
cargo run
```
If you want to run it on web, you will need [`wasm-pack`]:
```
cd examples/tour
wasm-pack build --target web
```
Then, simply serve the directory with any HTTP server. For instance:
```
python3 -m http.server
```
[![Tour - Iced][gui_gif]][gui_gfycat]
[`ggez`]: https://github.com/ggez/ggez
[`tour`]: tour/tour.rs
[`renderer`]: tour/renderer
[`widget`]: tour/widget.rs
[`main`]: tour/main.rs
[`tour`]: tour/src/tour.rs
[`iced_ggez`]: tour/src/iced_ggez
[`renderer`]: src/iced_ggez/renderer
[`widget`]: tour/src/widget.rs
[`main`]: tour/src/main.rs
[`lib`]: tour/src/lib.rs
[`web`]: tour/src/web.rs
[`wasm-pack`]: https://rustwasm.github.io/wasm-pack/installer/
[personal fork]: https://github.com/hecrj/ggez
[add a `FontCache` type]: https://github.com/ggez/ggez/pull/679
[fix some issues with HiDPI]: https://github.com/hecrj/ggez/commit/dfe2fd2423c51a6daf42c75f66dfaeaacd439fb1

33
examples/tour/Cargo.toml Normal file
View file

@ -0,0 +1,33 @@
[package]
name = "iced_tour"
version = "0.0.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
description = "Tour example for Iced"
license = "MIT"
repository = "https://github.com/hecrj/iced"
edition = "2018"
publish = false
[lib]
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "main"
path = "src/main.rs"
[dependencies]
futures-preview = "=0.3.0-alpha.18"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
iced_native = { version = "0.1.0-alpha", path = "../../native" }
# A personal `ggez` fork that introduces a `FontCache` type to measure text
# efficiently and fixes HiDPI issues.
ggez = { version = "0.5", git = "https://github.com/hecrj/ggez.git" }
env_logger = "0.6"
[target.'cfg(target_arch = "wasm32")'.dependencies]
iced_web = { path = "../../web" }
wasm-bindgen = "0.2.50"
log = "0.4"
console_error_panic_hook = "0.1.6"
console_log = "0.1.2"

View file

@ -3,31 +3,62 @@
A simple UI tour showcasing different widgets that can be built using Iced. It
also shows how the library can be integrated into an existing system.
The example is built on top of [`ggez`], a game library for Rust. Currently, it
is using a [personal fork] to [add a `FontCache` type] and
[fix some issues with HiDPI].
The example can run both on native and web platforms, using the same GUI code!
The native renderer of the example is built on top of [`ggez`], a game library
for Rust. Currently, it is using a [personal fork] to [add a `FontCache` type]
and [fix some issues with HiDPI].
The web version uses `iced_web` directly. This crate is still a work in
progress. In particular, the styling of elements is not finished yet
(text color, alignment, sizing, etc).
The implementation consists of different modules:
- __[`tour`]__ contains the actual GUI code: __state__, __messages__,
__update logic__ and __view logic__.
- __[`renderer`]__ implements a simple renderer for each of the used widgets on
top of the graphics module of [`ggez`].
- __[`widget`]__ re-exposes Iced's built-in widgets with the renderer type parameter
replaced with the implemented [`renderer`], for convenience.
- __[`tour`]__ contains the actual cross-platform GUI code: __state__,
__messages__, __update logic__ and __view logic__.
- __[`iced_ggez`]__ implements a simple renderer for each of the used widgets
on top of the graphics module of [`ggez`].
- __[`widget`]__ conditionally re-exposes the correct platform widgets based
on the target architecture.
- __[`main`]__ integrates Iced with [`ggez`] and connects the [`tour`] with
the [`renderer`].
the native [`renderer`].
- __[`lib`]__ exposes the [`tour`] types and conditionally implements the
WebAssembly entrypoint in the [`web`] module.
The conditional compilation awkwardness from targetting both native and web
platforms should be handled seamlessly by the `iced` crate in the near future!
If you want to run it as a native app:
```
cargo run --example tour
cd examples/tour
cargo run
```
If you want to run it on web, you will need [`wasm-pack`]:
```
cd examples/tour
wasm-pack build --target web
```
Then, simply serve the directory with any HTTP server. For instance:
```
python3 -m http.server
```
[![Tour - Iced][gui_gif]][gui_gfycat]
[`ggez`]: https://github.com/ggez/ggez
[`tour`]: tour.rs
[`renderer`]: renderer
[`widget`]: widget.rs
[`main`]: main.rs
[`tour`]: src/tour.rs
[`iced_ggez`]: src/iced_ggez
[`renderer`]: src/iced_ggez/renderer
[`widget`]: src/widget.rs
[`main`]: src/main.rs
[`lib`]: src/lib.rs
[`web`]: src/web.rs
[`wasm-pack`]: https://rustwasm.github.io/wasm-pack/installer/
[personal fork]: https://github.com/hecrj/ggez
[add a `FontCache` type]: https://github.com/ggez/ggez/pull/679
[fix some issues with HiDPI]: https://github.com/hecrj/ggez/commit/dfe2fd2423c51a6daf42c75f66dfaeaacd439fb1

13
examples/tour/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Tour - Iced</title>
</head>
<body>
<script type="module">
import init from "./pkg/iced_tour.js";
init("./pkg/iced_tour_bg.wasm");
</script>
</body>
</html>

View file

@ -1,51 +0,0 @@
use super::Renderer;
use ggez::{graphics, nalgebra};
use iced::image;
impl image::Renderer<graphics::Image> for Renderer<'_> {
fn node(
&self,
style: iced::Style,
image: &graphics::Image,
width: Option<u16>,
height: Option<u16>,
_source: Option<iced::Rectangle<u16>>,
) -> iced::Node {
let aspect_ratio = image.width() as f32 / image.height() as f32;
let style = match (width, height) {
(Some(width), Some(height)) => style.width(width).height(height),
(Some(width), None) => style
.width(width)
.height((width as f32 / aspect_ratio).round() as u16),
(None, Some(height)) => style
.height(height)
.width((height as f32 * aspect_ratio).round() as u16),
(None, None) => style.width(image.width()).height(image.height()),
};
iced::Node::new(style)
}
fn draw(
&mut self,
image: &graphics::Image,
bounds: iced::Rectangle,
_source: Option<iced::Rectangle<u16>>,
) {
// We should probably use batches to draw images efficiently and keep
// draw side-effect free, but this is good enough for the example.
graphics::draw(
self.context,
image,
graphics::DrawParam::new()
.dest(nalgebra::Point2::new(bounds.x, bounds.y))
.scale(nalgebra::Vector2::new(
bounds.width / image.width() as f32,
bounds.height / image.height() as f32,
)),
)
.expect("Draw image");
}
}

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -0,0 +1,6 @@
mod renderer;
mod widget;
pub use renderer::Cache as ImageCache;
pub use renderer::Renderer;
pub use widget::*;

View file

@ -11,8 +11,11 @@ use ggez::graphics::{
};
use ggez::Context;
pub use image::Cache;
pub struct Renderer<'a> {
pub context: &'a mut Context,
pub images: &'a mut image::Cache,
pub sprites: SpriteBatch,
pub spritesheet: Image,
pub font: Font,
@ -20,14 +23,16 @@ pub struct Renderer<'a> {
debug_mesh: Option<MeshBuilder>,
}
impl Renderer<'_> {
impl<'a> Renderer<'a> {
pub fn new(
context: &mut Context,
context: &'a mut Context,
images: &'a mut image::Cache,
spritesheet: Image,
font: Font,
) -> Renderer {
) -> Renderer<'a> {
Renderer {
context,
images,
sprites: SpriteBatch::new(spritesheet.clone()),
spritesheet,
font,
@ -61,3 +66,12 @@ impl Renderer<'_> {
}
}
}
pub fn into_color(color: iced_native::Color) -> graphics::Color {
graphics::Color {
r: color.r,
g: color.g,
b: color.b,
a: color.a,
}
}

View file

@ -2,7 +2,7 @@ use super::Renderer;
use ggez::graphics::{
self, Align, Color, DrawParam, Rect, Scale, Text, TextFragment, WHITE,
};
use iced::{button, MouseCursor};
use iced_native::{button, Button, Layout, Length, MouseCursor, Node, Style};
const LEFT: Rect = Rect {
x: 0.0,
@ -26,20 +26,29 @@ const RIGHT: Rect = Rect {
};
impl button::Renderer for Renderer<'_> {
fn draw(
fn node<Message>(&self, button: &Button<'_, Message>) -> Node {
let style = Style::default()
.width(button.width)
.height(Length::Units(LEFT.h as u16))
.min_width(Length::Units(100))
.align_self(button.align_self);
Node::new(style)
}
fn draw<Message>(
&mut self,
cursor_position: iced::Point,
mut bounds: iced::Rectangle,
state: &button::State,
label: &str,
class: button::Class,
button: &Button<'_, Message>,
layout: Layout<'_>,
cursor_position: iced_native::Point,
) -> MouseCursor {
let mut bounds = layout.bounds();
let mouse_over = bounds.contains(cursor_position);
let mut state_offset = 0.0;
if mouse_over {
if state.is_pressed() {
if button.state.is_pressed() {
bounds.y += 4.0;
state_offset = RIGHT.x + RIGHT.w;
} else {
@ -47,7 +56,7 @@ impl button::Renderer for Renderer<'_> {
}
}
let class_index = match class {
let class_index = match button.class {
button::Class::Primary => 0,
button::Class::Secondary => 1,
button::Class::Positive => 2,
@ -103,7 +112,7 @@ impl button::Renderer for Renderer<'_> {
});
let mut text = Text::new(TextFragment {
text: String::from(label),
text: button.label.clone(),
font: Some(self.font),
scale: Some(Scale { x: 20.0, y: 20.0 }),
..Default::default()

View file

@ -1,7 +1,10 @@
use super::Renderer;
use ggez::graphics::{DrawParam, Rect};
use iced::{checkbox, MouseCursor};
use iced_native::{
checkbox, text, Align, Checkbox, Column, Layout, Length, MouseCursor, Node,
Row, Text, Widget,
};
const SPRITE: Rect = Rect {
x: 98.0,
@ -10,14 +13,41 @@ const SPRITE: Rect = Rect {
h: 28.0,
};
impl checkbox::Renderer for Renderer<'_> {
fn draw(
impl checkbox::Renderer for Renderer<'_>
where
Self: text::Renderer,
{
fn node<Message>(&mut self, checkbox: &Checkbox<Message>) -> Node {
Row::<(), Self>::new()
.spacing(15)
.align_items(Align::Center)
.push(
Column::new()
.width(Length::Units(SPRITE.w as u16))
.height(Length::Units(SPRITE.h as u16)),
)
.push(Text::new(&checkbox.label))
.node(self)
}
fn draw<Message>(
&mut self,
cursor_position: iced::Point,
bounds: iced::Rectangle,
text_bounds: iced::Rectangle,
is_checked: bool,
checkbox: &Checkbox<Message>,
layout: Layout<'_>,
cursor_position: iced_native::Point,
) -> MouseCursor {
let bounds = layout.bounds();
let children: Vec<_> = layout.children().collect();
let text_bounds = children[1].bounds();
let mut text = Text::new(&checkbox.label);
if let Some(label_color) = checkbox.label_color {
text = text.color(label_color);
}
text::Renderer::draw(self, &text, children[1]);
let mouse_over = bounds.contains(cursor_position)
|| text_bounds.contains(cursor_position);
@ -39,7 +69,7 @@ impl checkbox::Renderer for Renderer<'_> {
..DrawParam::default()
});
if is_checked {
if checkbox.is_checked {
self.sprites.add(DrawParam {
src: Rect {
x: (SPRITE.x + SPRITE.w * 2.0) / width,

View file

@ -1,10 +1,12 @@
use super::Renderer;
use ggez::graphics::{Color, DrawMode, MeshBuilder, Rect};
use super::{into_color, Renderer};
use ggez::graphics::{DrawMode, MeshBuilder, Rect};
impl iced::renderer::Debugger for Renderer<'_> {
type Color = Color;
fn explain(&mut self, layout: &iced::Layout<'_>, color: Color) {
impl iced_native::renderer::Debugger for Renderer<'_> {
fn explain(
&mut self,
layout: &iced_native::Layout<'_>,
color: iced_native::Color,
) {
let bounds = layout.bounds();
let mut debug_mesh =
@ -18,7 +20,7 @@ impl iced::renderer::Debugger for Renderer<'_> {
w: bounds.width,
h: bounds.height,
},
color,
into_color(color),
);
self.debug_mesh = Some(debug_mesh);

View file

@ -0,0 +1,76 @@
use super::Renderer;
use ggez::{graphics, nalgebra};
use iced_native::{image, Image, Layout, Length, Style};
pub struct Cache {
images: std::collections::HashMap<String, graphics::Image>,
}
impl Cache {
pub fn new() -> Self {
Self {
images: std::collections::HashMap::new(),
}
}
fn get<'a>(
&mut self,
name: &'a str,
context: &mut ggez::Context,
) -> graphics::Image {
if let Some(image) = self.images.get(name) {
return image.clone();
}
let mut image = graphics::Image::new(context, &format!("/{}", name))
.expect("Load ferris image");
image.set_filter(graphics::FilterMode::Linear);
self.images.insert(name.to_string(), image.clone());
image
}
}
impl<'a> image::Renderer<&'a str> for Renderer<'_> {
fn node(&mut self, image: &Image<&'a str>) -> iced_native::Node {
let ggez_image = self.images.get(image.handle, self.context);
let aspect_ratio =
ggez_image.width() as f32 / ggez_image.height() as f32;
let mut style = Style::default().align_self(image.align_self);
style = match (image.width, image.height) {
(Length::Units(width), _) => style.width(image.width).height(
Length::Units((width as f32 / aspect_ratio).round() as u16),
),
(_, _) => style
.width(Length::Units(ggez_image.width()))
.height(Length::Units(ggez_image.height())),
};
iced_native::Node::new(style)
}
fn draw(&mut self, image: &Image<&'a str>, layout: Layout<'_>) {
let image = self.images.get(image.handle, self.context);
let bounds = layout.bounds();
// We should probably use batches to draw images efficiently and keep
// draw side-effect free, but this is good enough for the example.
graphics::draw(
self.context,
&image,
graphics::DrawParam::new()
.dest(nalgebra::Point2::new(bounds.x, bounds.y))
.scale(nalgebra::Vector2::new(
bounds.width / image.width() as f32,
bounds.height / image.height() as f32,
)),
)
.expect("Draw image");
}
}

View file

@ -1,7 +1,10 @@
use super::Renderer;
use ggez::graphics::{DrawParam, Rect};
use iced::{radio, MouseCursor, Point, Rectangle};
use iced_native::{
radio, text, Align, Column, Layout, Length, MouseCursor, Node, Point,
Radio, Row, Text, Widget,
};
const SPRITE: Rect = Rect {
x: 98.0,
@ -10,15 +13,41 @@ const SPRITE: Rect = Rect {
h: 28.0,
};
impl radio::Renderer for Renderer<'_> {
fn draw(
impl radio::Renderer for Renderer<'_>
where
Self: text::Renderer,
{
fn node<Message>(&mut self, radio: &Radio<Message>) -> Node {
Row::<(), Self>::new()
.spacing(15)
.align_items(Align::Center)
.push(
Column::new()
.width(Length::Units(SPRITE.w as u16))
.height(Length::Units(SPRITE.h as u16)),
)
.push(Text::new(&radio.label))
.node(self)
}
fn draw<Message>(
&mut self,
radio: &Radio<Message>,
layout: Layout<'_>,
cursor_position: Point,
bounds: Rectangle,
bounds_with_label: Rectangle,
is_selected: bool,
) -> MouseCursor {
let mouse_over = bounds_with_label.contains(cursor_position);
let children: Vec<_> = layout.children().collect();
let mut text = Text::new(&radio.label);
if let Some(label_color) = radio.label_color {
text = text.color(label_color);
}
text::Renderer::draw(self, &text, children[1]);
let bounds = layout.bounds();
let mouse_over = bounds.contains(cursor_position);
let width = self.spritesheet.width() as f32;
let height = self.spritesheet.height() as f32;
@ -38,7 +67,7 @@ impl radio::Renderer for Renderer<'_> {
..DrawParam::default()
});
if is_selected {
if radio.is_selected {
self.sprites.add(DrawParam {
src: Rect {
x: (SPRITE.x + SPRITE.w * 2.0) / width,

View file

@ -1,8 +1,9 @@
use super::Renderer;
use ggez::graphics::{DrawParam, Rect};
use iced::{slider, MouseCursor, Point, Rectangle};
use std::ops::RangeInclusive;
use iced_native::{
slider, Layout, Length, MouseCursor, Node, Point, Slider, Style,
};
const RAIL: Rect = Rect {
x: 98.0,
@ -19,14 +20,22 @@ const MARKER: Rect = Rect {
};
impl slider::Renderer for Renderer<'_> {
fn draw(
fn node<Message>(&self, slider: &Slider<'_, Message>) -> Node {
let style = Style::default()
.width(slider.width)
.height(Length::Units(25))
.min_width(Length::Units(100));
Node::new(style)
}
fn draw<Message>(
&mut self,
slider: &Slider<'_, Message>,
layout: Layout<'_>,
cursor_position: Point,
bounds: Rectangle,
state: &slider::State,
range: RangeInclusive<f32>,
value: f32,
) -> MouseCursor {
let bounds = layout.bounds();
let width = self.spritesheet.width() as f32;
let height = self.spritesheet.height() as f32;
@ -48,13 +57,14 @@ impl slider::Renderer for Renderer<'_> {
..DrawParam::default()
});
let (range_start, range_end) = range.into_inner();
let (range_start, range_end) = slider.range.clone().into_inner();
let marker_offset = (bounds.width - MARKER.w as f32)
* ((value - range_start) / (range_end - range_start).max(1.0));
* ((slider.value - range_start)
/ (range_end - range_start).max(1.0));
let mouse_over = bounds.contains(cursor_position);
let is_active = state.is_dragging() || mouse_over;
let is_active = slider.state.is_dragging() || mouse_over;
self.sprites.add(DrawParam {
src: Rect {
@ -66,12 +76,13 @@ impl slider::Renderer for Renderer<'_> {
},
dest: ggez::mint::Point2 {
x: bounds.x + marker_offset.round(),
y: bounds.y + (if state.is_dragging() { 2.0 } else { 0.0 }),
y: bounds.y
+ (if slider.state.is_dragging() { 2.0 } else { 0.0 }),
},
..DrawParam::default()
});
if state.is_dragging() {
if slider.state.is_dragging() {
MouseCursor::Grabbing
} else if mouse_over {
MouseCursor::Grab

View file

@ -1,20 +1,15 @@
use super::Renderer;
use ggez::graphics::{self, mint, Align, Color, Scale, Text, TextFragment};
use super::{into_color, Renderer};
use ggez::graphics::{self, mint, Align, Scale, Text, TextFragment};
use iced::text;
use iced_native::{text, Layout, Node, Style};
use std::cell::RefCell;
use std::f32;
impl text::Renderer<Color> for Renderer<'_> {
fn node(
&self,
style: iced::Style,
content: &str,
size: Option<u16>,
) -> iced::Node {
impl text::Renderer for Renderer<'_> {
fn node(&self, text: &iced_native::Text) -> Node {
let font = self.font;
let font_cache = graphics::font_cache(self.context);
let content = String::from(content);
let content = String::from(&text.content);
// TODO: Investigate why stretch tries to measure this MANY times
// with every ancestor's bounds.
@ -23,20 +18,22 @@ impl text::Renderer<Color> for Renderer<'_> {
// I noticed that the first measure is the one that matters in
// practice. Here, we use a RefCell to store the cached measurement.
let measure = RefCell::new(None);
let size = size.map(f32::from).unwrap_or(self.font_size);
let size = text.size.map(f32::from).unwrap_or(self.font_size);
iced::Node::with_measure(style, move |bounds| {
let style = Style::default().width(text.width);
iced_native::Node::with_measure(style, move |bounds| {
let mut measure = measure.borrow_mut();
if measure.is_none() {
let bounds = (
match bounds.width {
iced::Number::Undefined => f32::INFINITY,
iced::Number::Defined(w) => w,
iced_native::Number::Undefined => f32::INFINITY,
iced_native::Number::Defined(w) => w,
},
match bounds.height {
iced::Number::Undefined => f32::INFINITY,
iced::Number::Defined(h) => h,
iced_native::Number::Undefined => f32::INFINITY,
iced_native::Number::Defined(h) => h,
},
);
@ -57,7 +54,7 @@ impl text::Renderer<Color> for Renderer<'_> {
let (width, height) = font_cache.dimensions(&text);
let size = iced::Size {
let size = iced_native::Size {
width: width as f32,
height: height as f32,
};
@ -75,30 +72,23 @@ impl text::Renderer<Color> for Renderer<'_> {
})
}
fn draw(
&mut self,
bounds: iced::Rectangle,
content: &str,
size: Option<u16>,
color: Option<Color>,
horizontal_alignment: text::HorizontalAlignment,
_vertical_alignment: text::VerticalAlignment,
) {
let size = size.map(f32::from).unwrap_or(self.font_size);
fn draw(&mut self, text: &iced_native::Text, layout: Layout<'_>) {
let size = text.size.map(f32::from).unwrap_or(self.font_size);
let bounds = layout.bounds();
let mut text = Text::new(TextFragment {
text: String::from(content),
let mut ggez_text = Text::new(TextFragment {
text: text.content.clone(),
font: Some(self.font),
scale: Some(Scale { x: size, y: size }),
..Default::default()
});
text.set_bounds(
ggez_text.set_bounds(
mint::Point2 {
x: bounds.width,
y: bounds.height,
},
match horizontal_alignment {
match text.horizontal_alignment {
text::HorizontalAlignment::Left => graphics::Align::Left,
text::HorizontalAlignment::Center => graphics::Align::Center,
text::HorizontalAlignment::Right => graphics::Align::Right,
@ -107,12 +97,14 @@ impl text::Renderer<Color> for Renderer<'_> {
graphics::queue_text(
self.context,
&text,
&ggez_text,
mint::Point2 {
x: bounds.x,
y: bounds.y,
},
color.or(Some(graphics::BLACK)),
text.color
.or(Some(iced_native::Color::BLACK))
.map(into_color),
);
}
}

View file

@ -0,0 +1,12 @@
use super::Renderer;
pub use iced_native::{
button, slider, text, Align, Button, Checkbox, Color, Length, Radio,
Slider, Text,
};
pub type Image<'a> = iced_native::Image<&'a str>;
pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer<'a>>;
pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer<'a>>;
pub type Element<'a, Message> = iced_native::Element<'a, Message, Renderer<'a>>;

11
examples/tour/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
pub mod tour;
pub use tour::{Message, Tour};
mod widget;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(not(target_arch = "wasm32"))]
pub mod iced_ggez;

View file

@ -1,10 +1,4 @@
mod renderer;
mod tour;
mod widget;
use renderer::Renderer;
use tour::Tour;
use widget::Column;
use iced_tour::{iced_ggez, Tour};
use ggez;
use ggez::event;
@ -28,10 +22,7 @@ pub fn main() -> ggez::GameResult {
filesystem::mount(
context,
std::path::Path::new(&format!(
"{}/examples/resources",
env!("CARGO_MANIFEST_DIR")
)),
std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
true,
);
@ -43,10 +34,11 @@ pub fn main() -> ggez::GameResult {
struct Game {
spritesheet: graphics::Image,
font: graphics::Font,
images: iced_ggez::ImageCache,
tour: Tour,
events: Vec<iced::Event>,
cache: Option<iced::Cache>,
events: Vec<iced_native::Event>,
cache: Option<iced_native::Cache>,
}
impl Game {
@ -54,12 +46,15 @@ impl Game {
graphics::set_default_filter(context, graphics::FilterMode::Nearest);
Ok(Game {
spritesheet: graphics::Image::new(context, "/ui.png").unwrap(),
font: graphics::Font::new(context, "/Roboto-Regular.ttf").unwrap(),
tour: Tour::new(context),
spritesheet: graphics::Image::new(context, "/resources/ui.png")
.unwrap(),
font: graphics::Font::new(context, "/resources/Roboto-Regular.ttf")
.unwrap(),
images: iced_ggez::ImageCache::new(),
tour: Tour::new(),
events: Vec::new(),
cache: Some(iced::Cache::default()),
cache: Some(iced_native::Cache::default()),
})
}
}
@ -76,10 +71,10 @@ impl event::EventHandler for Game {
_x: f32,
_y: f32,
) {
self.events.push(iced::Event::Mouse(
iced::input::mouse::Event::Input {
state: iced::input::ButtonState::Pressed,
button: iced::input::mouse::Button::Left, // TODO: Map `button`
self.events.push(iced_native::Event::Mouse(
iced_native::input::mouse::Event::Input {
state: iced_native::input::ButtonState::Pressed,
button: iced_native::input::mouse::Button::Left, // TODO: Map `button`
},
));
}
@ -91,10 +86,10 @@ impl event::EventHandler for Game {
_x: f32,
_y: f32,
) {
self.events.push(iced::Event::Mouse(
iced::input::mouse::Event::Input {
state: iced::input::ButtonState::Released,
button: iced::input::mouse::Button::Left, // TODO: Map `button`
self.events.push(iced_native::Event::Mouse(
iced_native::input::mouse::Event::Input {
state: iced_native::input::ButtonState::Released,
button: iced_native::input::mouse::Button::Left, // TODO: Map `button`
},
));
}
@ -107,8 +102,8 @@ impl event::EventHandler for Game {
_dx: f32,
_dy: f32,
) {
self.events.push(iced::Event::Mouse(
iced::input::mouse::Event::CursorMoved { x, y },
self.events.push(iced_native::Event::Mouse(
iced_native::input::mouse::Event::CursorMoved { x, y },
));
}
@ -138,21 +133,22 @@ impl event::EventHandler for Game {
let (messages, cursor) = {
let view = self.tour.view();
let content = Column::new()
.width(screen.w as u16)
.height(screen.h as u16)
let content = iced_ggez::Column::new()
.width(iced_native::Length::Units(screen.w as u16))
.height(iced_native::Length::Units(screen.h as u16))
.padding(20)
.align_items(iced::Align::Center)
.justify_content(iced::Justify::Center)
.align_items(iced_native::Align::Center)
.justify_content(iced_native::Justify::Center)
.push(view);
let renderer = &mut Renderer::new(
let renderer = &mut iced_ggez::Renderer::new(
context,
&mut self.images,
self.spritesheet.clone(),
self.font,
);
let mut ui = iced::UserInterface::build(
let mut ui = iced_native::UserInterface::build(
content,
self.cache.take().unwrap(),
renderer,
@ -183,13 +179,13 @@ impl event::EventHandler for Game {
}
}
fn into_cursor_type(cursor: iced::MouseCursor) -> mouse::MouseCursor {
fn into_cursor_type(cursor: iced_native::MouseCursor) -> mouse::MouseCursor {
match cursor {
iced::MouseCursor::OutOfBounds => mouse::MouseCursor::Default,
iced::MouseCursor::Idle => mouse::MouseCursor::Default,
iced::MouseCursor::Pointer => mouse::MouseCursor::Hand,
iced::MouseCursor::Working => mouse::MouseCursor::Progress,
iced::MouseCursor::Grab => mouse::MouseCursor::Grab,
iced::MouseCursor::Grabbing => mouse::MouseCursor::Grabbing,
iced_native::MouseCursor::OutOfBounds => mouse::MouseCursor::Default,
iced_native::MouseCursor::Idle => mouse::MouseCursor::Default,
iced_native::MouseCursor::Pointer => mouse::MouseCursor::Hand,
iced_native::MouseCursor::Working => mouse::MouseCursor::Progress,
iced_native::MouseCursor::Grab => mouse::MouseCursor::Grab,
iced_native::MouseCursor::Grabbing => mouse::MouseCursor::Grabbing,
}
}

View file

@ -1,12 +1,8 @@
use super::widget::{
button, slider, Button, Checkbox, Column, Element, Image, Radio, Row,
Slider, Text,
use crate::widget::{
button, slider, text::HorizontalAlignment, Align, Button, Checkbox, Color,
Column, Element, Image, Length, Radio, Row, Slider, Text,
};
use ggez::graphics::{self, Color, FilterMode, BLACK};
use ggez::Context;
use iced::{text::HorizontalAlignment, Align};
pub struct Tour {
steps: Steps,
back_button: button::State,
@ -15,9 +11,9 @@ pub struct Tour {
}
impl Tour {
pub fn new(context: &mut Context) -> Tour {
pub fn new() -> Tour {
Tour {
steps: Steps::new(context),
steps: Steps::new(),
back_button: button::State::new(),
next_button: button::State::new(),
debug: false,
@ -65,14 +61,14 @@ impl Tour {
}
let element: Element<_> = Column::new()
.max_width(500)
.max_width(Length::Units(500))
.spacing(20)
.push(steps.view(self.debug).map(Message::StepMessage))
.push(controls)
.into();
if self.debug {
element.explain(BLACK)
element.explain(Color::BLACK)
} else {
element
}
@ -92,7 +88,7 @@ struct Steps {
}
impl Steps {
fn new(context: &mut Context) -> Steps {
fn new() -> Steps {
Steps {
steps: vec![
Step::Welcome,
@ -109,19 +105,10 @@ impl Steps {
size_slider: slider::State::new(),
size: 30,
color_sliders: [slider::State::new(); 3],
color: BLACK,
color: Color::BLACK,
},
Step::Radio { selection: None },
Step::Image {
ferris: {
let mut image =
graphics::Image::new(context, "/ferris.png")
.expect("Load ferris image");
image.set_filter(FilterMode::Linear);
image
},
width: 300,
slider: slider::State::new(),
},
@ -183,7 +170,6 @@ enum Step {
selection: Option<Language>,
},
Image {
ferris: graphics::Image,
width: u16,
slider: slider::State,
},
@ -273,11 +259,7 @@ impl<'a> Step {
color_sliders,
color,
} => Self::text(size_slider, *size, color_sliders, *color).into(),
Step::Image {
ferris,
width,
slider,
} => Self::image(ferris.clone(), *width, slider).into(),
Step::Image { width, slider } => Self::image(*width, slider).into(),
Step::RowsAndColumns {
layout,
spacing_slider,
@ -313,8 +295,8 @@ impl<'a> Step {
))
.push(Text::new(
"Iced does not provide a built-in renderer. This example runs \
on a fairly simple renderer built on top of ggez, another \
game library.",
on WebAssembly using dodrio, an experimental VDOM library \
for Rust.",
))
.push(Text::new(
"You will need to interact with the UI in order to reach the \
@ -489,13 +471,16 @@ impl<'a> Step {
}
fn image(
ferris: graphics::Image,
width: u16,
slider: &'a mut slider::State,
) -> Column<'a, StepMessage> {
Self::container("Image")
.push(Text::new("An image that tries to keep its aspect ratio."))
.push(Image::new(ferris).width(width).align_self(Align::Center))
.push(
Image::new("resources/ferris.png")
.width(Length::Units(width))
.align_self(Align::Center),
)
.push(Slider::new(
slider,
100.0..=500.0,

33
examples/tour/src/web.rs Normal file
View file

@ -0,0 +1,33 @@
use futures::Future;
use iced_web::UserInterface;
use wasm_bindgen::prelude::*;
use crate::tour::{self, Tour};
#[wasm_bindgen(start)]
pub fn run() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Trace)
.expect("Initialize logging");
let tour = Tour::new();
tour.run();
}
impl iced_web::UserInterface for Tour {
type Message = tour::Message;
fn update(
&mut self,
message: tour::Message,
) -> Option<Box<dyn Future<Output = tour::Message>>> {
self.update(message);
None
}
fn view(&mut self) -> iced_web::Element<tour::Message> {
self.view()
}
}

View file

@ -0,0 +1,5 @@
#[cfg(target_arch = "wasm32")]
pub use iced_web::*;
#[cfg(not(target_arch = "wasm32"))]
pub use crate::iced_ggez::*;

View file

@ -1,14 +0,0 @@
use super::Renderer;
use ggez::graphics::{self, Color};
pub use iced::{button, slider, Button, Slider};
pub type Text = iced::Text<Color>;
pub type Checkbox<Message> = iced::Checkbox<Color, Message>;
pub type Radio<Message> = iced::Radio<Color, Message>;
pub type Image = iced::Image<graphics::Image>;
pub type Column<'a, Message> = iced::Column<'a, Message, Renderer<'a>>;
pub type Row<'a, Message> = iced::Row<'a, Message, Renderer<'a>>;
pub type Element<'a, Message> = iced::Element<'a, Message, Renderer<'a>>;

19
native/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "iced_native"
version = "0.1.0-alpha"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A renderer-agnostic library for native GUIs"
license = "MIT"
repository = "https://github.com/hecrj/iced"
[package.metadata.docs.rs]
features = ["winit"]
[dependencies]
iced_core = { version = "0.1.0-alpha", path = "../core" }
stretch = "0.2"
twox-hash = "1.5"
# Enable to obtain conversion traits
winit = { version = "0.20.0-alpha3", optional = true }

View file

@ -1,7 +1,7 @@
use stretch::{geometry, result};
use crate::{
renderer, Event, Hasher, Layout, MouseCursor, Node, Point, Widget,
renderer, Color, Event, Hasher, Layout, MouseCursor, Node, Point, Widget,
};
/// A generic [`Widget`].
@ -87,7 +87,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
///
/// ```
/// # mod counter {
/// # use iced::{button, Button};
/// # use iced_native::{button, Button};
/// #
/// # #[derive(Debug, Clone, Copy)]
/// # pub enum Message {}
@ -101,19 +101,21 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
/// # }
/// #
/// # mod iced_wgpu {
/// # use iced::{
/// # button, MouseCursor, Node, Point, Rectangle, Style,
/// # use iced_native::{
/// # button, Button, MouseCursor, Node, Point, Rectangle, Style, Layout
/// # };
/// # pub struct Renderer;
/// #
/// # impl button::Renderer for Renderer {
/// # fn draw(
/// # fn node<Message>(&self, _button: &Button<'_, Message>) -> Node {
/// # Node::new(Style::default())
/// # }
/// #
/// # fn draw<Message>(
/// # &mut self,
/// # _button: &Button<'_, Message>,
/// # _layout: Layout<'_>,
/// # _cursor_position: Point,
/// # _bounds: Rectangle,
/// # _state: &button::State,
/// # _label: &str,
/// # _class: button::Class,
/// # ) -> MouseCursor {
/// # MouseCursor::OutOfBounds
/// # }
@ -130,7 +132,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
/// # pub enum Message {
/// # Counter(usize, counter::Message)
/// # }
/// use iced::{Element, Row};
/// use iced_native::{Element, Row};
/// use iced_wgpu::Renderer;
///
/// impl ManyCounters {
@ -210,20 +212,23 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
///
/// [`Element`]: struct.Element.html
/// [`Renderer`]: trait.Renderer.html
pub fn explain(
pub fn explain<C: Into<Color>>(
self,
color: Renderer::Color,
color: C,
) -> Element<'a, Message, Renderer>
where
Message: 'static,
Renderer: 'a + renderer::Debugger,
{
Element {
widget: Box::new(Explain::new(self, color)),
widget: Box::new(Explain::new(self, color.into())),
}
}
pub(crate) fn compute_layout(&self, renderer: &Renderer) -> result::Layout {
pub(crate) fn compute_layout(
&self,
renderer: &mut Renderer,
) -> result::Layout {
let node = self.widget.node(renderer);
node.0.compute_layout(geometry::Size::undefined()).unwrap()
@ -264,7 +269,7 @@ impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>
where
A: Copy,
{
fn node(&self, renderer: &Renderer) -> Node {
fn node(&self, renderer: &mut Renderer) -> Node {
self.widget.node(renderer)
}
@ -306,7 +311,7 @@ where
struct Explain<'a, Message, Renderer: renderer::Debugger> {
element: Element<'a, Message, Renderer>,
color: Renderer::Color,
color: Color,
}
impl<'a, Message, Renderer> std::fmt::Debug for Explain<'a, Message, Renderer>
@ -324,10 +329,7 @@ impl<'a, Message, Renderer> Explain<'a, Message, Renderer>
where
Renderer: renderer::Debugger,
{
fn new(
element: Element<'a, Message, Renderer>,
color: Renderer::Color,
) -> Self {
fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self {
Explain { element, color }
}
}
@ -337,7 +339,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
where
Renderer: renderer::Debugger,
{
fn node(&self, renderer: &Renderer) -> Node {
fn node(&self, renderer: &mut Renderer) -> Node {
self.element.widget.node(renderer)
}

View file

@ -12,7 +12,7 @@ use crate::{Point, Rectangle, Vector};
/// [`Widget::on_event`]: widget/trait.Widget.html#method.on_event
/// [`Widget::draw`]: widget/trait.Widget.html#tymethod.draw
/// [`Widget::node`]: widget/trait.Widget.html#tymethod.node
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub struct Layout<'a> {
layout: &'a result::Layout,
position: Point,

228
native/src/lib.rs Normal file
View file

@ -0,0 +1,228 @@
//! Iced is a renderer-agnostic GUI library focused on simplicity and
//! type-safety. Inspired by [Elm].
//!
//! # Features
//! * Simple, easy-to-use, renderer-agnostic API
//! * Responsive, flexbox-based layouting
//! * Type-safe, reactive programming model
//! * Built-in widgets
//! * Custom widget support
//!
//! Check out the [repository] and the [examples] for more details!
//!
//! [examples]: https://github.com/hecrj/iced/tree/0.1.0/examples
//! [repository]: https://github.com/hecrj/iced
//!
//! # Usage
//! Inspired by [The Elm Architecture], Iced expects you to split user interfaces
//! into four different concepts:
//!
//! * __State__ — the state of your application
//! * __Messages__ — user interactions or meaningful events that you care
//! about
//! * __View logic__ — a way to display your __state__ as widgets that
//! may produce __messages__ on user interaction
//! * __Update logic__ — a way to react to __messages__ and update your
//! __state__
//!
//! We can build something to see how this works! Let's say we want a simple counter
//! that can be incremented and decremented using two buttons.
//!
//! We start by modelling the __state__ of our application:
//!
//! ```
//! use iced_native::button;
//!
//! struct Counter {
//! // The counter value
//! value: i32,
//!
//! // The local state of the two buttons
//! increment_button: button::State,
//! decrement_button: button::State,
//! }
//! ```
//!
//! Next, we need to define the possible user interactions of our counter:
//! the button presses. These interactions are our __messages__:
//!
//! ```
//! #[derive(Debug, Clone, Copy)]
//! pub enum Message {
//! IncrementPressed,
//! DecrementPressed,
//! }
//! ```
//!
//! Now, let's show the actual counter by putting it all together in our
//! __view logic__:
//!
//! ```
//! # use iced_native::button;
//! #
//! # struct Counter {
//! # // The counter value
//! # value: i32,
//! #
//! # // The local state of the two buttons
//! # increment_button: button::State,
//! # decrement_button: button::State,
//! # }
//! #
//! # #[derive(Debug, Clone, Copy)]
//! # pub enum Message {
//! # IncrementPressed,
//! # DecrementPressed,
//! # }
//! #
//! # mod iced_wgpu {
//! # use iced_native::{
//! # button, text, Button, Text,
//! # MouseCursor, Node, Point, Rectangle, Style, Color, Layout
//! # };
//! #
//! # pub struct Renderer {}
//! #
//! # impl button::Renderer for Renderer {
//! # fn node<Message>(
//! # &self,
//! # _button: &Button<'_, Message>
//! # ) -> Node {
//! # Node::new(Style::default())
//! # }
//! #
//! # fn draw<Message>(
//! # &mut self,
//! # _button: &Button<'_, Message>,
//! # _layout: Layout<'_>,
//! # _cursor_position: Point,
//! # ) -> MouseCursor {
//! # MouseCursor::OutOfBounds
//! # }
//! # }
//! #
//! # impl text::Renderer for Renderer {
//! # fn node(&self, _text: &Text) -> Node {
//! # Node::new(Style::default())
//! # }
//! #
//! # fn draw(
//! # &mut self,
//! # _text: &Text,
//! # _layout: Layout<'_>,
//! # ) {
//! # }
//! # }
//! # }
//! use iced_native::{Button, Column, Text};
//! use iced_wgpu::Renderer; // Iced does not include a renderer! We need to bring our own!
//!
//! impl Counter {
//! pub fn view(&mut self) -> Column<Message, Renderer> {
//! // We use a column: a simple vertical layout
//! Column::new()
//! .push(
//! // The increment button. We tell it to produce an
//! // `IncrementPressed` message when pressed
//! Button::new(&mut self.increment_button, "+")
//! .on_press(Message::IncrementPressed),
//! )
//! .push(
//! // We show the value of the counter here
//! Text::new(&self.value.to_string()).size(50),
//! )
//! .push(
//! // The decrement button. We tell it to produce a
//! // `DecrementPressed` message when pressed
//! Button::new(&mut self.decrement_button, "-")
//! .on_press(Message::DecrementPressed),
//! )
//! }
//! }
//! ```
//!
//! Finally, we need to be able to react to any produced __messages__ and change
//! our __state__ accordingly in our __update logic__:
//!
//! ```
//! # use iced_native::button;
//! #
//! # struct Counter {
//! # // The counter value
//! # value: i32,
//! #
//! # // The local state of the two buttons
//! # increment_button: button::State,
//! # decrement_button: button::State,
//! # }
//! #
//! # #[derive(Debug, Clone, Copy)]
//! # pub enum Message {
//! # IncrementPressed,
//! # DecrementPressed,
//! # }
//! impl Counter {
//! // ...
//!
//! pub fn update(&mut self, message: Message) {
//! match message {
//! Message::IncrementPressed => {
//! self.value += 1;
//! }
//! Message::DecrementPressed => {
//! self.value -= 1;
//! }
//! }
//! }
//! }
//! ```
//!
//! And that's everything! We just wrote a whole user interface. Iced is now able
//! to:
//!
//! 1. Take the result of our __view logic__ and layout its widgets.
//! 1. Process events from our system and produce __messages__ for our
//! __update logic__.
//! 1. Draw the resulting user interface using our chosen __renderer__.
//!
//! Check out the [`UserInterface`] type to learn how to wire everything up!
//!
//! [Elm]: https://elm-lang.org/
//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/
//! [documentation]: https://docs.rs/iced
//! [examples]: https://github.com/hecrj/iced/tree/master/examples
//! [`UserInterface`]: struct.UserInterface.html
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![deny(unsafe_code)]
#![deny(rust_2018_idioms)]
pub mod input;
pub mod renderer;
pub mod widget;
mod element;
mod event;
mod hasher;
mod layout;
mod mouse_cursor;
mod node;
mod style;
mod user_interface;
pub(crate) use iced_core::Vector;
pub use iced_core::{Align, Color, Justify, Length, Point, Rectangle};
#[doc(no_inline)]
pub use stretch::{geometry::Size, number::Number};
pub use element::Element;
pub use event::Event;
pub use hasher::Hasher;
pub use layout::Layout;
pub use mouse_cursor::MouseCursor;
pub use node::Node;
pub use style::Style;
pub use user_interface::{Cache, UserInterface};
pub use widget::*;

View file

@ -17,19 +17,12 @@
//! [`text::Renderer`]: ../widget/text/trait.Renderer.html
//! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html
//! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html
use crate::Layout;
use crate::{Color, Layout};
/// A renderer able to graphically explain a [`Layout`].
///
/// [`Layout`]: ../struct.Layout.html
pub trait Debugger {
/// The color type that will be used to configure the _explanation_.
///
/// This is the type that will be asked in [`Element::explain`].
///
/// [`Element::explain`]: ../struct.Element.html#method.explain
type Color: Copy;
/// Explains the [`Layout`] of an [`Element`] for debugging purposes.
///
/// This will be called when [`Element::explain`] has been used. It should
@ -41,5 +34,5 @@ pub trait Debugger {
/// [`Layout`]: struct.Layout.html
/// [`Element`]: struct.Element.html
/// [`Element::explain`]: struct.Element.html#method.explain
fn explain(&mut self, layout: &Layout<'_>, color: Self::Color);
fn explain(&mut self, layout: &Layout<'_>, color: Color);
}

156
native/src/style.rs Normal file
View file

@ -0,0 +1,156 @@
use crate::{Align, Justify, Length};
use stretch::style;
/// The appearance of a [`Node`].
///
/// [`Node`]: struct.Node.html
#[derive(Debug, Clone, Copy)]
pub struct Style(pub(crate) style::Style);
impl Default for Style {
fn default() -> Style {
Style::new()
}
}
impl Style {
/// Creates a new [`Style`].
///
/// [`Style`]: struct.Style.html
pub fn new() -> Self {
Style(style::Style {
align_items: style::AlignItems::FlexStart,
justify_content: style::JustifyContent::FlexStart,
..style::Style::default()
})
}
/// Defines the width of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn width(mut self, width: Length) -> Self {
self.0.size.width = into_dimension(width);
self
}
/// Defines the height of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn height(mut self, height: Length) -> Self {
self.0.size.height = into_dimension(height);
self
}
/// Defines the minimum width of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn min_width(mut self, min_width: Length) -> Self {
self.0.min_size.width = into_dimension(min_width);
self
}
/// Defines the maximum width of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn max_width(mut self, max_width: Length) -> Self {
self.0.max_size.width = into_dimension(max_width);
self
}
/// Defines the minimum height of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn min_height(mut self, min_height: Length) -> Self {
self.0.min_size.height = into_dimension(min_height);
self
}
/// Defines the maximum height of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn max_height(mut self, max_height: Length) -> Self {
self.0.max_size.height = into_dimension(max_height);
self
}
pub(crate) fn align_items(mut self, align: Align) -> Self {
self.0.align_items = into_align_items(align);
self
}
pub(crate) fn justify_content(mut self, justify: Justify) -> Self {
self.0.justify_content = into_justify_content(justify);
self
}
/// Sets the alignment of a [`Node`].
///
/// If the [`Node`] is inside a...
///
/// * [`Column`], this setting will affect its __horizontal__ alignment.
/// * [`Row`], this setting will affect its __vertical__ alignment.
///
/// [`Node`]: struct.Node.html
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
pub fn align_self(mut self, align: Option<Align>) -> Self {
self.0.align_self = match align {
Some(align) => into_align_self(align),
None => stretch::style::AlignSelf::Auto,
};
self
}
/// Sets the padding of a [`Node`].
///
/// [`Node`]: struct.Node.html
pub fn padding(mut self, units: u16) -> Self {
self.0.padding = stretch::geometry::Rect {
start: style::Dimension::Points(units as f32),
end: style::Dimension::Points(units as f32),
top: style::Dimension::Points(units as f32),
bottom: style::Dimension::Points(units as f32),
};
self
}
}
fn into_dimension(length: Length) -> style::Dimension {
match length {
Length::Shrink => style::Dimension::Undefined,
Length::Fill => style::Dimension::Percent(1.0),
Length::Units(units) => style::Dimension::Points(units as f32),
}
}
fn into_align_items(align: Align) -> style::AlignItems {
match align {
Align::Start => style::AlignItems::FlexStart,
Align::Center => style::AlignItems::Center,
Align::End => style::AlignItems::FlexEnd,
Align::Stretch => style::AlignItems::Stretch,
}
}
fn into_align_self(align: Align) -> style::AlignSelf {
match align {
Align::Start => style::AlignSelf::FlexStart,
Align::Center => style::AlignSelf::Center,
Align::End => style::AlignSelf::FlexEnd,
Align::Stretch => style::AlignSelf::Stretch,
}
}
fn into_justify_content(justify: Justify) -> style::JustifyContent {
match justify {
Justify::Start => style::JustifyContent::FlexStart,
Justify::Center => style::JustifyContent::Center,
Justify::End => style::JustifyContent::FlexEnd,
Justify::SpaceBetween => style::JustifyContent::SpaceBetween,
Justify::SpaceAround => style::JustifyContent::SpaceAround,
Justify::SpaceEvenly => style::JustifyContent::SpaceEvenly,
}
}

View file

@ -35,7 +35,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// is naive way to set up our application loop:
///
/// ```no_run
/// use iced::{UserInterface, Cache};
/// use iced_native::{UserInterface, Cache};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@ -46,7 +46,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// # }
/// # }
/// #
/// # use iced::Column;
/// # use iced_native::Column;
/// #
/// # pub struct Counter;
/// #
@ -69,7 +69,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// let user_interface = UserInterface::build(
/// counter.view(),
/// cache,
/// &renderer,
/// &mut renderer,
/// );
///
/// // Update and draw the user interface here...
@ -82,7 +82,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
pub fn build<E: Into<Element<'a, Message, Renderer>>>(
root: E,
cache: Cache,
renderer: &Renderer,
renderer: &mut Renderer,
) -> Self {
let root = root.into();
@ -118,7 +118,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// [the previous example](#example):
///
/// ```no_run
/// use iced::{UserInterface, Cache};
/// use iced_native::{UserInterface, Cache};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@ -129,7 +129,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// # }
/// # }
/// #
/// # use iced::Column;
/// # use iced_native::Column;
/// #
/// # pub struct Counter;
/// #
@ -153,7 +153,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// let mut user_interface = UserInterface::build(
/// counter.view(),
/// cache,
/// &renderer,
/// &mut renderer,
/// );
///
/// // Update the user interface
@ -203,7 +203,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// [completing the last example](#example-1):
///
/// ```no_run
/// use iced::{UserInterface, Cache};
/// use iced_native::{UserInterface, Cache};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@ -214,7 +214,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// # }
/// # }
/// #
/// # use iced::Column;
/// # use iced_native::Column;
/// #
/// # pub struct Counter;
/// #
@ -236,7 +236,7 @@ impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> {
/// let mut user_interface = UserInterface::build(
/// counter.view(),
/// cache,
/// &renderer,
/// &mut renderer,
/// );
///
/// let messages = user_interface.update(events.drain(..));
@ -302,7 +302,7 @@ impl Cache {
Cache {
hash: hasher.finish(),
layout: root.compute_layout(&()),
layout: root.compute_layout(&mut ()),
cursor_position: Point::new(0.0, 0.0),
}
}

View file

@ -15,7 +15,7 @@
//! module. Therefore, you can directly type:
//!
//! ```
//! use iced::{button, Button, Widget};
//! use iced_native::{button, Button, Widget};
//! ```
//!
//! [`Widget`]: trait.Widget.html
@ -26,19 +26,25 @@ mod row;
pub mod button;
pub mod checkbox;
pub mod image;
//pub mod progress_bar;
pub mod radio;
pub mod slider;
pub mod text;
#[doc(no_inline)]
pub use button::Button;
#[doc(no_inline)]
pub use checkbox::Checkbox;
#[doc(no_inline)]
pub use column::Column;
#[doc(no_inline)]
pub use image::Image;
//pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
#[doc(no_inline)]
pub use row::Row;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
use crate::{Event, Hasher, Layout, MouseCursor, Node, Point};
@ -59,7 +65,7 @@ pub trait Widget<Message, Renderer>: std::fmt::Debug {
/// [`Node`]: ../struct.Node.html
/// [`Widget`]: trait.Widget.html
/// [`Layout`]: ../struct.Layout.html
fn node(&self, renderer: &Renderer) -> Node;
fn node(&self, renderer: &mut Renderer) -> Node;
/// Draws the [`Widget`] using the associated `Renderer`.
///

111
native/src/widget/button.rs Normal file
View file

@ -0,0 +1,111 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`] and a [`Class`].
//!
//! [`Button`]: struct.Button.html
//! [`State`]: struct.State.html
//! [`Class`]: enum.Class.html
use crate::input::{mouse, ButtonState};
use crate::{Element, Event, Hasher, Layout, MouseCursor, Node, Point, Widget};
use std::hash::Hash;
pub use iced_core::button::*;
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message>
where
Renderer: self::Renderer,
Message: Copy + std::fmt::Debug,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state,
}) => {
if let Some(on_press) = self.on_press {
let bounds = layout.bounds();
match state {
ButtonState::Pressed => {
self.state.is_pressed =
bounds.contains(cursor_position);
}
ButtonState::Released => {
let is_clicked = self.state.is_pressed
&& bounds.contains(cursor_position);
self.state.is_pressed = false;
if is_clicked {
messages.push(on_press);
}
}
}
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout, cursor_position)
}
fn hash_layout(&self, state: &mut Hasher) {
self.label.hash(state);
self.width.hash(state);
self.align_self.hash(state);
}
}
/// The renderer of a [`Button`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Button`] in your user interface.
///
/// [`Button`]: struct.Button.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Creates a [`Node`] for the provided [`Button`].
///
/// [`Node`]: ../../struct.Node.html
/// [`Button`]: struct.Button.html
fn node<Message>(&self, button: &Button<'_, Message>) -> Node;
/// Draws a [`Button`].
///
/// [`Button`]: struct.Button.html
fn draw<Message>(
&mut self,
button: &Button<'_, Message>,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Button<'a, Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static + Copy + std::fmt::Debug,
{
fn from(button: Button<'a, Message>) -> Element<'a, Message, Renderer> {
Element::new(button)
}
}

View file

@ -0,0 +1,95 @@
//! Show toggle controls using checkboxes.
use std::hash::Hash;
use crate::input::{mouse, ButtonState};
use crate::{Element, Event, Hasher, Layout, MouseCursor, Node, Point, Widget};
pub use iced_core::Checkbox;
impl<Message, Renderer> Widget<Message, Renderer> for Checkbox<Message>
where
Renderer: self::Renderer,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state: ButtonState::Pressed,
}) => {
let mouse_over = layout
.children()
.any(|child| child.bounds().contains(cursor_position));
if mouse_over {
messages.push((self.on_toggle)(!self.is_checked));
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout, cursor_position)
}
fn hash_layout(&self, state: &mut Hasher) {
self.label.hash(state);
}
}
/// The renderer of a [`Checkbox`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Checkbox`] in your user interface.
///
/// [`Checkbox`]: struct.Checkbox.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Creates a [`Node`] for the provided [`Checkbox`].
///
/// [`Node`]: ../../struct.Node.html
/// [`Checkbox`]: struct.Checkbox.html
fn node<Message>(&mut self, checkbox: &Checkbox<Message>) -> Node;
/// Draws a [`Checkbox`].
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Checkbox`]
/// * the bounds of the label of the [`Checkbox`]
/// * whether the [`Checkbox`] is checked or not
///
/// [`Checkbox`]: struct.Checkbox.html
fn draw<Message>(
&mut self,
checkbox: &Checkbox<Message>,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Checkbox<Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static,
{
fn from(checkbox: Checkbox<Message>) -> Element<'a, Message, Renderer> {
Element::new(checkbox)
}
}

118
native/src/widget/column.rs Normal file
View file

@ -0,0 +1,118 @@
use std::hash::Hash;
use crate::{
Element, Event, Hasher, Layout, MouseCursor, Node, Point, Style, Widget,
};
/// A container that distributes its contents vertically.
pub type Column<'a, Message, Renderer> =
iced_core::Column<Element<'a, Message, Renderer>>;
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Column<'a, Message, Renderer>
{
fn node(&self, renderer: &mut Renderer) -> Node {
let mut children: Vec<Node> = self
.children
.iter()
.map(|child| {
let mut node = child.widget.node(renderer);
let mut style = node.0.style();
style.margin.bottom =
stretch::style::Dimension::Points(f32::from(self.spacing));
node.0.set_style(style);
node
})
.collect();
if let Some(node) = children.last_mut() {
let mut style = node.0.style();
style.margin.bottom = stretch::style::Dimension::Undefined;
node.0.set_style(style);
}
let mut style = Style::default()
.width(self.width)
.height(self.height)
.max_width(self.max_width)
.max_height(self.max_height)
.padding(self.padding)
.align_self(self.align_self)
.align_items(self.align_items)
.justify_content(self.justify_content);
style.0.flex_direction = stretch::style::FlexDirection::Column;
Node::with_children(style, children)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
},
);
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let mut cursor = MouseCursor::OutOfBounds;
self.children.iter().zip(layout.children()).for_each(
|(child, layout)| {
let new_cursor =
child.widget.draw(renderer, layout, cursor_position);
if new_cursor != MouseCursor::OutOfBounds {
cursor = new_cursor;
}
},
);
cursor
}
fn hash_layout(&self, state: &mut Hasher) {
0.hash(state);
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_self.hash(state);
self.align_items.hash(state);
self.justify_content.hash(state);
self.spacing.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
}
impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a,
Message: 'static,
{
fn from(
column: Column<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(column)
}
}

View file

@ -0,0 +1,66 @@
//! Display images in your user interface.
use crate::{Element, Hasher, Layout, MouseCursor, Node, Point, Widget};
use std::hash::Hash;
pub use iced_core::Image;
impl<I, Message, Renderer> Widget<Message, Renderer> for Image<I>
where
Renderer: self::Renderer<I>,
I: Clone,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout);
MouseCursor::OutOfBounds
}
fn hash_layout(&self, state: &mut Hasher) {
self.width.hash(state);
self.height.hash(state);
self.align_self.hash(state);
}
}
/// The renderer of an [`Image`].
///
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Image`] in your user interface.
///
/// [`Image`]: struct.Image.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer<I> {
/// Creates a [`Node`] for the provided [`Image`].
///
/// You should probably keep the original aspect ratio, if possible.
///
/// [`Node`]: ../../struct.Node.html
/// [`Image`]: struct.Image.html
fn node(&mut self, image: &Image<I>) -> Node;
/// Draws an [`Image`].
///
/// [`Image`]: struct.Image.html
fn draw(&mut self, image: &Image<I>, layout: Layout<'_>);
}
impl<'a, I, Message, Renderer> From<Image<I>> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer<I>,
I: Clone + 'a,
{
fn from(image: Image<I>) -> Element<'a, Message, Renderer> {
Element::new(image)
}
}

View file

@ -0,0 +1,92 @@
//! Create choices using radio buttons.
use crate::input::{mouse, ButtonState};
use crate::{Element, Event, Hasher, Layout, MouseCursor, Node, Point, Widget};
use std::hash::Hash;
pub use iced_core::Radio;
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message>
where
Renderer: self::Renderer,
Message: Copy + std::fmt::Debug,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state: ButtonState::Pressed,
}) => {
if layout.bounds().contains(cursor_position) {
messages.push(self.on_click);
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout, cursor_position)
}
fn hash_layout(&self, state: &mut Hasher) {
self.label.hash(state);
}
}
/// The renderer of a [`Radio`] button.
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Radio`] button in your user interface.
///
/// [`Radio`]: struct.Radio.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Creates a [`Node`] for the provided [`Radio`].
///
/// [`Node`]: ../../struct.Node.html
/// [`Radio`]: struct.Radio.html
fn node<Message>(&mut self, radio: &Radio<Message>) -> Node;
/// Draws a [`Radio`] button.
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Radio`]
/// * the bounds of the label of the [`Radio`]
/// * whether the [`Radio`] is selected or not
///
/// [`Radio`]: struct.Radio.html
fn draw<Message>(
&mut self,
radio: &Radio<Message>,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Radio<Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static + Copy + std::fmt::Debug,
{
fn from(checkbox: Radio<Message>) -> Element<'a, Message, Renderer> {
Element::new(checkbox)
}
}

117
native/src/widget/row.rs Normal file
View file

@ -0,0 +1,117 @@
use std::hash::Hash;
use crate::{
Element, Event, Hasher, Layout, MouseCursor, Node, Point, Style, Widget,
};
/// A container that distributes its contents horizontally.
pub type Row<'a, Message, Renderer> =
iced_core::Row<Element<'a, Message, Renderer>>;
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Row<'a, Message, Renderer>
{
fn node(&self, renderer: &mut Renderer) -> Node {
let mut children: Vec<Node> = self
.children
.iter()
.map(|child| {
let mut node = child.widget.node(renderer);
let mut style = node.0.style();
style.margin.end =
stretch::style::Dimension::Points(f32::from(self.spacing));
node.0.set_style(style);
node
})
.collect();
if let Some(node) = children.last_mut() {
let mut style = node.0.style();
style.margin.end = stretch::style::Dimension::Undefined;
node.0.set_style(style);
}
let mut style = Style::default()
.width(self.width)
.height(self.height)
.max_width(self.max_width)
.max_height(self.max_height)
.padding(self.padding)
.align_self(self.align_self)
.align_items(self.align_items)
.justify_content(self.justify_content);
style.0.flex_direction = stretch::style::FlexDirection::Row;
Node::with_children(style, children)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
},
);
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let mut cursor = MouseCursor::OutOfBounds;
self.children.iter().zip(layout.children()).for_each(
|(child, layout)| {
let new_cursor =
child.widget.draw(renderer, layout, cursor_position);
if new_cursor != MouseCursor::OutOfBounds {
cursor = new_cursor;
}
},
);
cursor
}
fn hash_layout(&self, state: &mut Hasher) {
1.hash(state);
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_self.hash(state);
self.align_items.hash(state);
self.justify_content.hash(state);
self.spacing.hash(state);
self.spacing.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
}
impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a,
Message: 'static,
{
fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(row)
}
}

126
native/src/widget/slider.rs Normal file
View file

@ -0,0 +1,126 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
//!
//! [`Slider`]: struct.Slider.html
//! [`State`]: struct.State.html
use std::hash::Hash;
use crate::input::{mouse, ButtonState};
use crate::{Element, Event, Hasher, Layout, MouseCursor, Node, Point, Widget};
pub use iced_core::slider::*;
impl<'a, Message, Renderer> Widget<Message, Renderer> for Slider<'a, Message>
where
Renderer: self::Renderer,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
let mut change = || {
let bounds = layout.bounds();
if cursor_position.x <= bounds.x {
messages.push((self.on_change)(*self.range.start()));
} else if cursor_position.x >= bounds.x + bounds.width {
messages.push((self.on_change)(*self.range.end()));
} else {
let percent = (cursor_position.x - bounds.x) / bounds.width;
let value = (self.range.end() - self.range.start()) * percent
+ self.range.start();
messages.push((self.on_change)(value));
}
};
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state,
}) => match state {
ButtonState::Pressed => {
if layout.bounds().contains(cursor_position) {
change();
self.state.is_dragging = true;
}
}
ButtonState::Released => {
self.state.is_dragging = false;
}
},
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if self.state.is_dragging {
change();
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout, cursor_position)
}
fn hash_layout(&self, state: &mut Hasher) {
self.width.hash(state);
}
}
/// The renderer of a [`Slider`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Slider`] in your user interface.
///
/// [`Slider`]: struct.Slider.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Creates a [`Node`] for the provided [`Radio`].
///
/// [`Node`]: ../../struct.Node.html
/// [`Radio`]: struct.Radio.html
fn node<Message>(&self, slider: &Slider<'_, Message>) -> Node;
/// Draws a [`Slider`].
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Slider`]
/// * the local state of the [`Slider`]
/// * the range of values of the [`Slider`]
/// * the current value of the [`Slider`]
///
/// [`Slider`]: struct.Slider.html
/// [`State`]: struct.State.html
/// [`Class`]: enum.Class.html
fn draw<Message>(
&mut self,
slider: &Slider<'_, Message>,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Slider<'a, Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static,
{
fn from(slider: Slider<'a, Message>) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
}

79
native/src/widget/text.rs Normal file
View file

@ -0,0 +1,79 @@
//! Write some text for your users to read.
use crate::{Element, Hasher, Layout, MouseCursor, Node, Point, Widget};
use std::hash::Hash;
pub use iced_core::text::*;
impl<Message, Renderer> Widget<Message, Renderer> for Text
where
Renderer: self::Renderer,
{
fn node(&self, renderer: &mut Renderer) -> Node {
renderer.node(&self)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self, layout);
MouseCursor::OutOfBounds
}
fn hash_layout(&self, state: &mut Hasher) {
self.content.hash(state);
self.size.hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
/// The renderer of a [`Text`] fragment.
///
/// Your [renderer] will need to implement this trait before being
/// able to use [`Text`] in your [`UserInterface`].
///
/// [`Text`]: struct.Text.html
/// [renderer]: ../../renderer/index.html
/// [`UserInterface`]: ../../struct.UserInterface.html
pub trait Renderer {
/// Creates a [`Node`] with the given [`Style`] for the provided [`Text`]
/// contents and size.
///
/// You should probably use [`Node::with_measure`] to allow [`Text`] to
/// adapt to the dimensions of its container.
///
/// [`Node`]: ../../struct.Node.html
/// [`Style`]: ../../struct.Style.html
/// [`Text`]: struct.Text.html
/// [`Node::with_measure`]: ../../struct.Node.html#method.with_measure
fn node(&self, text: &Text) -> Node;
/// Draws a [`Text`] fragment.
///
/// It receives:
/// * the bounds of the [`Text`]
/// * the contents of the [`Text`]
/// * the size of the [`Text`]
/// * the color of the [`Text`]
/// * the [`HorizontalAlignment`] of the [`Text`]
/// * the [`VerticalAlignment`] of the [`Text`]
///
/// [`Text`]: struct.Text.html
/// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
/// [`VerticalAlignment`]: enum.VerticalAlignment.html
fn draw(&mut self, text: &Text, layout: Layout<'_>);
}
impl<'a, Message, Renderer> From<Text> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
{
fn from(text: Text) -> Element<'a, Message, Renderer> {
Element::new(text)
}
}

View file

@ -1,229 +0,0 @@
//! Iced is a renderer-agnostic GUI library focused on simplicity and
//! type-safety. Inspired by [Elm].
//!
//! # Features
//! * Simple, easy-to-use, renderer-agnostic API
//! * Responsive, flexbox-based layouting
//! * Type-safe, reactive programming model
//! * Built-in widgets
//! * Custom widget support
//!
//! Check out the [repository] and the [examples] for more details!
//!
//! [examples]: https://github.com/hecrj/iced/tree/0.1.0/examples
//! [repository]: https://github.com/hecrj/iced
//!
//! # Usage
//! Inspired by [The Elm Architecture], Iced expects you to split user interfaces
//! into four different concepts:
//!
//! * __State__ — the state of your application
//! * __Messages__ — user interactions or meaningful events that you care
//! about
//! * __View logic__ — a way to display your __state__ as widgets that
//! may produce __messages__ on user interaction
//! * __Update logic__ — a way to react to __messages__ and update your
//! __state__
//!
//! We can build something to see how this works! Let's say we want a simple counter
//! that can be incremented and decremented using two buttons.
//!
//! We start by modelling the __state__ of our application:
//!
//! ```
//! use iced::button;
//!
//! struct Counter {
//! // The counter value
//! value: i32,
//!
//! // The local state of the two buttons
//! increment_button: button::State,
//! decrement_button: button::State,
//! }
//! ```
//!
//! Next, we need to define the possible user interactions of our counter:
//! the button presses. These interactions are our __messages__:
//!
//! ```
//! #[derive(Debug, Clone, Copy)]
//! pub enum Message {
//! IncrementPressed,
//! DecrementPressed,
//! }
//! ```
//!
//! Now, let's show the actual counter by putting it all together in our
//! __view logic__:
//!
//! ```
//! # use iced::button;
//! #
//! # struct Counter {
//! # // The counter value
//! # value: i32,
//! #
//! # // The local state of the two buttons
//! # increment_button: button::State,
//! # decrement_button: button::State,
//! # }
//! #
//! # #[derive(Debug, Clone, Copy)]
//! # pub enum Message {
//! # IncrementPressed,
//! # DecrementPressed,
//! # }
//! #
//! # mod iced_wgpu {
//! # use iced::{
//! # button, text, text::HorizontalAlignment, text::VerticalAlignment,
//! # MouseCursor, Node, Point, Rectangle, Style,
//! # };
//! #
//! # pub struct Renderer {}
//! #
//! # impl button::Renderer for Renderer {
//! # fn draw(
//! # &mut self,
//! # _cursor_position: Point,
//! # _bounds: Rectangle,
//! # _state: &button::State,
//! # _label: &str,
//! # _class: button::Class,
//! # ) -> MouseCursor {
//! # MouseCursor::OutOfBounds
//! # }
//! # }
//! #
//! # impl text::Renderer<[f32; 4]> for Renderer {
//! # fn node(&self, style: Style, _content: &str, _size: Option<u16>) -> Node {
//! # Node::new(style)
//! # }
//! #
//! # fn draw(
//! # &mut self,
//! # _bounds: Rectangle,
//! # _content: &str,
//! # _size: Option<u16>,
//! # _color: Option<[f32; 4]>,
//! # _horizontal_alignment: HorizontalAlignment,
//! # _vertical_alignment: VerticalAlignment,
//! # ) {
//! # }
//! # }
//! # }
//! use iced::{Button, Column, Text};
//! use iced_wgpu::Renderer; // Iced does not include a renderer! We need to bring our own!
//!
//! impl Counter {
//! pub fn view(&mut self) -> Column<Message, Renderer> {
//! // We use a column: a simple vertical layout
//! Column::new()
//! .push(
//! // The increment button. We tell it to produce an
//! // `IncrementPressed` message when pressed
//! Button::new(&mut self.increment_button, "+")
//! .on_press(Message::IncrementPressed),
//! )
//! .push(
//! // We show the value of the counter here
//! Text::new(&self.value.to_string()).size(50),
//! )
//! .push(
//! // The decrement button. We tell it to produce a
//! // `DecrementPressed` message when pressed
//! Button::new(&mut self.decrement_button, "-")
//! .on_press(Message::DecrementPressed),
//! )
//! }
//! }
//! ```
//!
//! Finally, we need to be able to react to any produced __messages__ and change
//! our __state__ accordingly in our __update logic__:
//!
//! ```
//! # use iced::button;
//! #
//! # struct Counter {
//! # // The counter value
//! # value: i32,
//! #
//! # // The local state of the two buttons
//! # increment_button: button::State,
//! # decrement_button: button::State,
//! # }
//! #
//! # #[derive(Debug, Clone, Copy)]
//! # pub enum Message {
//! # IncrementPressed,
//! # DecrementPressed,
//! # }
//! impl Counter {
//! // ...
//!
//! pub fn update(&mut self, message: Message) {
//! match message {
//! Message::IncrementPressed => {
//! self.value += 1;
//! }
//! Message::DecrementPressed => {
//! self.value -= 1;
//! }
//! }
//! }
//! }
//! ```
//!
//! And that's everything! We just wrote a whole user interface. Iced is now able
//! to:
//!
//! 1. Take the result of our __view logic__ and layout its widgets.
//! 1. Process events from our system and produce __messages__ for our
//! __update logic__.
//! 1. Draw the resulting user interface using our chosen __renderer__.
//!
//! Check out the [`UserInterface`] type to learn how to wire everything up!
//!
//! [Elm]: https://elm-lang.org/
//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/
//! [documentation]: https://docs.rs/iced
//! [examples]: https://github.com/hecrj/iced/tree/master/examples
//! [`UserInterface`]: struct.UserInterface.html
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![deny(unsafe_code)]
#![deny(rust_2018_idioms)]
pub mod input;
pub mod renderer;
pub mod widget;
mod element;
mod event;
mod hasher;
mod layout;
mod mouse_cursor;
mod node;
mod point;
mod rectangle;
mod style;
mod user_interface;
mod vector;
#[doc(no_inline)]
pub use stretch::{geometry::Size, number::Number};
pub use element::Element;
pub use event::Event;
pub use hasher::Hasher;
pub use layout::Layout;
pub use mouse_cursor::MouseCursor;
pub use node::Node;
pub use point::Point;
pub use rectangle::Rectangle;
pub use style::{Align, Justify, Style};
pub use user_interface::{Cache, UserInterface};
pub(crate) use vector::Vector;
pub use widget::*;

View file

@ -1,262 +0,0 @@
use std::hash::{Hash, Hasher};
use stretch::{geometry, style};
/// The appearance of a [`Node`].
///
/// [`Node`]: struct.Node.html
#[derive(Debug, Clone, Copy)]
pub struct Style(pub(crate) style::Style);
impl Style {
/// Defines the width of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn width(mut self, width: u16) -> Self {
self.0.size.width = style::Dimension::Points(width as f32);
self
}
/// Defines the height of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn height(mut self, height: u16) -> Self {
self.0.size.height = style::Dimension::Points(height as f32);
self
}
/// Defines the minimum width of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn min_width(mut self, min_width: u16) -> Self {
self.0.min_size.width = style::Dimension::Points(min_width as f32);
self
}
/// Defines the maximum width of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn max_width(mut self, max_width: u16) -> Self {
self.0.max_size.width = style::Dimension::Points(max_width as f32);
self.fill_width()
}
/// Defines the minimum height of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn min_height(mut self, min_height: u16) -> Self {
self.0.min_size.height =
style::Dimension::Points(f32::from(min_height));
self
}
/// Defines the maximum height of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn max_height(mut self, max_height: u16) -> Self {
self.0.max_size.height =
style::Dimension::Points(f32::from(max_height));
self.fill_height()
}
/// Makes a [`Node`] fill all the horizontal available space.
///
/// [`Node`]: struct.Node.html
pub fn fill_width(mut self) -> Self {
self.0.size.width = stretch::style::Dimension::Percent(1.0);
self
}
/// Makes a [`Node`] fill all the vertical available space.
///
/// [`Node`]: struct.Node.html
pub fn fill_height(mut self) -> Self {
self.0.size.height = stretch::style::Dimension::Percent(1.0);
self
}
pub(crate) fn align_items(mut self, align: Align) -> Self {
self.0.align_items = align.into();
self
}
pub(crate) fn justify_content(mut self, justify: Justify) -> Self {
self.0.justify_content = justify.into();
self
}
/// Sets the alignment of a [`Node`].
///
/// If the [`Node`] is inside a...
///
/// * [`Column`], this setting will affect its __horizontal__ alignment.
/// * [`Row`], this setting will affect its __vertical__ alignment.
///
/// [`Node`]: struct.Node.html
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
pub fn align_self(mut self, align: Align) -> Self {
self.0.align_self = align.into();
self
}
/// Sets the padding of a [`Node`] in pixels.
///
/// [`Node`]: struct.Node.html
pub fn padding(mut self, px: u16) -> Self {
self.0.padding = stretch::geometry::Rect {
start: style::Dimension::Points(px as f32),
end: style::Dimension::Points(px as f32),
top: style::Dimension::Points(px as f32),
bottom: style::Dimension::Points(px as f32),
};
self
}
}
impl Default for Style {
fn default() -> Style {
Style(style::Style {
align_items: style::AlignItems::FlexStart,
justify_content: style::JustifyContent::FlexStart,
..style::Style::default()
})
}
}
impl Hash for Style {
fn hash<H: Hasher>(&self, state: &mut H) {
hash_size(&self.0.size, state);
hash_size(&self.0.min_size, state);
hash_size(&self.0.max_size, state);
hash_rect(&self.0.margin, state);
(self.0.flex_direction as u8).hash(state);
(self.0.align_items as u8).hash(state);
(self.0.justify_content as u8).hash(state);
(self.0.align_self as u8).hash(state);
(self.0.flex_grow as u32).hash(state);
}
}
fn hash_size<H: Hasher>(
size: &geometry::Size<style::Dimension>,
state: &mut H,
) {
hash_dimension(size.width, state);
hash_dimension(size.height, state);
}
fn hash_rect<H: Hasher>(
rect: &geometry::Rect<style::Dimension>,
state: &mut H,
) {
hash_dimension(rect.start, state);
hash_dimension(rect.end, state);
hash_dimension(rect.top, state);
hash_dimension(rect.bottom, state);
}
fn hash_dimension<H: Hasher>(dimension: style::Dimension, state: &mut H) {
match dimension {
style::Dimension::Undefined => state.write_u8(0),
style::Dimension::Auto => state.write_u8(1),
style::Dimension::Points(points) => {
state.write_u8(2);
(points as u32).hash(state);
}
style::Dimension::Percent(percent) => {
state.write_u8(3);
(percent as u32).hash(state);
}
}
}
/// Alignment on the cross axis of a container.
///
/// * On a [`Column`], it describes __horizontal__ alignment.
/// * On a [`Row`], it describes __vertical__ alignment.
///
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
/// Align at the start of the cross axis.
Start,
/// Align at the center of the cross axis.
Center,
/// Align at the end of the cross axis.
End,
/// Stretch over the cross axis.
Stretch,
}
#[doc(hidden)]
impl From<Align> for style::AlignItems {
fn from(align: Align) -> Self {
match align {
Align::Start => style::AlignItems::FlexStart,
Align::Center => style::AlignItems::Center,
Align::End => style::AlignItems::FlexEnd,
Align::Stretch => style::AlignItems::Stretch,
}
}
}
#[doc(hidden)]
impl From<Align> for style::AlignSelf {
fn from(align: Align) -> Self {
match align {
Align::Start => style::AlignSelf::FlexStart,
Align::Center => style::AlignSelf::Center,
Align::End => style::AlignSelf::FlexEnd,
Align::Stretch => style::AlignSelf::Stretch,
}
}
}
/// Distribution on the main axis of a container.
///
/// * On a [`Column`], it describes __vertical__ distribution.
/// * On a [`Row`], it describes __horizontal__ distribution.
///
/// [`Column`]: widget/struct.Column.html
/// [`Row`]: widget/struct.Row.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Justify {
/// Place items at the start of the main axis.
Start,
/// Place items at the center of the main axis.
Center,
/// Place items at the end of the main axis.
End,
/// Place items with space between.
SpaceBetween,
/// Place items with space around.
SpaceAround,
/// Place items with evenly distributed space.
SpaceEvenly,
}
#[doc(hidden)]
impl From<Justify> for style::JustifyContent {
fn from(justify: Justify) -> Self {
match justify {
Justify::Start => style::JustifyContent::FlexStart,
Justify::Center => style::JustifyContent::Center,
Justify::End => style::JustifyContent::FlexEnd,
Justify::SpaceBetween => style::JustifyContent::SpaceBetween,
Justify::SpaceAround => style::JustifyContent::SpaceAround,
Justify::SpaceEvenly => style::JustifyContent::SpaceEvenly,
}
}
}

View file

@ -1,282 +0,0 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`] and a [`Class`].
//!
//! [`Button`]: struct.Button.html
//! [`State`]: struct.State.html
//! [`Class`]: enum.Class.html
use crate::input::{mouse, ButtonState};
use crate::{
Align, Element, Event, Hasher, Layout, MouseCursor, Node, Point, Rectangle,
Style, Widget,
};
use std::hash::Hash;
/// A generic widget that produces a message when clicked.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`button::Renderer`] trait.
///
/// [`Widget`]: ../trait.Widget.html
/// [`button::Renderer`]: trait.Renderer.html
///
/// # Example
///
/// ```
/// use iced::{button, Button};
///
/// pub enum Message {
/// ButtonClicked,
/// }
///
/// let state = &mut button::State::new();
///
/// Button::new(state, "Click me!")
/// .on_press(Message::ButtonClicked);
/// ```
///
/// ![Button drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/button.png?raw=true)
pub struct Button<'a, Message> {
state: &'a mut State,
label: String,
class: Class,
on_press: Option<Message>,
style: Style,
}
impl<'a, Message> std::fmt::Debug for Button<'a, Message>
where
Message: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Button")
.field("state", &self.state)
.field("label", &self.label)
.field("class", &self.class)
.field("on_press", &self.on_press)
.field("style", &self.style)
.finish()
}
}
impl<'a, Message> Button<'a, Message> {
/// Creates a new [`Button`] with some local [`State`] and the given label.
///
/// The default [`Class`] of a new [`Button`] is [`Class::Primary`].
///
/// [`Button`]: struct.Button.html
/// [`State`]: struct.State.html
/// [`Class`]: enum.Class.html
/// [`Class::Primary`]: enum.Class.html#variant.Primary
pub fn new(state: &'a mut State, label: &str) -> Self {
Button {
state,
label: String::from(label),
class: Class::Primary,
on_press: None,
style: Style::default().min_width(100),
}
}
/// Sets the width of the [`Button`] in pixels.
///
/// [`Button`]: struct.Button.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
/// Makes the [`Button`] fill the horizontal space of its container.
///
/// [`Button`]: struct.Button.html
pub fn fill_width(mut self) -> Self {
self.style = self.style.fill_width();
self
}
/// Sets the alignment of the [`Button`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Button`]: struct.Button.html
pub fn align_self(mut self, align: Align) -> Self {
self.style = self.style.align_self(align);
self
}
/// Sets the [`Class`] of the [`Button`].
///
///
/// [`Button`]: struct.Button.html
/// [`Class`]: enum.Class.html
pub fn class(mut self, class: Class) -> Self {
self.class = class;
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// [`Button`]: struct.Button.html
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message>
where
Renderer: self::Renderer,
Message: Copy + std::fmt::Debug,
{
fn node(&self, _renderer: &Renderer) -> Node {
Node::new(self.style.height(50))
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state,
}) => {
if let Some(on_press) = self.on_press {
let bounds = layout.bounds();
match state {
ButtonState::Pressed => {
self.state.is_pressed =
bounds.contains(cursor_position);
}
ButtonState::Released => {
let is_clicked = self.state.is_pressed
&& bounds.contains(cursor_position);
self.state.is_pressed = false;
if is_clicked {
messages.push(on_press);
}
}
}
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(
cursor_position,
layout.bounds(),
self.state,
&self.label,
self.class,
)
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
}
}
/// The local state of a [`Button`].
///
/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_pressed: bool,
}
impl State {
/// Creates a new [`State`].
///
/// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
/// Returns whether the associated [`Button`] is currently being pressed or
/// not.
///
/// [`Button`]: struct.Button.html
pub fn is_pressed(&self) -> bool {
self.is_pressed
}
}
/// The type of a [`Button`].
///
/// ![Different buttons drawn by the built-in renderer in Coffee](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/button_classes.png?raw=true)
///
/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
/// The [`Button`] performs the main action.
///
/// [`Button`]: struct.Button.html
Primary,
/// The [`Button`] performs an alternative action.
///
/// [`Button`]: struct.Button.html
Secondary,
/// The [`Button`] performs a productive action.
///
/// [`Button`]: struct.Button.html
Positive,
}
/// The renderer of a [`Button`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Button`] in your user interface.
///
/// [`Button`]: struct.Button.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Draws a [`Button`].
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Button`]
/// * the local state of the [`Button`]
/// * the label of the [`Button`]
/// * the [`Class`] of the [`Button`]
///
/// [`Button`]: struct.Button.html
/// [`State`]: struct.State.html
/// [`Class`]: enum.Class.html
fn draw(
&mut self,
cursor_position: Point,
bounds: Rectangle,
state: &State,
label: &str,
class: Class,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Button<'a, Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static + Copy + std::fmt::Debug,
{
fn from(button: Button<'a, Message>) -> Element<'a, Message, Renderer> {
Element::new(button)
}
}

View file

@ -1,203 +0,0 @@
//! Show toggle controls using checkboxes.
use std::hash::Hash;
use crate::input::{mouse, ButtonState};
use crate::widget::{text, Column, Row, Text};
use crate::{
Align, Element, Event, Hasher, Layout, MouseCursor, Node, Point, Rectangle,
Widget,
};
/// A box that can be checked, with a generic text `Color`.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`checkbox::Renderer`] trait.
///
/// [`Widget`]: ../trait.Widget.html
/// [`checkbox::Renderer`]: trait.Renderer.html
///
/// # Example
///
/// ```
/// use iced::Checkbox;
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Color {
/// Black,
/// }
///
/// pub enum Message {
/// CheckboxToggled(bool),
/// }
///
/// let is_checked = true;
///
/// Checkbox::new(is_checked, "Toggle me!", Message::CheckboxToggled)
/// .label_color(Color::Black);
/// ```
///
/// ![Checkbox drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/checkbox.png?raw=true)
pub struct Checkbox<Color, Message> {
is_checked: bool,
on_toggle: Box<dyn Fn(bool) -> Message>,
label: String,
label_color: Option<Color>,
}
impl<Color, Message> std::fmt::Debug for Checkbox<Color, Message>
where
Color: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Checkbox")
.field("is_checked", &self.is_checked)
.field("label", &self.label)
.field("label_color", &self.label_color)
.finish()
}
}
impl<Color, Message> Checkbox<Color, Message> {
/// Creates a new [`Checkbox`].
///
/// It expects:
/// * a boolean describing whether the [`Checkbox`] is checked or not
/// * the label of the [`Checkbox`]
/// * a function that will be called when the [`Checkbox`] is toggled.
/// It will receive the new state of the [`Checkbox`] and must produce
/// a `Message`.
///
/// [`Checkbox`]: struct.Checkbox.html
pub fn new<F>(is_checked: bool, label: &str, f: F) -> Self
where
F: 'static + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Box::new(f),
label: String::from(label),
label_color: None,
}
}
/// Sets the `Color` of the label of the [`Checkbox`].
///
/// [`Checkbox`]: struct.Checkbox.html
pub fn label_color(mut self, color: Color) -> Self {
self.label_color = Some(color);
self
}
}
impl<Color, Message, Renderer> Widget<Message, Renderer>
for Checkbox<Color, Message>
where
Color: 'static + Copy + std::fmt::Debug,
Renderer: self::Renderer + text::Renderer<Color>,
{
fn node(&self, renderer: &Renderer) -> Node {
Row::<(), Renderer>::new()
.spacing(15)
.align_items(Align::Center)
.push(Column::new().width(28).height(28))
.push(Text::new(&self.label))
.node(renderer)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state: ButtonState::Pressed,
}) => {
let mouse_over = layout
.children()
.any(|child| child.bounds().contains(cursor_position));
if mouse_over {
messages.push((self.on_toggle)(!self.is_checked));
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let children: Vec<_> = layout.children().collect();
let text_bounds = children[1].bounds();
text::Renderer::draw(
renderer,
text_bounds,
&self.label,
None,
self.label_color,
text::HorizontalAlignment::Left,
text::VerticalAlignment::Top,
);
self::Renderer::draw(
renderer,
cursor_position,
children[0].bounds(),
text_bounds,
self.is_checked,
)
}
fn hash_layout(&self, state: &mut Hasher) {
self.label.hash(state);
}
}
/// The renderer of a [`Checkbox`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Checkbox`] in your user interface.
///
/// [`Checkbox`]: struct.Checkbox.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Draws a [`Checkbox`].
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Checkbox`]
/// * the bounds of the label of the [`Checkbox`]
/// * whether the [`Checkbox`] is checked or not
///
/// [`Checkbox`]: struct.Checkbox.html
fn draw(
&mut self,
cursor_position: Point,
bounds: Rectangle,
label_bounds: Rectangle,
is_checked: bool,
) -> MouseCursor;
}
impl<'a, Color, Message, Renderer> From<Checkbox<Color, Message>>
for Element<'a, Message, Renderer>
where
Color: 'static + Copy + std::fmt::Debug,
Renderer: self::Renderer + text::Renderer<Color>,
Message: 'static,
{
fn from(
checkbox: Checkbox<Color, Message>,
) -> Element<'a, Message, Renderer> {
Element::new(checkbox)
}
}

View file

@ -1,224 +0,0 @@
use std::hash::Hash;
use crate::{
Align, Element, Event, Hasher, Justify, Layout, MouseCursor, Node, Point,
Style, Widget,
};
/// A container that distributes its contents vertically.
///
/// A [`Column`] will try to fill the horizontal space of its container.
///
/// [`Column`]: struct.Column.html
#[derive(Default)]
pub struct Column<'a, Message, Renderer> {
style: Style,
spacing: u16,
children: Vec<Element<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> std::fmt::Debug for Column<'a, Message, Renderer> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Column")
.field("style", &self.style)
.field("spacing", &self.spacing)
.field("children", &self.children)
.finish()
}
}
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
/// Creates an empty [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn new() -> Self {
let mut style = Style::default().fill_width();
style.0.flex_direction = stretch::style::FlexDirection::Column;
Column {
style,
spacing: 0,
children: Vec::new(),
}
}
/// Sets the vertical spacing _between_ elements in pixels.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, px: u16) -> Self {
self.spacing = px;
self
}
/// Sets the padding of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn padding(mut self, px: u16) -> Self {
self.style = self.style.padding(px);
self
}
/// Sets the width of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
/// Sets the height of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn height(mut self, height: u16) -> Self {
self.style = self.style.height(height);
self
}
/// Sets the maximum width of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: u16) -> Self {
self.style = self.style.max_width(max_width);
self
}
/// Sets the maximum height of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: u16) -> Self {
self.style = self.style.max_height(max_height);
self
}
/// Sets the alignment of the [`Column`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Column`]: struct.Column.html
pub fn align_self(mut self, align: Align) -> Self {
self.style = self.style.align_self(align);
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn align_items(mut self, align: Align) -> Self {
self.style = self.style.align_items(align);
self
}
/// Sets the vertical distribution strategy for the contents of the
/// [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn justify_content(mut self, justify: Justify) -> Self {
self.style = self.style.justify_content(justify);
self
}
/// Adds an [`Element`] to the [`Column`].
///
/// [`Element`]: ../struct.Element.html
/// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Column<'a, Message, Renderer>
where
E: Into<Element<'a, Message, Renderer>>,
{
self.children.push(child.into());
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Column<'a, Message, Renderer>
{
fn node(&self, renderer: &Renderer) -> Node {
let mut children: Vec<Node> = self
.children
.iter()
.map(|child| {
let mut node = child.widget.node(renderer);
let mut style = node.0.style();
style.margin.bottom =
stretch::style::Dimension::Points(f32::from(self.spacing));
node.0.set_style(style);
node
})
.collect();
if let Some(node) = children.last_mut() {
let mut style = node.0.style();
style.margin.bottom = stretch::style::Dimension::Undefined;
node.0.set_style(style);
}
Node::with_children(self.style, children)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
},
);
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let mut cursor = MouseCursor::OutOfBounds;
self.children.iter().zip(layout.children()).for_each(
|(child, layout)| {
let new_cursor =
child.widget.draw(renderer, layout, cursor_position);
if new_cursor != MouseCursor::OutOfBounds {
cursor = new_cursor;
}
},
);
cursor
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
self.spacing.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
}
impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a,
Message: 'static,
{
fn from(
column: Column<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(column)
}
}

View file

@ -1,178 +0,0 @@
//! Display images in your user interface.
use crate::{
Align, Element, Hasher, Layout, MouseCursor, Node, Point, Rectangle, Style,
Widget,
};
use std::hash::Hash;
/// A frame that displays an image while keeping aspect ratio.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`image::Renderer`] trait.
///
/// [`Widget`]: ../../core/trait.Widget.html
/// [`image::Renderer`]: trait.Renderer.html
///
/// # Example
///
/// ```
/// use iced::Image;
///
/// # let my_handle = String::from("some_handle");
/// let image = Image::new(my_handle);
/// ```
pub struct Image<I> {
image: I,
source: Option<Rectangle<u16>>,
width: Option<u16>,
height: Option<u16>,
style: Style,
}
impl<I> std::fmt::Debug for Image<I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Image")
.field("source", &self.source)
.field("width", &self.width)
.field("height", &self.height)
.field("style", &self.style)
.finish()
}
}
impl<I> Image<I> {
/// Creates a new [`Image`] with given image handle.
///
/// [`Image`]: struct.Image.html
pub fn new(image: I) -> Self {
Image {
image,
source: None,
width: None,
height: None,
style: Style::default(),
}
}
/// Sets the portion of the [`Image`] to draw.
///
/// [`Image`]: struct.Image.html
pub fn clip(mut self, source: Rectangle<u16>) -> Self {
self.source = Some(source);
self
}
/// Sets the width of the [`Image`] boundaries in pixels.
///
/// [`Image`]: struct.Image.html
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
/// Sets the height of the [`Image`] boundaries in pixels.
///
/// [`Image`]: struct.Image.html
pub fn height(mut self, height: u16) -> Self {
self.height = Some(height);
self
}
/// Sets the alignment of the [`Image`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Image`]: struct.Image.html
pub fn align_self(mut self, align: Align) -> Self {
self.style = self.style.align_self(align);
self
}
}
impl<I, Message, Renderer> Widget<Message, Renderer> for Image<I>
where
Renderer: self::Renderer<I>,
I: Clone,
{
fn node(&self, renderer: &Renderer) -> Node {
renderer.node(
self.style,
&self.image,
self.width,
self.height,
self.source,
)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> MouseCursor {
renderer.draw(&self.image, layout.bounds(), self.source);
MouseCursor::OutOfBounds
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
/// The renderer of an [`Image`].
///
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Image`] in your user interface.
///
/// [`Image`]: struct.Image.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer<I> {
/// Creates a [`Node`] with the given [`Style`] for the provided [`Image`]
/// and its size.
///
/// You should probably keep the original aspect ratio, if possible.
///
/// [`Node`]: ../../struct.Node.html
/// [`Style`]: ../../struct.Style.html
/// [`Image`]: struct.Image.html
fn node(
&self,
style: Style,
image: &I,
width: Option<u16>,
height: Option<u16>,
source: Option<Rectangle<u16>>,
) -> Node;
/// Draws an [`Image`].
///
/// It receives:
/// * the bounds of the [`Image`]
/// * the handle of the loaded [`Image`]
/// * the portion of the image to draw. If not specified, the entire image
/// should be drawn.
///
/// [`Image`]: struct.Image.html
fn draw(
&mut self,
image: &I,
bounds: Rectangle<f32>,
source: Option<Rectangle<u16>>,
);
}
impl<'a, I, Message, Renderer> From<Image<I>> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer<I>,
I: Clone + 'a,
{
fn from(image: Image<I>) -> Element<'a, Message, Renderer> {
Element::new(image)
}
}

View file

@ -1,94 +0,0 @@
use std::hash::Hash;
use crate::graphics::{Point, Rectangle};
use crate::ui::core::{
Event, Hasher, Layout, MouseCursor, Node, Style, Widget,
};
pub struct Panel<'a, Message, Renderer> {
style: Style,
content: Box<Widget<Message, Renderer> + 'a>,
}
impl<'a, Message, Renderer> Panel<'a, Message, Renderer> {
pub fn new(content: impl Widget<Message, Renderer> + 'a) -> Self {
Panel {
style: Style::default().padding(20),
content: Box::new(content),
}
}
pub fn width(mut self, width: u32) -> Self {
self.style = self.style.width(width);
self
}
pub fn max_width(mut self, max_width: u32) -> Self {
self.style = self.style.max_width(max_width);
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Panel<'a, Message, Renderer>
where
Renderer: self::Renderer,
{
fn node(&self, renderer: &Renderer) -> Node {
Node::with_children(self.style, vec![self.content.node(renderer)])
}
fn on_event(
&mut self,
event: Event,
layout: Layout,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
[&mut self.content]
.iter_mut()
.zip(layout.children())
.for_each(|(child, layout)| {
child.on_event(event, layout, cursor_position, messages)
});
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout,
cursor_position: Point,
) -> MouseCursor {
let bounds = layout.bounds();
let mut cursor = MouseCursor::OutOfBounds;
renderer.draw(bounds);
[&self.content].iter().zip(layout.children()).for_each(
|(child, layout)| {
let new_cursor = child.draw(renderer, layout, cursor_position);
if new_cursor != MouseCursor::OutOfBounds {
cursor = new_cursor;
}
},
);
if cursor == MouseCursor::OutOfBounds {
if bounds.contains(cursor_position) {
MouseCursor::Idle
} else {
MouseCursor::OutOfBounds
}
} else {
cursor
}
}
fn hash(&self, state: &mut Hasher) {
self.style.hash(state);
}
}
pub trait Renderer {
fn draw(&mut self, bounds: Rectangle);
}

View file

@ -1,106 +0,0 @@
//! Provide visual feedback to your users when performing a slow task.
use crate::{
Element, Hasher, Layout, MouseCursor, Node, Point, Rectangle, Style, Widget,
};
use std::hash::Hash;
/// A bar that is filled based on an amount of progress.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`progress_bar::Renderer`] trait.
///
/// [`Widget`]: ../trait.Widget.html
/// [`progress_bar::Renderer`]: trait.Renderer.html
///
/// # Example
///
/// ```
/// use iced::ProgressBar;
///
/// let progress = 0.75;
///
/// ProgressBar::new(progress);
/// ```
#[derive(Debug)]
pub struct ProgressBar {
progress: f32,
style: Style,
}
impl ProgressBar {
/// Creates a new [`ProgressBar`] filled based on the given amount of
/// progress.
///
/// The progress should be in the `0.0..=1.0` range. `0` meaning no work
/// done, and `1` meaning work finished.
///
/// [`ProgressBar`]: struct.ProgressBar.html
pub fn new(progress: f32) -> Self {
ProgressBar {
progress,
style: Style::default().fill_width(),
}
}
/// Sets the width of the [`ProgressBar`] in pixels.
///
/// [`ProgressBar`]: struct.ProgressBar.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
}
impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar
where
Renderer: self::Renderer,
{
fn node(&self, _renderer: &Renderer) -> Node {
Node::new(self.style.height(50))
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> MouseCursor {
renderer.draw(layout.bounds(), self.progress);
MouseCursor::OutOfBounds
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
}
}
/// The renderer of a [`ProgressBar`].
///
/// Your [renderer] will need to implement this trait before being able to use
/// a [`ProgressBar`] in your user interface.
///
/// [`ProgressBar`]: struct.ProgressBar.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Draws a [`ProgressBar`].
///
/// It receives:
/// * the bounds of the [`ProgressBar`]
/// * the current progress of the [`ProgressBar`], in the `0.0..=1.0`
/// range.
///
/// [`ProgressBar`]: struct.ProgressBar.html
fn draw(&mut self, bounds: Rectangle, progress: f32);
}
impl<'a, Message, Renderer> From<ProgressBar> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
{
fn from(progress_bar: ProgressBar) -> Element<'a, Message, Renderer> {
Element::new(progress_bar)
}
}

View file

@ -1,211 +0,0 @@
//! Create choices using radio buttons.
use crate::input::{mouse, ButtonState};
use crate::widget::{text, Column, Row, Text};
use crate::{
Align, Element, Event, Hasher, Layout, MouseCursor, Node, Point, Rectangle,
Widget,
};
use std::hash::Hash;
/// A circular button representing a choice, with a generic text `Color`.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`radio::Renderer`] trait.
///
/// [`Widget`]: ../trait.Widget.html
/// [`radio::Renderer`]: trait.Renderer.html
///
/// # Example
/// ```
/// use iced::{Column, Radio};
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Color {
/// Black,
/// }
///
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice {
/// A,
/// B,
/// }
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Message {
/// RadioSelected(Choice),
/// }
///
/// let selected_choice = Some(Choice::A);
///
/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected)
/// .label_color(Color::Black);
///
/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected)
/// .label_color(Color::Black);
/// ```
///
/// ![Radio buttons drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/radio.png?raw=true)
pub struct Radio<Color, Message> {
is_selected: bool,
on_click: Message,
label: String,
label_color: Option<Color>,
}
impl<Color, Message> std::fmt::Debug for Radio<Color, Message>
where
Color: std::fmt::Debug,
Message: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Radio")
.field("is_selected", &self.is_selected)
.field("on_click", &self.on_click)
.field("label", &self.label)
.field("label_color", &self.label_color)
.finish()
}
}
impl<Color, Message> Radio<Color, Message> {
/// Creates a new [`Radio`] button.
///
/// It expects:
/// * the value related to the [`Radio`] button
/// * the label of the [`Radio`] button
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
///
/// [`Radio`]: struct.Radio.html
pub fn new<F, V>(value: V, label: &str, selected: Option<V>, f: F) -> Self
where
V: Eq + Copy,
F: 'static + Fn(V) -> Message,
{
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
label: String::from(label),
label_color: None,
}
}
/// Sets the `Color` of the label of the [`Radio`].
///
/// [`Radio`]: struct.Radio.html
pub fn label_color(mut self, color: Color) -> Self {
self.label_color = Some(color);
self
}
}
impl<Color, Message, Renderer> Widget<Message, Renderer>
for Radio<Color, Message>
where
Color: 'static + Copy + std::fmt::Debug,
Renderer: self::Renderer + text::Renderer<Color>,
Message: Copy + std::fmt::Debug,
{
fn node(&self, renderer: &Renderer) -> Node {
Row::<(), Renderer>::new()
.spacing(15)
.align_items(Align::Center)
.push(Column::new().width(28).height(28))
.push(Text::new(&self.label))
.node(renderer)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state: ButtonState::Pressed,
}) => {
if layout.bounds().contains(cursor_position) {
messages.push(self.on_click);
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let children: Vec<_> = layout.children().collect();
let mut text_bounds = children[1].bounds();
text_bounds.y -= 2.0;
text::Renderer::draw(
renderer,
text_bounds,
&self.label,
None,
self.label_color,
text::HorizontalAlignment::Left,
text::VerticalAlignment::Top,
);
self::Renderer::draw(
renderer,
cursor_position,
children[0].bounds(),
layout.bounds(),
self.is_selected,
)
}
fn hash_layout(&self, state: &mut Hasher) {
self.label.hash(state);
}
}
/// The renderer of a [`Radio`] button.
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Radio`] button in your user interface.
///
/// [`Radio`]: struct.Radio.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Draws a [`Radio`] button.
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Radio`]
/// * the bounds of the label of the [`Radio`]
/// * whether the [`Radio`] is selected or not
///
/// [`Radio`]: struct.Radio.html
fn draw(
&mut self,
cursor_position: Point,
bounds: Rectangle,
label_bounds: Rectangle,
is_selected: bool,
) -> MouseCursor;
}
impl<'a, Color, Message, Renderer> From<Radio<Color, Message>>
for Element<'a, Message, Renderer>
where
Color: 'static + Copy + std::fmt::Debug,
Renderer: self::Renderer + text::Renderer<Color>,
Message: 'static + Copy + std::fmt::Debug,
{
fn from(checkbox: Radio<Color, Message>) -> Element<'a, Message, Renderer> {
Element::new(checkbox)
}
}

View file

@ -1,219 +0,0 @@
use std::hash::Hash;
use crate::{
Align, Element, Event, Hasher, Justify, Layout, MouseCursor, Node, Point,
Style, Widget,
};
/// A container that distributes its contents horizontally.
///
/// A [`Row`] will try to fill the horizontal space of its container.
///
/// [`Row`]: struct.Row.html
#[derive(Default)]
pub struct Row<'a, Message, Renderer> {
style: Style,
spacing: u16,
children: Vec<Element<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> std::fmt::Debug for Row<'a, Message, Renderer> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Row")
.field("style", &self.style)
.field("spacing", &self.spacing)
.field("children", &self.children)
.finish()
}
}
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
/// Creates an empty [`Row`].
///
/// [`Row`]: struct.Row.html
pub fn new() -> Self {
Row {
style: Style::default().fill_width(),
spacing: 0,
children: Vec::new(),
}
}
/// Sets the horizontal spacing _between_ elements in pixels.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, px: u16) -> Self {
self.spacing = px;
self
}
/// Sets the padding of the [`Row`] in pixels.
///
/// [`Row`]: struct.Row.html
pub fn padding(mut self, px: u16) -> Self {
self.style = self.style.padding(px);
self
}
/// Sets the width of the [`Row`] in pixels.
///
/// [`Row`]: struct.Row.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
/// Sets the height of the [`Row`] in pixels.
///
/// [`Row`]: struct.Row.html
pub fn height(mut self, height: u16) -> Self {
self.style = self.style.height(height);
self
}
/// Sets the maximum width of the [`Row`] in pixels.
///
/// [`Row`]: struct.Row.html
pub fn max_width(mut self, max_width: u16) -> Self {
self.style = self.style.max_width(max_width);
self
}
/// Sets the maximum height of the [`Row`] in pixels.
///
/// [`Row`]: struct.Row.html
pub fn max_height(mut self, max_height: u16) -> Self {
self.style = self.style.max_height(max_height);
self
}
/// Sets the alignment of the [`Row`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Row`]: struct.Row.html
pub fn align_self(mut self, align: Align) -> Self {
self.style = self.style.align_self(align);
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
///
/// [`Row`]: struct.Row.html
pub fn align_items(mut self, align: Align) -> Self {
self.style = self.style.align_items(align);
self
}
/// Sets the horizontal distribution strategy for the contents of the
/// [`Row`] .
///
/// [`Row`]: struct.Row.html
pub fn justify_content(mut self, justify: Justify) -> Self {
self.style = self.style.justify_content(justify);
self
}
/// Adds an [`Element`] to the [`Row`].
///
/// [`Element`]: ../struct.Element.html
/// [`Row`]: struct.Row.html
pub fn push<E>(mut self, child: E) -> Row<'a, Message, Renderer>
where
E: Into<Element<'a, Message, Renderer>>,
{
self.children.push(child.into());
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Row<'a, Message, Renderer>
{
fn node(&self, renderer: &Renderer) -> Node {
let mut children: Vec<Node> = self
.children
.iter()
.map(|child| {
let mut node = child.widget.node(renderer);
let mut style = node.0.style();
style.margin.end =
stretch::style::Dimension::Points(f32::from(self.spacing));
node.0.set_style(style);
node
})
.collect();
if let Some(node) = children.last_mut() {
let mut style = node.0.style();
style.margin.end = stretch::style::Dimension::Undefined;
node.0.set_style(style);
}
Node::with_children(self.style, children)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| {
child
.widget
.on_event(event, layout, cursor_position, messages)
},
);
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
let mut cursor = MouseCursor::OutOfBounds;
self.children.iter().zip(layout.children()).for_each(
|(child, layout)| {
let new_cursor =
child.widget.draw(renderer, layout, cursor_position);
if new_cursor != MouseCursor::OutOfBounds {
cursor = new_cursor;
}
},
);
cursor
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
self.spacing.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
}
impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a,
Message: 'static,
{
fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(row)
}
}

View file

@ -1,241 +0,0 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
//!
//! [`Slider`]: struct.Slider.html
//! [`State`]: struct.State.html
use std::hash::Hash;
use std::ops::RangeInclusive;
use crate::input::{mouse, ButtonState};
use crate::{
Element, Event, Hasher, Layout, MouseCursor, Node, Point, Rectangle, Style,
Widget,
};
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`slider::Renderer`] trait.
///
/// [`Slider`]: struct.Slider.html
/// [`Widget`]: ../trait.Widget.html
/// [`slider::Renderer`]: trait.Renderer.html
///
/// # Example
/// ```
/// use iced::{slider, Slider};
///
/// pub enum Message {
/// SliderChanged(f32),
/// }
///
/// let state = &mut slider::State::new();
/// let value = 50.0;
///
/// Slider::new(state, 0.0..=100.0, value, Message::SliderChanged);
/// ```
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
pub struct Slider<'a, Message> {
state: &'a mut State,
range: RangeInclusive<f32>,
value: f32,
on_change: Box<dyn Fn(f32) -> Message>,
style: Style,
}
impl<'a, Message> std::fmt::Debug for Slider<'a, Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Slider")
.field("state", &self.state)
.field("range", &self.range)
.field("value", &self.value)
.field("style", &self.style)
.finish()
}
}
impl<'a, Message> Slider<'a, Message> {
/// Creates a new [`Slider`].
///
/// It expects:
/// * the local [`State`] of the [`Slider`]
/// * an inclusive range of possible values
/// * the current value of the [`Slider`]
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
///
/// [`Slider`]: struct.Slider.html
/// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
range: RangeInclusive<f32>,
value: f32,
on_change: F,
) -> Self
where
F: 'static + Fn(f32) -> Message,
{
Slider {
state,
value: value.max(*range.start()).min(*range.end()),
range,
on_change: Box::new(on_change),
style: Style::default().min_width(100).fill_width(),
}
}
/// Sets the width of the [`Slider`] in pixels.
///
/// [`Slider`]: struct.Slider.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for Slider<'a, Message>
where
Renderer: self::Renderer,
{
fn node(&self, _renderer: &Renderer) -> Node {
Node::new(self.style.height(25))
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
let mut change = || {
let bounds = layout.bounds();
if cursor_position.x <= bounds.x {
messages.push((self.on_change)(*self.range.start()));
} else if cursor_position.x >= bounds.x + bounds.width {
messages.push((self.on_change)(*self.range.end()));
} else {
let percent = (cursor_position.x - bounds.x) / bounds.width;
let value = (self.range.end() - self.range.start()) * percent
+ self.range.start();
messages.push((self.on_change)(value));
}
};
match event {
Event::Mouse(mouse::Event::Input {
button: mouse::Button::Left,
state,
}) => match state {
ButtonState::Pressed => {
if layout.bounds().contains(cursor_position) {
change();
self.state.is_dragging = true;
}
}
ButtonState::Released => {
self.state.is_dragging = false;
}
},
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if self.state.is_dragging {
change();
}
}
_ => {}
}
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> MouseCursor {
renderer.draw(
cursor_position,
layout.bounds(),
self.state,
self.range.clone(),
self.value,
)
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
}
}
/// The local state of a [`Slider`].
///
/// [`Slider`]: struct.Slider.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_dragging: bool,
}
impl State {
/// Creates a new [`State`].
///
/// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
/// Returns whether the associated [`Slider`] is currently being dragged or
/// not.
///
/// [`Slider`]: struct.Slider.html
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
}
/// The renderer of a [`Slider`].
///
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Slider`] in your user interface.
///
/// [`Slider`]: struct.Slider.html
/// [renderer]: ../../renderer/index.html
pub trait Renderer {
/// Draws a [`Slider`].
///
/// It receives:
/// * the current cursor position
/// * the bounds of the [`Slider`]
/// * the local state of the [`Slider`]
/// * the range of values of the [`Slider`]
/// * the current value of the [`Slider`]
///
/// [`Slider`]: struct.Slider.html
/// [`State`]: struct.State.html
/// [`Class`]: enum.Class.html
fn draw(
&mut self,
cursor_position: Point,
bounds: Rectangle,
state: &State,
range: RangeInclusive<f32>,
value: f32,
) -> MouseCursor;
}
impl<'a, Message, Renderer> From<Slider<'a, Message>>
for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
Message: 'static,
{
fn from(slider: Slider<'a, Message>) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
}

View file

@ -1,224 +0,0 @@
//! Write some text for your users to read.
use crate::{
Element, Hasher, Layout, MouseCursor, Node, Point, Rectangle, Style, Widget,
};
use std::hash::Hash;
/// A fragment of text with a generic `Color`.
///
/// It implements [`Widget`] when the associated `Renderer` implements the
/// [`text::Renderer`] trait.
///
/// [`Widget`]: ../trait.Widget.html
/// [`text::Renderer`]: trait.Renderer.html
///
/// # Example
///
/// ```
/// use iced::Text;
///
/// #[derive(Debug, Clone, Copy)]
/// pub enum Color {
/// Black,
/// }
///
/// Text::new("I <3 iced!")
/// .size(40)
/// .color(Color::Black);
/// ```
#[derive(Debug, Clone)]
pub struct Text<Color> {
content: String,
size: Option<u16>,
color: Option<Color>,
style: Style,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
}
impl<Color> Text<Color> {
/// Create a new fragment of [`Text`] with the given contents.
///
/// [`Text`]: struct.Text.html
pub fn new(label: &str) -> Self {
Text {
content: String::from(label),
size: None,
color: None,
style: Style::default().fill_width(),
horizontal_alignment: HorizontalAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
/// Sets the size of the [`Text`] in pixels.
///
/// [`Text`]: struct.Text.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
/// Sets the `Color` of the [`Text`].
///
/// [`Text`]: struct.Text.html
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
/// Sets the width of the [`Text`] boundaries in pixels.
///
/// [`Text`]: struct.Text.html
pub fn width(mut self, width: u16) -> Self {
self.style = self.style.width(width);
self
}
/// Sets the height of the [`Text`] boundaries in pixels.
///
/// [`Text`]: struct.Text.html
pub fn height(mut self, height: u16) -> Self {
self.style = self.style.height(height);
self
}
/// Sets the [`HorizontalAlignment`] of the [`Text`].
///
/// [`Text`]: struct.Text.html
/// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
pub fn horizontal_alignment(
mut self,
alignment: HorizontalAlignment,
) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the [`VerticalAlignment`] of the [`Text`].
///
/// [`Text`]: struct.Text.html
/// [`VerticalAlignment`]: enum.VerticalAlignment.html
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
}
}
impl<Message, Renderer, Color> Widget<Message, Renderer> for Text<Color>
where
Color: Copy + std::fmt::Debug,
Renderer: self::Renderer<Color>,
{
fn node(&self, renderer: &Renderer) -> Node {
renderer.node(self.style, &self.content, self.size)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
_cursor_position: Point,
) -> MouseCursor {
renderer.draw(
layout.bounds(),
&self.content,
self.size,
self.color,
self.horizontal_alignment,
self.vertical_alignment,
);
MouseCursor::OutOfBounds
}
fn hash_layout(&self, state: &mut Hasher) {
self.style.hash(state);
self.content.hash(state);
self.size.hash(state);
}
}
/// The renderer of a [`Text`] fragment with a generic `Color`.
///
/// Your [renderer] will need to implement this trait before being
/// able to use [`Text`] in your [`UserInterface`].
///
/// [`Text`]: struct.Text.html
/// [renderer]: ../../renderer/index.html
/// [`UserInterface`]: ../../struct.UserInterface.html
pub trait Renderer<Color> {
/// Creates a [`Node`] with the given [`Style`] for the provided [`Text`]
/// contents and size.
///
/// You should probably use [`Node::with_measure`] to allow [`Text`] to
/// adapt to the dimensions of its container.
///
/// [`Node`]: ../../struct.Node.html
/// [`Style`]: ../../struct.Style.html
/// [`Text`]: struct.Text.html
/// [`Node::with_measure`]: ../../struct.Node.html#method.with_measure
fn node(&self, style: Style, content: &str, size: Option<u16>) -> Node;
/// Draws a [`Text`] fragment.
///
/// It receives:
/// * the bounds of the [`Text`]
/// * the contents of the [`Text`]
/// * the size of the [`Text`]
/// * the color of the [`Text`]
/// * the [`HorizontalAlignment`] of the [`Text`]
/// * the [`VerticalAlignment`] of the [`Text`]
///
/// [`Text`]: struct.Text.html
/// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
/// [`VerticalAlignment`]: enum.VerticalAlignment.html
fn draw(
&mut self,
bounds: Rectangle,
content: &str,
size: Option<u16>,
color: Option<Color>,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
);
}
impl<'a, Message, Renderer, Color> From<Text<Color>>
for Element<'a, Message, Renderer>
where
Color: 'static + Copy + std::fmt::Debug,
Renderer: self::Renderer<Color>,
{
fn from(text: Text<Color>) -> Element<'a, Message, Renderer> {
Element::new(text)
}
}
/// The horizontal alignment of some resource.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizontalAlignment {
/// Align left
Left,
/// Horizontally centered
Center,
/// Align right
Right,
}
/// The vertical alignment of some resource.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerticalAlignment {
/// Align top
Top,
/// Vertically centered
Center,
/// Align bottom
Bottom,
}

33
web/Cargo.toml Normal file
View file

@ -0,0 +1,33 @@
[package]
name = "iced_web"
version = "0.1.0-alpha"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A web backend for Iced"
license = "MIT"
repository = "https://github.com/hecrj/iced"
documentation = "https://docs.rs/iced_web"
readme = "README.md"
keywords = ["gui", "ui", "web", "interface", "widgets"]
categories = ["web-programming"]
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
iced_core = { version = "0.1.0-alpha", path = "../core" }
dodrio = "0.1.0"
futures-preview = "=0.3.0-alpha.18"
wasm-bindgen = "0.2.50"
[dependencies.web-sys]
version = "0.3.27"
features = [
"console",
"Document",
"HtmlElement",
"HtmlInputElement",
"Event",
"EventTarget",
"InputEvent",
]

40
web/src/bus.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::Application;
use std::rc::Rc;
#[derive(Clone)]
pub struct Bus<Message> {
publish: Rc<Box<dyn Fn(Message, &mut dyn dodrio::RootRender)>>,
}
impl<Message> Bus<Message>
where
Message: 'static,
{
pub fn new() -> Self {
Self {
publish: Rc::new(Box::new(|message, root| {
let app = root.unwrap_mut::<Application<Message>>();
app.update(message)
})),
}
}
pub fn publish(&self, message: Message, root: &mut dyn dodrio::RootRender) {
(self.publish)(message, root);
}
pub fn map<B>(&self, mapper: Rc<Box<dyn Fn(B) -> Message>>) -> Bus<B>
where
B: 'static,
{
let publish = self.publish.clone();
Bus {
publish: Rc::new(Box::new(move |message, root| {
publish(mapper(message), root)
})),
}
}
}

62
web/src/element.rs Normal file
View file

@ -0,0 +1,62 @@
use crate::{Bus, Color, Widget};
use dodrio::bumpalo;
use std::rc::Rc;
pub struct Element<'a, Message> {
pub(crate) widget: Box<dyn Widget<Message> + 'a>,
}
impl<'a, Message> Element<'a, Message> {
pub fn new(widget: impl Widget<Message> + 'a) -> Self {
Self {
widget: Box::new(widget),
}
}
pub fn explain(self, _color: Color) -> Element<'a, Message> {
self
}
pub fn map<F, B>(self, f: F) -> Element<'a, B>
where
Message: 'static,
B: 'static,
F: 'static + Fn(Message) -> B,
{
Element {
widget: Box::new(Map::new(self.widget, f)),
}
}
}
struct Map<'a, A, B> {
widget: Box<dyn Widget<A> + 'a>,
mapper: Rc<Box<dyn Fn(A) -> B>>,
}
impl<'a, A, B> Map<'a, A, B> {
pub fn new<F>(widget: Box<dyn Widget<A> + 'a>, mapper: F) -> Map<'a, A, B>
where
F: 'static + Fn(A) -> B,
{
Map {
widget,
mapper: Rc::new(Box::new(mapper)),
}
}
}
impl<'a, A, B> Widget<B> for Map<'a, A, B>
where
A: 'static,
B: 'static,
{
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<B>,
) -> dodrio::Node<'b> {
self.widget.node(bump, &bus.map(self.mapper.clone()))
}
}

74
web/src/lib.rs Normal file
View file

@ -0,0 +1,74 @@
use dodrio::bumpalo;
use futures::Future;
use std::cell::RefCell;
mod bus;
mod element;
pub mod widget;
pub use bus::Bus;
pub use element::Element;
pub use iced_core::{Align, Color, Justify, Length};
pub use widget::*;
pub trait UserInterface {
type Message;
fn update(
&mut self,
message: Self::Message,
) -> Option<Box<dyn Future<Output = Self::Message>>>;
fn view(&mut self) -> Element<Self::Message>;
fn run(self)
where
Self: 'static + Sized,
{
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let app = Application::new(self);
let vdom = dodrio::Vdom::new(&body, app);
vdom.forget();
}
}
struct Application<Message> {
ui: RefCell<Box<dyn UserInterface<Message = Message>>>,
}
impl<Message> Application<Message> {
fn new(ui: impl UserInterface<Message = Message> + 'static) -> Self {
Self {
ui: RefCell::new(Box::new(ui)),
}
}
fn update(&mut self, message: Message) {
let mut ui = self.ui.borrow_mut();
// TODO: Resolve futures and publish resulting messages
let _ = ui.update(message);
}
}
impl<Message> dodrio::Render for Application<Message>
where
Message: 'static,
{
fn render<'a, 'bump>(
&'a self,
bump: &'bump bumpalo::Bump,
) -> dodrio::Node<'bump>
where
'a: 'bump,
{
let mut ui = self.ui.borrow_mut();
let element = ui.view();
element.widget.node(bump, &Bus::new())
}
}

35
web/src/widget.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::Bus;
use dodrio::bumpalo;
pub mod button;
pub mod slider;
pub mod text;
mod checkbox;
mod column;
mod image;
mod radio;
mod row;
#[doc(no_inline)]
pub use button::Button;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
pub use checkbox::Checkbox;
pub use column::Column;
pub use image::Image;
pub use radio::Radio;
pub use row::Row;
pub trait Widget<Message> {
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
_bus: &Bus<Message>,
) -> dodrio::Node<'b>;
}

45
web/src/widget/button.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub use iced_core::button::*;
impl<'a, Message> Widget<Message> for Button<'a, Message>
where
Message: 'static + Copy,
{
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let label = bumpalo::format!(in bump, "{}", self.label);
let mut node = button(bump).children(vec![text(label.into_bump_str())]);
if let Some(on_press) = self.on_press {
let event_bus = bus.clone();
node = node.on("click", move |root, vdom, _event| {
event_bus.publish(on_press, root);
vdom.schedule_render();
});
}
// TODO: Complete styling
node.finish()
}
}
impl<'a, Message> From<Button<'a, Message>> for Element<'a, Message>
where
Message: 'static + Copy,
{
fn from(button: Button<'a, Message>) -> Element<'a, Message> {
Element::new(button)
}
}

View file

@ -0,0 +1,48 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub use iced_core::Checkbox;
impl<Message> Widget<Message> for Checkbox<Message>
where
Message: 'static + Copy,
{
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let checkbox_label = bumpalo::format!(in bump, "{}", self.label);
let event_bus = bus.clone();
let msg = (self.on_toggle)(!self.is_checked);
// TODO: Complete styling
label(bump)
.children(vec![
input(bump)
.attr("type", "checkbox")
.bool_attr("checked", self.is_checked)
.on("click", move |root, vdom, _event| {
event_bus.publish(msg, root);
vdom.schedule_render();
})
.finish(),
text(checkbox_label.into_bump_str()),
])
.finish()
}
}
impl<'a, Message> From<Checkbox<Message>> for Element<'a, Message>
where
Message: 'static + Copy,
{
fn from(checkbox: Checkbox<Message>) -> Element<'a, Message> {
Element::new(checkbox)
}
}

36
web/src/widget/column.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub type Column<'a, Message> = iced_core::Column<Element<'a, Message>>;
impl<'a, Message> Widget<Message> for Column<'a, Message> {
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
publish: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let children: Vec<_> = self
.children
.iter()
.map(|element| element.widget.node(bump, publish))
.collect();
// TODO: Complete styling
div(bump)
.attr("style", "display: flex; flex-direction: column")
.children(children)
.finish()
}
}
impl<'a, Message> From<Column<'a, Message>> for Element<'a, Message>
where
Message: 'static,
{
fn from(column: Column<'a, Message>) -> Element<'a, Message> {
Element::new(column)
}
}

42
web/src/widget/image.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::{Bus, Element, Length, Widget};
use dodrio::bumpalo;
pub type Image<'a> = iced_core::Image<&'a str>;
impl<'a, Message> Widget<Message> for Image<'a> {
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
_bus: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let src = bumpalo::format!(in bump, "{}", self.handle);
let mut image = img(bump).attr("src", src.into_bump_str());
match self.width {
Length::Shrink => {}
Length::Fill => {
image = image.attr("width", "100%");
}
Length::Units(px) => {
image = image.attr(
"width",
bumpalo::format!(in bump, "{}px", px).into_bump_str(),
);
}
}
// TODO: Complete styling
image.finish()
}
}
impl<'a, Message> From<Image<'a>> for Element<'a, Message> {
fn from(image: Image<'a>) -> Element<'a, Message> {
Element::new(image)
}
}

49
web/src/widget/radio.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub use iced_core::Radio;
impl<Message> Widget<Message> for Radio<Message>
where
Message: 'static + Copy,
{
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let radio_label = bumpalo::format!(in bump, "{}", self.label);
let event_bus = bus.clone();
let on_click = self.on_click;
// TODO: Complete styling
label(bump)
.attr("style", "display: block")
.children(vec![
input(bump)
.attr("type", "radio")
.bool_attr("checked", self.is_selected)
.on("click", move |root, vdom, _event| {
event_bus.publish(on_click, root);
vdom.schedule_render();
})
.finish(),
text(radio_label.into_bump_str()),
])
.finish()
}
}
impl<'a, Message> From<Radio<Message>> for Element<'a, Message>
where
Message: 'static + Copy,
{
fn from(radio: Radio<Message>) -> Element<'a, Message> {
Element::new(radio)
}
}

36
web/src/widget/row.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub type Row<'a, Message> = iced_core::Row<Element<'a, Message>>;
impl<'a, Message> Widget<Message> for Row<'a, Message> {
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
publish: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let children: Vec<_> = self
.children
.iter()
.map(|element| element.widget.node(bump, publish))
.collect();
// TODO: Complete styling
div(bump)
.attr("style", "display: flex; flex-direction: row")
.children(children)
.finish()
}
}
impl<'a, Message> From<Row<'a, Message>> for Element<'a, Message>
where
Message: 'static,
{
fn from(column: Row<'a, Message>) -> Element<'a, Message> {
Element::new(column)
}
}

62
web/src/widget/slider.rs Normal file
View file

@ -0,0 +1,62 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub use iced_core::slider::*;
impl<'a, Message> Widget<Message> for Slider<'a, Message>
where
Message: 'static + Copy,
{
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
use wasm_bindgen::JsCast;
let (start, end) = self.range.clone().into_inner();
let min = bumpalo::format!(in bump, "{}", start);
let max = bumpalo::format!(in bump, "{}", end);
let value = bumpalo::format!(in bump, "{}", self.value);
let on_change = self.on_change.clone();
let event_bus = bus.clone();
// TODO: Make `step` configurable
// TODO: Complete styling
label(bump)
.children(vec![input(bump)
.attr("type", "range")
.attr("step", "0.01")
.attr("min", min.into_bump_str())
.attr("max", max.into_bump_str())
.attr("value", value.into_bump_str())
.on("input", move |root, vdom, event| {
let slider = match event.target().and_then(|t| {
t.dyn_into::<web_sys::HtmlInputElement>().ok()
}) {
None => return,
Some(slider) => slider,
};
if let Ok(value) = slider.value().parse::<f32>() {
event_bus.publish(on_change(value), root);
vdom.schedule_render();
}
})
.finish()])
.finish()
}
}
impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message>
where
Message: 'static + Copy,
{
fn from(slider: Slider<'a, Message>) -> Element<'a, Message> {
Element::new(slider)
}
}

29
web/src/widget/text.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{Bus, Element, Widget};
use dodrio::bumpalo;
pub use iced_core::text::*;
impl<'a, Message> Widget<Message> for Text {
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
_publish: &Bus<Message>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
let content = bumpalo::format!(in bump, "{}", self.content);
let size = bumpalo::format!(in bump, "font-size: {}px", self.size.unwrap_or(20));
// TODO: Complete styling
p(bump)
.attr("style", size.into_bump_str())
.children(vec![text(content.into_bump_str())])
.finish()
}
}
impl<'a, Message> From<Text> for Element<'a, Message> {
fn from(text: Text) -> Element<'a, Message> {
Element::new(text)
}
}