Merge branch 'master' into feature/text-selection

This commit is contained in:
Héctor Ramón Jiménez 2020-03-24 19:08:21 +01:00
commit e77fa175aa
90 changed files with 3623 additions and 538 deletions

View file

@ -73,9 +73,11 @@ A bunch of simpler examples exist:
- [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time.
- [`counter`](counter), the classic counter example explained in the [`README`](../README.md).
- [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle.
- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
- [`events`](events), a log of native events displayed using a conditional `Subscription`.
- [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu).
- [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application.
- [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized.
- [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].
- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider.
- [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.

View file

@ -23,7 +23,6 @@ mod bezier {
basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions,
StrokeTessellator, VertexBuffers,
};
use std::sync::Arc;
pub struct Bezier<'a, Message> {
state: &'a mut State,
@ -175,10 +174,10 @@ mod bezier {
let mesh = Primitive::Mesh2D {
origin: Point::new(bounds.x, bounds.y),
buffers: Arc::new(Mesh2D {
buffers: Mesh2D {
vertices: buffer.vertices,
indices: buffer.indices,
}),
},
};
(

View file

@ -0,0 +1,12 @@
[package]
name = "download_progress"
version = "0.1.0"
authors = ["Songtronix <contact@songtronix.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../..", features = ["tokio"] }
iced_native = { path = "../../native" }
iced_futures = { path = "../../futures" }
reqwest = "0.10"

View file

@ -0,0 +1,17 @@
## Download progress
A basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress.
<div align="center">
<a href="https://gfycat.com/wildearlyafricanwilddog">
<img src="https://thumbs.gfycat.com/WildEarlyAfricanwilddog-small.gif">
</a>
</div>
You can run it with `cargo run`:
```
cargo run --package download_progress
```

View file

@ -0,0 +1,112 @@
use iced_futures::futures;
// Just a little utility function
pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> {
iced::Subscription::from_recipe(Download {
url: url.to_string(),
})
}
pub struct Download {
url: String,
}
// Make sure iced can use our download stream
impl<H, I> iced_native::subscription::Recipe<H, I> for Download
where
H: std::hash::Hasher,
{
type Output = Progress;
fn hash(&self, state: &mut H) {
use std::hash::Hash;
std::any::TypeId::of::<Self>().hash(state);
self.url.hash(state);
}
fn stream(
self: Box<Self>,
_input: futures::stream::BoxStream<'static, I>,
) -> futures::stream::BoxStream<'static, Self::Output> {
Box::pin(futures::stream::unfold(
State::Ready(self.url),
|state| async move {
match state {
State::Ready(url) => {
let response = reqwest::get(&url).await;
match response {
Ok(response) => {
if let Some(total) = response.content_length() {
Some((
Progress::Started,
State::Downloading {
response,
total,
downloaded: 0,
},
))
} else {
Some((Progress::Errored, State::Finished))
}
}
Err(_) => {
Some((Progress::Errored, State::Finished))
}
}
}
State::Downloading {
mut response,
total,
downloaded,
} => match response.chunk().await {
Ok(Some(chunk)) => {
let downloaded = downloaded + chunk.len() as u64;
let percentage =
(downloaded as f32 / total as f32) * 100.0;
Some((
Progress::Advanced(percentage),
State::Downloading {
response,
total,
downloaded,
},
))
}
Ok(None) => Some((Progress::Finished, State::Finished)),
Err(_) => Some((Progress::Errored, State::Finished)),
},
State::Finished => {
// We do not let the stream die, as it would start a
// new download repeatedly if the user is not careful
// in case of errors.
let _: () = iced::futures::future::pending().await;
None
}
}
},
))
}
}
#[derive(Debug, Clone)]
pub enum Progress {
Started,
Advanced(f32),
Finished,
Errored,
}
pub enum State {
Ready(String),
Downloading {
response: reqwest::Response,
total: u64,
downloaded: u64,
},
Finished,
}

View file

@ -0,0 +1,143 @@
use iced::{
button, executor, Align, Application, Button, Column, Command, Container,
Element, Length, ProgressBar, Settings, Subscription, Text,
};
mod download;
pub fn main() {
Example::run(Settings::default())
}
#[derive(Debug)]
enum Example {
Idle { button: button::State },
Downloading { progress: f32 },
Finished { button: button::State },
Errored { button: button::State },
}
#[derive(Debug, Clone)]
pub enum Message {
Download,
DownloadProgressed(download::Progress),
}
impl Application for Example {
type Executor = executor::Default;
type Message = Message;
fn new() -> (Example, Command<Message>) {
(
Example::Idle {
button: button::State::new(),
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Download progress - Iced")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Download => match self {
Example::Idle { .. }
| Example::Finished { .. }
| Example::Errored { .. } => {
*self = Example::Downloading { progress: 0.0 };
}
_ => {}
},
Message::DownloadProgressed(message) => match self {
Example::Downloading { progress } => match message {
download::Progress::Started => {
*progress = 0.0;
}
download::Progress::Advanced(percentage) => {
*progress = percentage;
}
download::Progress::Finished => {
*self = Example::Finished {
button: button::State::new(),
}
}
download::Progress::Errored => {
*self = Example::Errored {
button: button::State::new(),
};
}
},
_ => {}
},
};
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
match self {
Example::Downloading { .. } => {
download::file("https://speed.hetzner.de/100MB.bin")
.map(Message::DownloadProgressed)
}
_ => Subscription::none(),
}
}
fn view(&mut self) -> Element<Message> {
let current_progress = match self {
Example::Idle { .. } => 0.0,
Example::Downloading { progress } => *progress,
Example::Finished { .. } => 100.0,
Example::Errored { .. } => 0.0,
};
let progress_bar = ProgressBar::new(0.0..=100.0, current_progress);
let control: Element<_> = match self {
Example::Idle { button } => {
Button::new(button, Text::new("Start the download!"))
.on_press(Message::Download)
.into()
}
Example::Finished { button } => Column::new()
.spacing(10)
.align_items(Align::Center)
.push(Text::new("Download finished!"))
.push(
Button::new(button, Text::new("Start again"))
.on_press(Message::Download),
)
.into(),
Example::Downloading { .. } => {
Text::new(format!("Downloading... {:.2}%", current_progress))
.into()
}
Example::Errored { button } => Column::new()
.spacing(10)
.align_items(Align::Center)
.push(Text::new("Something went wrong :("))
.push(
Button::new(button, Text::new("Try again"))
.on_press(Message::Download),
)
.into(),
};
let content = Column::new()
.spacing(10)
.padding(10)
.align_items(Align::Center)
.push(progress_bar)
.push(control);
Container::new(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}

View file

@ -87,7 +87,7 @@ mod rainbow {
(
Primitive::Mesh2D {
origin: Point::new(b.x, b.y),
buffers: std::sync::Arc::new(Mesh2D {
buffers: Mesh2D {
vertices: vec![
Vertex2D {
position: posn_center,
@ -136,7 +136,7 @@ mod rainbow {
0, 7, 8, // BL
0, 8, 1, // L
],
}),
},
},
MouseCursor::OutOfBounds,
)

View file

@ -10,7 +10,7 @@ use iced_wgpu::{
use iced_winit::{winit, Cache, Clipboard, MouseCursor, Size, UserInterface};
use winit::{
event::{DeviceEvent, Event, ModifiersState, WindowEvent},
event::{Event, ModifiersState, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
@ -40,11 +40,12 @@ pub fn main() {
});
let surface = wgpu::Surface::create(&window);
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
let mut swap_chain = {
let size = window.inner_size();
SwapChain::new(&device, &surface, size.width, size.height)
SwapChain::new(&device, &surface, format, size.width, size.height)
};
let mut resized = false;
@ -65,14 +66,11 @@ pub fn main() {
*control_flow = ControlFlow::Wait;
match event {
Event::DeviceEvent {
event: DeviceEvent::ModifiersChanged(new_modifiers),
..
} => {
modifiers = new_modifiers;
}
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers;
}
WindowEvent::Resized(new_size) => {
logical_size =
new_size.to_logical(window.scale_factor());
@ -81,6 +79,7 @@ pub fn main() {
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
_ => {}
}
@ -163,6 +162,7 @@ pub fn main() {
swap_chain = SwapChain::new(
&device,
&surface,
format,
size.width,
size.height,
);

View file

@ -0,0 +1,9 @@
[package]
name = "pane_grid"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }

View file

@ -0,0 +1,28 @@
## Pane grid
A grid of panes that can be split, resized, and reorganized.
This example showcases the `PaneGrid` widget, which features:
* Vertical and horizontal splits
* Tracking of the last active pane
* Mouse-based resizing
* Drag and drop to reorganize panes
* Hotkey support
* Configurable modifier keys
* API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
The __[`main`]__ file contains all the code of the example.
<div align="center">
<a href="https://gfycat.com/mixedflatjellyfish">
<img src="https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif">
</a>
</div>
You can run it with `cargo run`:
```
cargo run --package pane_grid
```
[`main`]: src/main.rs

View file

@ -0,0 +1,306 @@
use iced::{
button, keyboard, pane_grid, scrollable, Align, Button, Column, Container,
Element, HorizontalAlignment, Length, PaneGrid, Sandbox, Scrollable,
Settings, Text,
};
pub fn main() {
Example::run(Settings::default())
}
struct Example {
panes: pane_grid::State<Content>,
panes_created: usize,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Split(pane_grid::Axis, pane_grid::Pane),
SplitFocused(pane_grid::Axis),
FocusAdjacent(pane_grid::Direction),
Dragged(pane_grid::DragEvent),
Resized(pane_grid::ResizeEvent),
Close(pane_grid::Pane),
CloseFocused,
}
impl Sandbox for Example {
type Message = Message;
fn new() -> Self {
let (panes, _) = pane_grid::State::new(Content::new(0));
Example {
panes,
panes_created: 1,
}
}
fn title(&self) -> String {
String::from("Pane grid - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::Split(axis, pane) => {
let _ = self.panes.split(
axis,
&pane,
Content::new(self.panes_created),
);
self.panes_created += 1;
}
Message::SplitFocused(axis) => {
if let Some(pane) = self.panes.active() {
let _ = self.panes.split(
axis,
&pane,
Content::new(self.panes_created),
);
self.panes_created += 1;
}
}
Message::FocusAdjacent(direction) => {
if let Some(pane) = self.panes.active() {
if let Some(adjacent) =
self.panes.adjacent(&pane, direction)
{
self.panes.focus(&adjacent);
}
}
}
Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
self.panes.resize(&split, ratio);
}
Message::Dragged(pane_grid::DragEvent::Dropped {
pane,
target,
}) => {
self.panes.swap(&pane, &target);
}
Message::Dragged(_) => {}
Message::Close(pane) => {
let _ = self.panes.close(&pane);
}
Message::CloseFocused => {
if let Some(pane) = self.panes.active() {
let _ = self.panes.close(&pane);
}
}
}
}
fn view(&mut self) -> Element<Message> {
let total_panes = self.panes.len();
let pane_grid =
PaneGrid::new(&mut self.panes, |pane, content, focus| {
content.view(pane, focus, total_panes)
})
.width(Length::Fill)
.height(Length::Fill)
.spacing(10)
.on_drag(Message::Dragged)
.on_resize(Message::Resized)
.on_key_press(handle_hotkey);
Column::new()
.width(Length::Fill)
.height(Length::Fill)
.padding(10)
.push(pane_grid)
.into()
}
}
fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> {
use keyboard::KeyCode;
use pane_grid::{Axis, Direction};
let direction = match event.key_code {
KeyCode::Up => Some(Direction::Up),
KeyCode::Down => Some(Direction::Down),
KeyCode::Left => Some(Direction::Left),
KeyCode::Right => Some(Direction::Right),
_ => None,
};
match event.key_code {
KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
KeyCode::W => Some(Message::CloseFocused),
_ => direction.map(Message::FocusAdjacent),
}
}
struct Content {
id: usize,
scroll: scrollable::State,
split_horizontally: button::State,
split_vertically: button::State,
close: button::State,
}
impl Content {
fn new(id: usize) -> Self {
Content {
id,
scroll: scrollable::State::new(),
split_horizontally: button::State::new(),
split_vertically: button::State::new(),
close: button::State::new(),
}
}
fn view(
&mut self,
pane: pane_grid::Pane,
focus: Option<pane_grid::Focus>,
total_panes: usize,
) -> Element<Message> {
let Content {
id,
scroll,
split_horizontally,
split_vertically,
close,
} = self;
let button = |state, label, message, style| {
Button::new(
state,
Text::new(label)
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Center)
.size(16),
)
.width(Length::Fill)
.padding(8)
.on_press(message)
.style(style)
};
let mut controls = Column::new()
.spacing(5)
.max_width(150)
.push(button(
split_horizontally,
"Split horizontally",
Message::Split(pane_grid::Axis::Horizontal, pane),
style::Button::Primary,
))
.push(button(
split_vertically,
"Split vertically",
Message::Split(pane_grid::Axis::Vertical, pane),
style::Button::Primary,
));
if total_panes > 1 {
controls = controls.push(button(
close,
"Close",
Message::Close(pane),
style::Button::Destructive,
));
}
let content = Scrollable::new(scroll)
.width(Length::Fill)
.spacing(10)
.align_items(Align::Center)
.push(Text::new(format!("Pane {}", id)).size(30))
.push(controls);
Container::new(Column::new().padding(5).push(content))
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.style(style::Pane {
is_focused: focus.is_some(),
})
.into()
}
}
mod style {
use iced::{button, container, Background, Color, Vector};
const SURFACE: Color = Color::from_rgb(
0xF2 as f32 / 255.0,
0xF3 as f32 / 255.0,
0xF5 as f32 / 255.0,
);
const ACTIVE: Color = Color::from_rgb(
0x72 as f32 / 255.0,
0x89 as f32 / 255.0,
0xDA as f32 / 255.0,
);
const HOVERED: Color = Color::from_rgb(
0x67 as f32 / 255.0,
0x7B as f32 / 255.0,
0xC4 as f32 / 255.0,
);
pub struct Pane {
pub is_focused: bool,
}
impl container::StyleSheet for Pane {
fn style(&self) -> container::Style {
container::Style {
background: Some(Background::Color(SURFACE)),
border_width: 2,
border_color: Color {
a: if self.is_focused { 1.0 } else { 0.3 },
..Color::BLACK
},
..Default::default()
}
}
}
pub enum Button {
Primary,
Destructive,
}
impl button::StyleSheet for Button {
fn active(&self) -> button::Style {
let (background, text_color) = match self {
Button::Primary => (Some(ACTIVE), Color::WHITE),
Button::Destructive => {
(None, Color::from_rgb8(0xFF, 0x47, 0x47))
}
};
button::Style {
text_color,
background: background.map(Background::Color),
border_radius: 5,
shadow_offset: Vector::new(0.0, 0.0),
..button::Style::default()
}
}
fn hovered(&self) -> button::Style {
let active = self.active();
let background = match self {
Button::Primary => Some(HOVERED),
Button::Destructive => Some(Color {
a: 0.2,
..active.text_color
}),
};
button::Style {
background: background.map(Background::Color),
..active
}
}
}
}

View file

@ -7,12 +7,16 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["image", "debug", "tokio"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rand = { version = "0.7", features = ["wasm-bindgen"] }
[dependencies.serde]
version = "1.0"
features = ["derive"]
[dependencies.reqwest]
version = "0.10"
git = "https://github.com/hecrj/reqwest.git"
branch = "feature/wasm-deserialize-json"
version = "0.10.2"
features = ["json"]
[dependencies.rand]
version = "0.7"
features = ["wasm-bindgen"]

View file

@ -93,6 +93,7 @@ impl Sandbox for Styling {
ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme);
let scrollable = Scrollable::new(&mut self.scroll)
.width(Length::Fill)
.height(Length::Units(100))
.style(self.theme)
.push(Text::new("Scroll me!"))

View file

@ -7,3 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["svg"] }
env_logger = "0.7"

View file

@ -1,6 +1,8 @@
use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg};
pub fn main() {
env_logger::init();
Tiger::run(Settings::default())
}
@ -22,9 +24,12 @@ impl Sandbox for Tiger {
fn view(&mut self) -> Element<()> {
let content = Column::new().padding(20).push(
Svg::new(format!("{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR")))
.width(Length::Fill)
.height(Length::Fill),
Svg::new(format!(
"{}/resources/tiger.svg",
env!("CARGO_MANIFEST_DIR")
))
.width(Length::Fill)
.height(Length::Fill),
);
Container::new(content)