Merge branch 'master' into beacon

This commit is contained in:
Héctor Ramón Jiménez 2025-03-04 19:11:37 +01:00
commit 8bd5de72ea
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
371 changed files with 33138 additions and 12950 deletions

View file

@ -1,8 +1,6 @@
# Examples
__Iced moves fast and the `master` branch can contain breaking changes!__ If
you want to learn about a specific release, check out [the release list].
[the release list]: https://github.com/iced-rs/iced/releases
__Iced moves fast and the `master` branch can contain breaking changes!__ If you want to browse examples that are compatible with the latest release,
then [switch to the `latest` branch](https://github.com/iced-rs/iced/tree/latest/examples#examples).
## [Tour](tour)
A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced.

View file

@ -2,7 +2,7 @@
name = "arc"
version = "0.1.0"
authors = ["ThatsNoMoon <git@thatsnomoon.dev>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -2,12 +2,13 @@ use std::{f32::consts::PI, time::Instant};
use iced::mouse;
use iced::widget::canvas::{
self, stroke, Cache, Canvas, Geometry, Path, Stroke,
self, Cache, Canvas, Geometry, Path, Stroke, stroke,
};
use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme};
use iced::window;
use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
pub fn main() -> iced::Result {
iced::program("Arc - Iced", Arc::update, Arc::view)
iced::application("Arc - Iced", Arc::update, Arc::view)
.subscription(Arc::subscription)
.theme(|_| Theme::Dark)
.antialiasing(true)
@ -30,15 +31,11 @@ impl Arc {
}
fn view(&self) -> Element<Message> {
Canvas::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
Canvas::new(self).width(Fill).height(Fill).into()
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(10))
.map(|_| Message::Tick)
window::frames().map(|_| Message::Tick)
}
}

View file

@ -2,7 +2,7 @@
name = "bezier_tool"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,10 +1,9 @@
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
use iced::alignment;
use iced::widget::{button, container, horizontal_space, hover};
use iced::{Element, Length, Theme};
use iced::widget::{button, container, horizontal_space, hover, right};
use iced::{Element, Theme};
pub fn main() -> iced::Result {
iced::program("Bezier Tool - Iced", Example::update, Example::view)
iced::application("Bezier Tool - Iced", Example::update, Example::view)
.theme(|_| Theme::CatppuccinMocha)
.antialiasing(true)
.run()
@ -42,14 +41,12 @@ impl Example {
if self.curves.is_empty() {
container(horizontal_space())
} else {
container(
right(
button("Clear")
.style(button::danger)
.on_press(Message::Clear),
)
.padding(10)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right)
},
))
.padding(20)
@ -59,9 +56,10 @@ impl Example {
mod bezier {
use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke};
use iced::{Element, Length, Point, Rectangle, Renderer, Theme};
use iced::widget::canvas::{
self, Canvas, Event, Frame, Geometry, Path, Stroke,
};
use iced::{Element, Fill, Point, Rectangle, Renderer, Theme};
#[derive(Default)]
pub struct State {
@ -74,8 +72,8 @@ mod bezier {
state: self,
curves,
})
.width(Length::Fill)
.height(Length::Fill)
.width(Fill)
.height(Fill)
.into()
}
@ -89,57 +87,56 @@ mod bezier {
curves: &'a [Curve],
}
impl<'a> canvas::Program<Curve> for Bezier<'a> {
impl canvas::Program<Curve> for Bezier<'_> {
type State = Option<Pending>;
fn update(
&self,
state: &mut Self::State,
event: Event,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> (event::Status, Option<Curve>) {
let Some(cursor_position) = cursor.position_in(bounds) else {
return (event::Status::Ignored, None);
};
) -> Option<canvas::Action<Curve>> {
let cursor_position = cursor.position_in(bounds)?;
match event {
Event::Mouse(mouse_event) => {
let message = match mouse_event {
mouse::Event::ButtonPressed(mouse::Button::Left) => {
match *state {
None => {
*state = Some(Pending::One {
from: cursor_position,
});
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)) => Some(
match *state {
None => {
*state = Some(Pending::One {
from: cursor_position,
});
None
}
Some(Pending::One { from }) => {
*state = Some(Pending::Two {
from,
to: cursor_position,
});
None
}
Some(Pending::Two { from, to }) => {
*state = None;
Some(Curve {
from,
to,
control: cursor_position,
})
}
}
canvas::Action::request_redraw()
}
_ => None,
};
Some(Pending::One { from }) => {
*state = Some(Pending::Two {
from,
to: cursor_position,
});
(event::Status::Captured, message)
canvas::Action::request_redraw()
}
Some(Pending::Two { from, to }) => {
*state = None;
canvas::Action::publish(Curve {
from,
to,
control: cursor_position,
})
}
}
.and_capture(),
),
Event::Mouse(mouse::Event::CursorMoved { .. })
if state.is_some() =>
{
Some(canvas::Action::request_redraw())
}
_ => (event::Status::Ignored, None),
_ => None,
}
}

View file

@ -0,0 +1,26 @@
[package]
name = "changelog"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2024"
publish = false
[lints.clippy]
large_enum_variant = "allow"
[dependencies]
iced.workspace = true
iced.features = ["tokio", "markdown", "highlighter", "debug"]
log.workspace = true
thiserror.workspace = true
tokio.features = ["fs", "process"]
tokio.workspace = true
serde = "1"
webbrowser = "1"
tracing-subscriber = "0.3"
[dependencies.reqwest]
version = "0.12"
features = ["json"]

View file

@ -0,0 +1,386 @@
use serde::Deserialize;
use tokio::fs;
use tokio::process;
use std::collections::BTreeSet;
use std::env;
use std::fmt;
use std::io;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Changelog {
ids: Vec<u64>,
added: Vec<String>,
changed: Vec<String>,
fixed: Vec<String>,
removed: Vec<String>,
authors: Vec<String>,
}
impl Changelog {
pub fn new() -> Self {
Self {
ids: Vec::new(),
added: Vec::new(),
changed: Vec::new(),
fixed: Vec::new(),
removed: Vec::new(),
authors: Vec::new(),
}
}
pub async fn list() -> Result<(Self, Vec<Contribution>), Error> {
let mut changelog = Self::new();
{
let markdown = fs::read_to_string("CHANGELOG.md").await?;
if let Some(unreleased) = markdown.split("\n## ").nth(1) {
let sections = unreleased.split("\n\n");
for section in sections {
if section.starts_with("Many thanks to...") {
for author in section.lines().skip(1) {
let author = author.trim_start_matches("- @");
if author.is_empty() {
continue;
}
changelog.authors.push(author.to_owned());
}
continue;
}
let Some((_, rest)) = section.split_once("### ") else {
continue;
};
let Some((name, rest)) = rest.split_once("\n") else {
continue;
};
let category = match name {
"Added" => Category::Added,
"Fixed" => Category::Fixed,
"Changed" => Category::Changed,
"Removed" => Category::Removed,
_ => continue,
};
for entry in rest.lines() {
let Some((_, id)) = entry.split_once("[#") else {
continue;
};
let Some((id, _)) = id.split_once(']') else {
continue;
};
let Ok(id): Result<u64, _> = id.parse() else {
continue;
};
changelog.ids.push(id);
let target = match category {
Category::Added => &mut changelog.added,
Category::Changed => &mut changelog.changed,
Category::Fixed => &mut changelog.fixed,
Category::Removed => &mut changelog.removed,
};
target.push(entry.to_owned());
}
}
}
}
let mut candidates = Contribution::list().await?;
for reviewed_entry in changelog.entries() {
candidates.retain(|candidate| candidate.id != reviewed_entry);
}
Ok((changelog, candidates))
}
pub async fn save(self) -> Result<(), Error> {
let markdown = fs::read_to_string("CHANGELOG.md").await?;
let Some((header, rest)) = markdown.split_once("\n## ") else {
return Err(Error::InvalidFormat);
};
let Some((_unreleased, rest)) = rest.split_once("\n## ") else {
return Err(Error::InvalidFormat);
};
let unreleased = format!("\n## [Unreleased]\n{self}");
let rest = format!("\n## {rest}");
let changelog = [header, &unreleased, &rest].concat();
fs::write("CHANGELOG.md", changelog).await?;
Ok(())
}
pub fn len(&self) -> usize {
self.ids.len()
}
pub fn entries(&self) -> impl Iterator<Item = u64> + '_ {
self.ids.iter().copied()
}
pub fn push(&mut self, entry: Entry) {
self.ids.push(entry.id);
let item = format!(
"- {title}. [#{id}](https://github.com/iced-rs/iced/pull/{id})",
title = entry.title,
id = entry.id
);
let target = match entry.category {
Category::Added => &mut self.added,
Category::Changed => &mut self.changed,
Category::Fixed => &mut self.fixed,
Category::Removed => &mut self.removed,
};
target.push(item);
if entry.author != "hecrj" && !self.authors.contains(&entry.author) {
self.authors.push(entry.author);
self.authors.sort_by_key(|author| author.to_lowercase());
}
}
}
impl fmt::Display for Changelog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn section(category: Category, entries: &[String]) -> String {
if entries.is_empty() {
return String::new();
}
format!("### {category}\n{list}\n", list = entries.join("\n"))
}
fn thank_you<'a>(authors: impl IntoIterator<Item = &'a str>) -> String {
let mut list = String::new();
for author in authors {
list.push_str(&format!("- @{author}\n"));
}
format!("Many thanks to...\n{list}")
}
let changelog = [
section(Category::Added, &self.added),
section(Category::Changed, &self.changed),
section(Category::Fixed, &self.fixed),
section(Category::Removed, &self.removed),
thank_you(self.authors.iter().map(String::as_str)),
]
.into_iter()
.filter(|section| !section.is_empty())
.collect::<Vec<String>>()
.join("\n");
f.write_str(&changelog)
}
}
#[derive(Debug, Clone)]
pub struct Entry {
pub id: u64,
pub title: String,
pub category: Category,
pub author: String,
}
impl Entry {
pub fn new(
title: &str,
category: Category,
pull_request: &PullRequest,
) -> Option<Self> {
let title = title.strip_suffix(".").unwrap_or(title);
if title.is_empty() {
return None;
};
Some(Self {
id: pull_request.id,
title: title.to_owned(),
category,
author: pull_request.author.clone(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
Added,
Changed,
Fixed,
Removed,
}
impl Category {
pub const ALL: &'static [Self] =
&[Self::Added, Self::Changed, Self::Fixed, Self::Removed];
pub fn guess(label: &str) -> Option<Self> {
Some(match label {
"feature" | "addition" => Self::Added,
"change" => Self::Changed,
"bug" | "fix" => Self::Fixed,
_ => None?,
})
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Category::Added => "Added",
Category::Changed => "Changed",
Category::Fixed => "Fixed",
Category::Removed => "Removed",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Contribution {
pub id: u64,
}
impl Contribution {
pub async fn list() -> Result<Vec<Contribution>, Error> {
let output = process::Command::new("git")
.args([
"log",
"--oneline",
"--grep",
"#[0-9]*",
"origin/latest..HEAD",
])
.output()
.await?;
let log = String::from_utf8_lossy(&output.stdout);
let mut contributions: Vec<_> = log
.lines()
.filter(|title| !title.is_empty())
.filter_map(|title| {
let (_, pull_request) = title.split_once("#")?;
let (pull_request, _) = pull_request.split_once([')', ' '])?;
Some(Contribution {
id: pull_request.parse().ok()?,
})
})
.collect();
let mut unique = BTreeSet::from_iter(contributions.clone());
contributions.retain_mut(|contribution| unique.remove(contribution));
Ok(contributions)
}
}
#[derive(Debug, Clone)]
pub struct PullRequest {
pub id: u64,
pub title: String,
pub description: Option<String>,
pub labels: Vec<String>,
pub author: String,
}
impl PullRequest {
pub async fn fetch(contribution: Contribution) -> Result<Self, Error> {
let request = reqwest::Client::new()
.request(
reqwest::Method::GET,
format!(
"https://api.github.com/repos/iced-rs/iced/pulls/{}",
contribution.id
),
)
.header("User-Agent", "iced changelog generator")
.header(
"Authorization",
format!(
"Bearer {}",
env::var("GITHUB_TOKEN")
.map_err(|_| Error::GitHubTokenNotFound)?
),
);
#[derive(Deserialize)]
struct Schema {
title: String,
body: Option<String>,
user: User,
labels: Vec<Label>,
}
#[derive(Deserialize)]
struct User {
login: String,
}
#[derive(Deserialize)]
struct Label {
name: String,
}
let schema: Schema = request.send().await?.json().await?;
Ok(Self {
id: contribution.id,
title: schema.title,
description: schema.body,
labels: schema.labels.into_iter().map(|label| label.name).collect(),
author: schema.user.login,
})
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
#[error("io operation failed: {0}")]
IOFailed(Arc<io::Error>),
#[error("http request failed: {0}")]
RequestFailed(Arc<reqwest::Error>),
#[error("no GITHUB_TOKEN variable was set")]
GitHubTokenNotFound,
#[error("the changelog format is not valid")]
InvalidFormat,
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Error::IOFailed(Arc::new(error))
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::RequestFailed(Arc::new(error))
}
}

View file

@ -0,0 +1,10 @@
use iced::widget::{text, Text};
use iced::Font;
pub const FONT_BYTES: &[u8] = include_bytes!("../fonts/changelog-icons.ttf");
const FONT: Font = Font::with_name("changelog-icons");
pub fn copy() -> Text<'static> {
text('\u{e800}').font(FONT)
}

View file

@ -0,0 +1,375 @@
mod changelog;
use crate::changelog::Changelog;
use iced::font;
use iced::widget::{
button, center, column, container, markdown, pick_list, progress_bar,
rich_text, row, scrollable, span, stack, text, text_input,
};
use iced::{Center, Element, Fill, FillPortion, Font, Task, Theme};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::application("Changelog Generator", Generator::update, Generator::view)
.theme(Generator::theme)
.run_with(Generator::new)
}
enum Generator {
Loading,
Reviewing {
changelog: Changelog,
pending: Vec<changelog::Contribution>,
state: State,
preview: Vec<markdown::Item>,
},
Done,
}
enum State {
Loading(changelog::Contribution),
Loaded {
pull_request: changelog::PullRequest,
description: Vec<markdown::Item>,
title: String,
category: changelog::Category,
},
}
#[derive(Debug, Clone)]
enum Message {
ChangelogListed(
Result<(Changelog, Vec<changelog::Contribution>), changelog::Error>,
),
PullRequestFetched(Result<changelog::PullRequest, changelog::Error>),
UrlClicked(markdown::Url),
TitleChanged(String),
CategorySelected(changelog::Category),
Next,
OpenPullRequest(u64),
ChangelogSaved(Result<(), changelog::Error>),
Quit,
}
impl Generator {
fn new() -> (Self, Task<Message>) {
(
Self::Loading,
Task::perform(Changelog::list(), Message::ChangelogListed),
)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ChangelogListed(Ok((changelog, mut pending))) => {
if let Some(contribution) = pending.pop() {
let preview =
markdown::parse(&changelog.to_string()).collect();
*self = Self::Reviewing {
changelog,
pending,
state: State::Loading(contribution.clone()),
preview,
};
Task::perform(
changelog::PullRequest::fetch(contribution),
Message::PullRequestFetched,
)
} else {
*self = Self::Done;
Task::none()
}
}
Message::PullRequestFetched(Ok(pull_request)) => {
let Self::Reviewing { state, .. } = self else {
return Task::none();
};
let description = markdown::parse(
pull_request
.description
.as_deref()
.unwrap_or("*No description provided*"),
)
.collect();
*state = State::Loaded {
title: pull_request.title.clone(),
category: pull_request
.labels
.iter()
.map(String::as_str)
.filter_map(changelog::Category::guess)
.next()
.unwrap_or(changelog::Category::Added),
pull_request,
description,
};
Task::none()
}
Message::UrlClicked(url) => {
let _ = webbrowser::open(url.as_str());
Task::none()
}
Message::TitleChanged(new_title) => {
let Self::Reviewing { state, .. } = self else {
return Task::none();
};
let State::Loaded { title, .. } = state else {
return Task::none();
};
*title = new_title;
Task::none()
}
Message::CategorySelected(new_category) => {
let Self::Reviewing { state, .. } = self else {
return Task::none();
};
let State::Loaded { category, .. } = state else {
return Task::none();
};
*category = new_category;
Task::none()
}
Message::Next => {
let Self::Reviewing {
changelog,
pending,
state,
preview,
..
} = self
else {
return Task::none();
};
let State::Loaded {
title,
category,
pull_request,
..
} = state
else {
return Task::none();
};
if let Some(entry) =
changelog::Entry::new(title, *category, pull_request)
{
changelog.push(entry);
let save = Task::perform(
changelog.clone().save(),
Message::ChangelogSaved,
);
*preview =
markdown::parse(&changelog.to_string()).collect();
if let Some(contribution) = pending.pop() {
*state = State::Loading(contribution.clone());
Task::batch([
save,
Task::perform(
changelog::PullRequest::fetch(contribution),
Message::PullRequestFetched,
),
])
} else {
*self = Self::Done;
save
}
} else {
Task::none()
}
}
Message::OpenPullRequest(id) => {
let _ = webbrowser::open(&format!(
"https://github.com/iced-rs/iced/pull/{id}"
));
Task::none()
}
Message::ChangelogSaved(Ok(())) => Task::none(),
Message::ChangelogListed(Err(error))
| Message::PullRequestFetched(Err(error))
| Message::ChangelogSaved(Err(error)) => {
log::error!("{error}");
Task::none()
}
Message::Quit => iced::exit(),
}
}
fn view(&self) -> Element<Message> {
match self {
Self::Loading => center("Loading...").into(),
Self::Done => center(
column![
text("Changelog is up-to-date! 🎉")
.shaping(text::Shaping::Advanced),
button("Quit").on_press(Message::Quit),
]
.spacing(10)
.align_x(Center),
)
.into(),
Self::Reviewing {
changelog,
pending,
state,
preview,
} => {
let progress = {
let total = pending.len() + changelog.len();
let bar = progress_bar(
0.0..=1.0,
changelog.len() as f32 / total as f32,
)
.style(progress_bar::secondary);
let label = text!(
"{amount_reviewed} / {total}",
amount_reviewed = changelog.len()
)
.font(Font::MONOSPACE)
.size(12);
stack![bar, center(label)]
};
let form: Element<_> = match state {
State::Loading(contribution) => {
text!("Loading #{}...", contribution.id).into()
}
State::Loaded {
pull_request,
description,
title,
category,
} => {
let details = {
let title = rich_text![
span(&pull_request.title)
.size(24)
.link(pull_request.id),
span(format!(" by {}", pull_request.author))
.font(Font {
style: font::Style::Italic,
..Font::default()
}),
]
.on_link_click(Message::OpenPullRequest)
.font(Font::MONOSPACE);
let description =
markdown(description, self.theme())
.map(Message::UrlClicked);
let labels =
row(pull_request.labels.iter().map(|label| {
container(
text(label)
.size(10)
.font(Font::MONOSPACE),
)
.padding(5)
.style(container::rounded_box)
.into()
}))
.spacing(10)
.wrap();
column![
title,
labels,
scrollable(description)
.spacing(10)
.width(Fill)
.height(Fill)
]
.spacing(10)
};
let title = text_input(
"Type a changelog entry title...",
title,
)
.on_input(Message::TitleChanged);
let category = pick_list(
changelog::Category::ALL,
Some(category),
Message::CategorySelected,
);
let next = button("Next →")
.on_press(Message::Next)
.style(button::success);
column![
details,
row![title, category, next].spacing(10)
]
.spacing(10)
.into()
}
};
let preview = if preview.is_empty() {
center(
container(
text("The changelog is empty... so far!").size(12),
)
.padding(10)
.style(container::rounded_box),
)
} else {
container(
scrollable(
markdown(
preview,
markdown::Settings::with_text_size(
12,
self.theme(),
),
)
.map(Message::UrlClicked),
)
.spacing(10),
)
.width(Fill)
.padding(10)
.style(container::rounded_box)
};
let review = column![container(form).height(Fill), progress]
.spacing(10)
.width(FillPortion(2));
row![review, preview].spacing(10).padding(10).into()
}
}
}
fn theme(&self) -> Theme {
Theme::TokyoNightStorm
}
}

View file

@ -2,7 +2,7 @@
name = "checkbox"
version = "0.1.0"
authors = ["Casper Rogild Storm<casper@rogildstorm.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -4,7 +4,7 @@ use iced::{Element, Font};
const ICON_FONT: Font = Font::with_name("icons");
pub fn main() -> iced::Result {
iced::program("Checkbox - Iced", Example::update, Example::view)
iced::application("Checkbox - Iced", Example::update, Example::view)
.font(include_bytes!("../fonts/icons.ttf").as_slice())
.run()
}

View file

@ -2,12 +2,11 @@
name = "clock"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]
time = { version = "0.3", features = ["local-offset"] }
chrono = "0.4"
tracing-subscriber = "0.3"

View file

@ -1,16 +1,17 @@
use iced::alignment;
use iced::mouse;
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::time::{self, milliseconds};
use iced::widget::canvas::{Cache, Geometry, LineCap, Path, Stroke, stroke};
use iced::widget::{canvas, container};
use iced::{
Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
Theme, Vector,
Degrees, Element, Fill, Font, Point, Radians, Rectangle, Renderer, Size,
Subscription, Theme, Vector,
};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Clock - Iced", Clock::update, Clock::view)
iced::application("Clock - Iced", Clock::update, Clock::view)
.subscription(Clock::subscription)
.theme(Clock::theme)
.antialiasing(true)
@ -18,13 +19,13 @@ pub fn main() -> iced::Result {
}
struct Clock {
now: time::OffsetDateTime,
now: chrono::DateTime<chrono::Local>,
clock: Cache,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Tick(time::OffsetDateTime),
Tick(chrono::DateTime<chrono::Local>),
}
impl Clock {
@ -42,28 +43,18 @@ impl Clock {
}
fn view(&self) -> Element<Message> {
let canvas = canvas(self as &Self)
.width(Length::Fill)
.height(Length::Fill);
let canvas = canvas(self as &Self).width(Fill).height(Fill);
container(canvas)
.width(Length::Fill)
.height(Length::Fill)
.padding(20)
.into()
container(canvas).padding(20).into()
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
Message::Tick(
time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
)
})
time::every(milliseconds(500))
.map(|_| Message::Tick(chrono::offset::Local::now()))
}
fn theme(&self) -> Theme {
Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()]
Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()]
.clone()
}
}
@ -71,8 +62,7 @@ impl Clock {
impl Default for Clock {
fn default() -> Self {
Self {
now: time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
now: chrono::offset::Local::now(),
clock: Cache::default(),
}
}
@ -89,6 +79,8 @@ impl<Message> canvas::Program<Message> for Clock {
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
use chrono::Timelike;
let clock = self.clock.draw(renderer, bounds.size(), |frame| {
let palette = theme.extended_palette();
@ -125,9 +117,14 @@ impl<Message> canvas::Program<Message> for Clock {
};
frame.translate(Vector::new(center.x, center.y));
let minutes_portion =
Radians::from(hand_rotation(self.now.minute(), 60)) / 12.0;
let hour_hand_angle =
Radians::from(hand_rotation(self.now.hour(), 12))
+ minutes_portion;
frame.with_save(|frame| {
frame.rotate(hand_rotation(self.now.hour(), 12));
frame.rotate(hour_hand_angle);
frame.stroke(&short_hand, wide_stroke());
});
@ -163,13 +160,49 @@ impl<Message> canvas::Program<Message> for Clock {
..canvas::Text::default()
});
});
// Draw clock numbers
for hour in 1..=12 {
let angle = Radians::from(hand_rotation(hour, 12))
- Radians::from(Degrees(90.0));
let x = radius * angle.0.cos();
let y = radius * angle.0.sin();
frame.fill_text(canvas::Text {
content: format!("{}", hour),
size: (radius / 5.0).into(),
position: Point::new(x * 0.82, y * 0.82),
color: palette.secondary.strong.text,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
font: Font::MONOSPACE,
..canvas::Text::default()
});
}
// Draw ticks
for tick in 0..60 {
let angle = hand_rotation(tick, 60);
let width = if tick % 5 == 0 { 3.0 } else { 1.0 };
frame.with_save(|frame| {
frame.rotate(angle);
frame.fill(
&Path::rectangle(
Point::new(0.0, radius - 15.0),
Size::new(width, 7.0),
),
palette.secondary.strong.text,
);
});
}
});
vec![clock]
}
}
fn hand_rotation(n: u8, total: u8) -> Degrees {
fn hand_rotation(n: u32, total: u32) -> Degrees {
let turns = n as f32 / total as f32;
Degrees(360.0 * turns)

View file

@ -2,7 +2,7 @@
name = "color_palette"
version = "0.1.0"
authors = ["Clark Moody <clark@clarkmoody.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,17 +1,17 @@
use iced::alignment::{self, Alignment};
use iced::alignment;
use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider};
use iced::widget::{Slider, column, row, text};
use iced::{
Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size,
Vector,
Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer,
Size, Vector,
};
use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue};
use palette::{Darken, Hsl, Lighten, ShiftHue, convert::FromColor, rgb::Rgb};
use std::marker::PhantomData;
use std::ops::RangeInclusive;
pub fn main() -> iced::Result {
iced::program(
iced::application(
"Color Palette - Iced",
ColorPalette::update,
ColorPalette::view,
@ -89,6 +89,7 @@ impl ColorPalette {
primary: *self.theme.lower.first().unwrap(),
text: *self.theme.higher.last().unwrap(),
success: *self.theme.lower.last().unwrap(),
warning: *self.theme.higher.last().unwrap(),
danger: *self.theme.higher.last().unwrap(),
},
)
@ -150,10 +151,7 @@ impl Theme {
}
pub fn view(&self) -> Element<Message> {
Canvas::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
Canvas::new(self).width(Fill).height(Fill).into()
}
fn draw(&self, frame: &mut Frame, text_color: Color) {
@ -320,7 +318,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> {
text(color.to_string()).width(185).size(12),
]
.spacing(10)
.align_items(Alignment::Center)
.align_y(Center)
.into()
}
}

View file

@ -2,7 +2,7 @@
name = "combo_box"
version = "0.1.0"
authors = ["Joao Freitas <jhff.15@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,7 +1,7 @@
use iced::widget::{
center, column, combo_box, scrollable, text, vertical_space,
};
use iced::{Alignment, Element, Length};
use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Combo Box - Iced", Example::update, Example::view)
@ -64,8 +64,8 @@ impl Example {
combo_box,
vertical_space().height(150),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.width(Fill)
.align_x(Center)
.spacing(10);
center(scrollable(content)).into()

View file

@ -1,10 +0,0 @@
[package]
name = "component"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["debug", "lazy"]

View file

@ -1,156 +0,0 @@
use iced::widget::center;
use iced::Element;
use numeric_input::numeric_input;
pub fn main() -> iced::Result {
iced::run("Component - Iced", Component::update, Component::view)
}
#[derive(Default)]
struct Component {
value: Option<u32>,
}
#[derive(Debug, Clone, Copy)]
enum Message {
NumericInputChanged(Option<u32>),
}
impl Component {
fn update(&mut self, message: Message) {
match message {
Message::NumericInputChanged(value) => {
self.value = value;
}
}
}
fn view(&self) -> Element<Message> {
center(numeric_input(self.value, Message::NumericInputChanged))
.padding(20)
.into()
}
}
mod numeric_input {
use iced::alignment::{self, Alignment};
use iced::widget::{button, component, row, text, text_input, Component};
use iced::{Element, Length, Size};
pub struct NumericInput<Message> {
value: Option<u32>,
on_change: Box<dyn Fn(Option<u32>) -> Message>,
}
pub fn numeric_input<Message>(
value: Option<u32>,
on_change: impl Fn(Option<u32>) -> Message + 'static,
) -> NumericInput<Message> {
NumericInput::new(value, on_change)
}
#[derive(Debug, Clone)]
pub enum Event {
InputChanged(String),
IncrementPressed,
DecrementPressed,
}
impl<Message> NumericInput<Message> {
pub fn new(
value: Option<u32>,
on_change: impl Fn(Option<u32>) -> Message + 'static,
) -> Self {
Self {
value,
on_change: Box::new(on_change),
}
}
}
impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
where
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
{
type State = ();
type Event = Event;
fn update(
&mut self,
_state: &mut Self::State,
event: Event,
) -> Option<Message> {
match event {
Event::IncrementPressed => Some((self.on_change)(Some(
self.value.unwrap_or_default().saturating_add(1),
))),
Event::DecrementPressed => Some((self.on_change)(Some(
self.value.unwrap_or_default().saturating_sub(1),
))),
Event::InputChanged(value) => {
if value.is_empty() {
Some((self.on_change)(None))
} else {
value
.parse()
.ok()
.map(Some)
.map(self.on_change.as_ref())
}
}
}
}
fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> {
let button = |label, on_press| {
button(
text(label)
.width(Length::Fill)
.height(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center),
)
.width(40)
.height(40)
.on_press(on_press)
};
row![
button("-", Event::DecrementPressed),
text_input(
"Type a number",
self.value
.as_ref()
.map(u32::to_string)
.as_deref()
.unwrap_or(""),
)
.on_input(Event::InputChanged)
.padding(10),
button("+", Event::IncrementPressed),
]
.align_items(Alignment::Center)
.spacing(10)
.into()
}
fn size_hint(&self) -> Size<Length> {
Size {
width: Length::Fill,
height: Length::Shrink,
}
}
}
impl<'a, Message, Theme> From<NumericInput<Message>>
for Element<'a, Message, Theme>
where
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
Message: 'a,
{
fn from(numeric_input: NumericInput<Message>) -> Self {
component(numeric_input)
}
}
}

View file

@ -2,7 +2,7 @@
name = "counter"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
@ -10,4 +10,7 @@ iced.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
iced.workspace = true
iced.features = ["webgl"]
iced.features = ["webgl", "fira-sans"]
[dev-dependencies]
iced_test.workspace = true

View file

@ -1,5 +1,5 @@
use iced::widget::{button, column, text, Column};
use iced::Alignment;
use iced::Center;
use iced::widget::{Column, button, column, text};
pub fn main() -> iced::Result {
iced::run("A cool counter", Counter::update, Counter::view)
@ -35,6 +35,34 @@ impl Counter {
button("Decrement").on_press(Message::Decrement)
]
.padding(20)
.align_items(Alignment::Center)
.align_x(Center)
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced_test::selector::text;
use iced_test::{Error, simulator};
#[test]
fn it_counts() -> Result<(), Error> {
let mut counter = Counter { value: 0 };
let mut ui = simulator(counter.view());
let _ = ui.click(text("Increment"))?;
let _ = ui.click(text("Increment"))?;
let _ = ui.click(text("Decrement"))?;
for message in ui.into_messages() {
counter.update(message);
}
assert_eq!(counter.value, 1);
let mut ui = simulator(counter.view());
assert!(ui.find(text("1")).is_ok(), "Counter should display 1!");
Ok(())
}
}

View file

@ -2,7 +2,7 @@
name = "custom_quad"
version = "0.1.0"
authors = ["Robert Krahn"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -3,12 +3,13 @@ mod quad {
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
use iced::border;
use iced::mouse;
use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size};
pub struct CustomQuad {
size: f32,
radius: [f32; 4],
radius: border::Radius,
border_width: f32,
shadow: Shadow,
}
@ -16,7 +17,7 @@ mod quad {
impl CustomQuad {
pub fn new(
size: f32,
radius: [f32; 4],
radius: border::Radius,
border_width: f32,
shadow: Shadow,
) -> Self {
@ -63,7 +64,7 @@ mod quad {
renderer::Quad {
bounds: layout.bounds(),
border: Border {
radius: self.radius.into(),
radius: self.radius,
width: self.border_width,
color: Color::from_rgb(1.0, 0.0, 0.0),
},
@ -74,22 +75,23 @@ mod quad {
}
}
impl<'a, Message> From<CustomQuad> for Element<'a, Message> {
impl<Message> From<CustomQuad> for Element<'_, Message> {
fn from(circle: CustomQuad) -> Self {
Self::new(circle)
}
}
}
use iced::border;
use iced::widget::{center, column, slider, text};
use iced::{Alignment, Color, Element, Shadow, Vector};
use iced::{Center, Color, Element, Shadow, Vector};
pub fn main() -> iced::Result {
iced::run("Custom Quad - Iced", Example::update, Example::view)
}
struct Example {
radius: [f32; 4],
radius: border::Radius,
border_width: f32,
shadow: Shadow,
}
@ -110,7 +112,7 @@ enum Message {
impl Example {
fn new() -> Self {
Self {
radius: [50.0; 4],
radius: border::radius(50),
border_width: 0.0,
shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, 0.8),
@ -121,19 +123,18 @@ impl Example {
}
fn update(&mut self, message: Message) {
let [tl, tr, br, bl] = self.radius;
match message {
Message::RadiusTopLeftChanged(radius) => {
self.radius = [radius, tr, br, bl];
self.radius = self.radius.top_left(radius);
}
Message::RadiusTopRightChanged(radius) => {
self.radius = [tl, radius, br, bl];
self.radius = self.radius.top_right(radius);
}
Message::RadiusBottomRightChanged(radius) => {
self.radius = [tl, tr, radius, bl];
self.radius = self.radius.bottom_right(radius);
}
Message::RadiusBottomLeftChanged(radius) => {
self.radius = [tl, tr, br, radius];
self.radius = self.radius.bottom_left(radius);
}
Message::BorderWidthChanged(width) => {
self.border_width = width;
@ -151,7 +152,13 @@ impl Example {
}
fn view(&self) -> Element<Message> {
let [tl, tr, br, bl] = self.radius;
let border::Radius {
top_left,
top_right,
bottom_right,
bottom_left,
} = self.radius;
let Shadow {
offset: Vector { x: sx, y: sy },
blur_radius: sr,
@ -165,16 +172,16 @@ impl Example {
self.border_width,
self.shadow
),
text(format!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}")),
slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01),
slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01),
slider(1.0..=100.0, br, Message::RadiusBottomRightChanged)
text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"),
slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01),
slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01),
slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged)
.step(0.01),
slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged)
slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged)
.step(0.01),
slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged)
.step(0.01),
text(format!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}")),
text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"),
slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged)
.step(0.01),
slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged)
@ -185,7 +192,7 @@ impl Example {
.padding(20)
.spacing(20)
.max_width(500)
.align_items(Alignment::Center);
.align_x(Center);
center(content).into()
}

View file

@ -2,11 +2,11 @@
name = "custom_shader"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
edition = "2024"
[dependencies]
iced.workspace = true
iced.features = ["debug", "advanced"]
iced.features = ["debug", "image", "advanced"]
image.workspace = true
bytemuck.workspace = true

View file

@ -3,15 +3,19 @@ mod scene;
use scene::Scene;
use iced::time::Instant;
use iced::widget::shader::wgpu;
use iced::wgpu;
use iced::widget::{center, checkbox, column, row, shader, slider, text};
use iced::window;
use iced::{Alignment, Color, Element, Length, Subscription};
use iced::{Center, Color, Element, Fill, Subscription};
fn main() -> iced::Result {
iced::program("Custom Shader - Iced", IcedCubes::update, IcedCubes::view)
.subscription(IcedCubes::subscription)
.run()
iced::application(
"Custom Shader - Iced",
IcedCubes::update,
IcedCubes::view,
)
.subscription(IcedCubes::subscription)
.run()
}
struct IcedCubes {
@ -118,12 +122,11 @@ impl IcedCubes {
let controls = column![top_controls, bottom_controls,]
.spacing(10)
.padding(20)
.align_items(Alignment::Center);
.align_x(Center);
let shader =
shader(&self.scene).width(Length::Fill).height(Length::Fill);
let shader = shader(&self.scene).width(Fill).height(Fill);
center(column![shader, controls].align_items(Alignment::Center)).into()
center(column![shader, controls].align_x(Center)).into()
}
fn subscription(&self) -> Subscription<Message> {

View file

@ -241,8 +241,10 @@ impl Pipeline {
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
entry_point: Some("vs_main"),
buffers: &[Vertex::desc(), cube::Raw::desc()],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
@ -259,7 +261,7 @@ impl Pipeline {
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
@ -276,8 +278,11 @@ impl Pipeline {
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
let depth_pipeline = DepthPipeline::new(
@ -488,8 +493,10 @@ impl DepthPipeline {
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
entry_point: Some("vs_main"),
buffers: &[],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
@ -502,14 +509,17 @@ impl DepthPipeline {
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
multiview: None,
cache: None,
});
Self {

View file

@ -1,8 +1,8 @@
use crate::scene::pipeline::Vertex;
use crate::wgpu;
use glam::{vec2, vec3, Vec3};
use rand::{thread_rng, Rng};
use glam::{Vec3, vec2, vec3};
use rand::{Rng, thread_rng};
/// A single instance of a cube.
#[derive(Debug, Clone)]

View file

@ -2,7 +2,7 @@
name = "custom_widget"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,19 +1,11 @@
//! This example showcases a simple native custom widget that draws a circle.
mod circle {
// For now, to implement a custom native widget you will need to add
// `iced_native` and `iced_wgpu` to your dependencies.
//
// Then, you simply need to define your widget type and implement the
// `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
//
// Of course, you can choose to make the implementation renderer-agnostic,
// if you wish to, by creating your own `Renderer` trait, which could be
// implemented by `iced_wgpu` and other renderers.
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
use iced::border;
use iced::mouse;
use iced::{Border, Color, Element, Length, Rectangle, Size};
use iced::{Color, Element, Length, Rectangle, Size};
pub struct Circle {
radius: f32,
@ -62,7 +54,7 @@ mod circle {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: Border::rounded(self.radius),
border: border::rounded(self.radius),
..renderer::Quad::default()
},
Color::BLACK,
@ -70,8 +62,8 @@ mod circle {
}
}
impl<'a, Message, Theme, Renderer> From<Circle>
for Element<'a, Message, Theme, Renderer>
impl<Message, Theme, Renderer> From<Circle>
for Element<'_, Message, Theme, Renderer>
where
Renderer: renderer::Renderer,
{
@ -83,7 +75,7 @@ mod circle {
use circle::circle;
use iced::widget::{center, column, slider, text};
use iced::{Alignment, Element};
use iced::{Center, Element};
pub fn main() -> iced::Result {
iced::run("Custom Widget - Iced", Example::update, Example::view)
@ -114,13 +106,13 @@ impl Example {
fn view(&self) -> Element<Message> {
let content = column![
circle(self.radius),
text(format!("Radius: {:.2}", self.radius)),
text!("Radius: {:.2}", self.radius),
slider(1.0..=100.0, self.radius, Message::RadiusChanged).step(0.01),
]
.padding(20)
.spacing(20)
.max_width(500)
.align_items(Alignment::Center);
.align_x(Center);
center(content).into()
}

View file

@ -2,7 +2,7 @@
name = "download_progress"
version = "0.1.0"
authors = ["Songtronix <contact@songtronix.com>", "Folyd <lyshuhow@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
@ -10,6 +10,5 @@ iced.workspace = true
iced.features = ["tokio"]
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["rustls-tls"]
version = "0.12"
features = ["stream"]

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="utf-8" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Download_Progress - Iced</title>
<base data-trunk-public-url />
</head>
<body style="height: 100%; margin: 0">
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="download_progress" />
</body>
</html>

View file

@ -1,85 +1,46 @@
use iced::subscription;
use iced::futures::StreamExt;
use iced::task::{Straw, sipper};
use std::hash::Hash;
use std::sync::Arc;
// Just a little utility function
pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
id: I,
url: T,
) -> iced::Subscription<(I, Progress)> {
subscription::unfold(id, State::Ready(url.to_string()), move |state| {
download(id, state)
pub fn download(url: impl AsRef<str>) -> impl Straw<(), Progress, Error> {
sipper(async move |mut progress| {
let response = reqwest::get(url.as_ref()).await?;
let total = response.content_length().ok_or(Error::NoContentLength)?;
let _ = progress.send(Progress { percent: 0.0 }).await;
let mut byte_stream = response.bytes_stream();
let mut downloaded = 0;
while let Some(next_bytes) = byte_stream.next().await {
let bytes = next_bytes?;
downloaded += bytes.len();
let _ = progress
.send(Progress {
percent: 100.0 * downloaded as f32 / total as f32,
})
.await;
}
Ok(())
})
}
async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
match state {
State::Ready(url) => {
let response = reqwest::get(&url).await;
match response {
Ok(response) => {
if let Some(total) = response.content_length() {
(
(id, Progress::Started),
State::Downloading {
response,
total,
downloaded: 0,
},
)
} else {
((id, Progress::Errored), State::Finished)
}
}
Err(_) => ((id, 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;
(
(id, Progress::Advanced(percentage)),
State::Downloading {
response,
total,
downloaded,
},
)
}
Ok(None) => ((id, Progress::Finished), State::Finished),
Err(_) => ((id, 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.
iced::futures::future::pending().await
}
}
#[derive(Debug, Clone)]
pub struct Progress {
pub percent: f32,
}
#[derive(Debug, Clone)]
pub enum Progress {
Started,
Advanced(f32),
Finished,
Errored,
pub enum Error {
RequestFailed(Arc<reqwest::Error>),
NoContentLength,
}
pub enum State {
Ready(String),
Downloading {
response: reqwest::Response,
total: u64,
downloaded: u64,
},
Finished,
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::RequestFailed(Arc::new(error))
}
}

View file

@ -1,12 +1,18 @@
mod download;
use iced::widget::{button, center, column, progress_bar, text, Column};
use iced::{Alignment, Element, Subscription};
use download::download;
use iced::task;
use iced::widget::{Column, button, center, column, progress_bar, text};
use iced::{Center, Element, Function, Right, Task};
pub fn main() -> iced::Result {
iced::program("Download Progress - Iced", Example::update, Example::view)
.subscription(Example::subscription)
.run()
iced::application(
"Download Progress - Iced",
Example::update,
Example::view,
)
.run()
}
#[derive(Debug)]
@ -19,7 +25,7 @@ struct Example {
pub enum Message {
Add,
Download(usize),
DownloadProgressed((usize, download::Progress)),
DownloadUpdated(usize, Update),
}
impl Example {
@ -30,32 +36,36 @@ impl Example {
}
}
fn update(&mut self, message: Message) {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Add => {
self.last_id += 1;
self.downloads.push(Download::new(self.last_id));
Task::none()
}
Message::Download(index) => {
if let Some(download) = self.downloads.get_mut(index) {
download.start();
}
let Some(download) = self.downloads.get_mut(index) else {
return Task::none();
};
let task = download.start();
task.map(Message::DownloadUpdated.with(index))
}
Message::DownloadProgressed((id, progress)) => {
Message::DownloadUpdated(id, update) => {
if let Some(download) =
self.downloads.iter_mut().find(|download| download.id == id)
{
download.progress(progress);
download.update(update);
}
Task::none()
}
}
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch(self.downloads.iter().map(Download::subscription))
}
fn view(&self) -> Element<Message> {
let downloads =
Column::with_children(self.downloads.iter().map(Download::view))
@ -65,7 +75,7 @@ impl Example {
.padding(10),
)
.spacing(20)
.align_items(Alignment::End);
.align_x(Right);
center(downloads).padding(20).into()
}
@ -83,10 +93,16 @@ struct Download {
state: State,
}
#[derive(Debug, Clone)]
pub enum Update {
Downloading(download::Progress),
Finished(Result<(), download::Error>),
}
#[derive(Debug)]
enum State {
Idle,
Downloading { progress: f32 },
Downloading { progress: f32, _task: task::Handle },
Finished,
Errored,
}
@ -99,50 +115,54 @@ impl Download {
}
}
pub fn start(&mut self) {
pub fn start(&mut self) -> Task<Update> {
match self.state {
State::Idle { .. }
| State::Finished { .. }
| State::Errored { .. } => {
self.state = State::Downloading { progress: 0.0 };
let (task, handle) = Task::sip(
download(
"https://huggingface.co/\
mattshumer/Reflection-Llama-3.1-70B/\
resolve/main/model-00001-of-00162.safetensors",
),
Update::Downloading,
Update::Finished,
)
.abortable();
self.state = State::Downloading {
progress: 0.0,
_task: handle.abort_on_drop(),
};
task
}
State::Downloading { .. } => {}
State::Downloading { .. } => Task::none(),
}
}
pub fn progress(&mut self, new_progress: download::Progress) {
if let State::Downloading { progress } = &mut self.state {
match new_progress {
download::Progress::Started => {
*progress = 0.0;
pub fn update(&mut self, update: Update) {
if let State::Downloading { progress, .. } = &mut self.state {
match update {
Update::Downloading(new_progress) => {
*progress = new_progress.percent;
}
download::Progress::Advanced(percentage) => {
*progress = percentage;
}
download::Progress::Finished => {
self.state = State::Finished;
}
download::Progress::Errored => {
self.state = State::Errored;
Update::Finished(result) => {
self.state = if result.is_ok() {
State::Finished
} else {
State::Errored
};
}
}
}
}
pub fn subscription(&self) -> Subscription<Message> {
match self.state {
State::Downloading { .. } => {
download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
.map(Message::DownloadProgressed)
}
_ => Subscription::none(),
}
}
pub fn view(&self) -> Element<Message> {
let current_progress = match &self.state {
State::Idle { .. } => 0.0,
State::Downloading { progress } => *progress,
State::Downloading { progress, .. } => *progress,
State::Finished { .. } => 100.0,
State::Errored { .. } => 0.0,
};
@ -156,25 +176,25 @@ impl Download {
State::Finished => {
column!["Download finished!", button("Start again")]
.spacing(10)
.align_items(Alignment::Center)
.align_x(Center)
.into()
}
State::Downloading { .. } => {
text(format!("Downloading... {current_progress:.2}%")).into()
text!("Downloading... {current_progress:.2}%").into()
}
State::Errored => column![
"Something went wrong :(",
button("Try again").on_press(Message::Download(self.id)),
]
.spacing(10)
.align_items(Alignment::Center)
.align_x(Center)
.into(),
};
Column::new()
.spacing(10)
.padding(10)
.align_items(Alignment::Center)
.align_x(Center)
.push(progress_bar)
.push(control)
.into()

View file

@ -2,7 +2,7 @@
name = "editor"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,10 +1,10 @@
use iced::highlighter::{self, Highlighter};
use iced::highlighter;
use iced::keyboard;
use iced::widget::{
button, column, container, horizontal_space, pick_list, row, text,
text_editor, tooltip,
self, button, center_x, column, container, horizontal_space, pick_list,
row, text, text_editor, toggler, tooltip,
};
use iced::{Alignment, Command, Element, Font, Length, Subscription, Theme};
use iced::{Center, Element, Fill, Font, Task, Theme};
use std::ffi;
use std::io;
@ -12,19 +12,18 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
pub fn main() -> iced::Result {
iced::program("Editor - Iced", Editor::update, Editor::view)
.load(Editor::load)
.subscription(Editor::subscription)
iced::application("Editor - Iced", Editor::update, Editor::view)
.theme(Editor::theme)
.font(include_bytes!("../fonts/icons.ttf").as_slice())
.default_font(Font::MONOSPACE)
.run()
.run_with(Editor::new)
}
struct Editor {
file: Option<PathBuf>,
content: text_editor::Content,
theme: highlighter::Theme,
word_wrap: bool,
is_loading: bool,
is_dirty: bool,
}
@ -33,6 +32,7 @@ struct Editor {
enum Message {
ActionPerformed(text_editor::Action),
ThemeSelected(highlighter::Theme),
WordWrapToggled(bool),
NewFile,
OpenFile,
FileOpened(Result<(PathBuf, Arc<String>), Error>),
@ -41,36 +41,47 @@ enum Message {
}
impl Editor {
fn new() -> Self {
Self {
file: None,
content: text_editor::Content::new(),
theme: highlighter::Theme::SolarizedDark,
is_loading: true,
is_dirty: false,
}
}
fn load() -> Command<Message> {
Command::perform(
load_file(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))),
Message::FileOpened,
fn new() -> (Self, Task<Message>) {
(
Self {
file: None,
content: text_editor::Content::new(),
theme: highlighter::Theme::SolarizedDark,
word_wrap: true,
is_loading: true,
is_dirty: false,
},
Task::batch([
Task::perform(
load_file(format!(
"{}/src/main.rs",
env!("CARGO_MANIFEST_DIR")
)),
Message::FileOpened,
),
widget::focus_next(),
]),
)
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ActionPerformed(action) => {
self.is_dirty = self.is_dirty || action.is_edit();
self.content.perform(action);
Command::none()
Task::none()
}
Message::ThemeSelected(theme) => {
self.theme = theme;
Command::none()
Task::none()
}
Message::WordWrapToggled(word_wrap) => {
self.word_wrap = word_wrap;
Task::none()
}
Message::NewFile => {
if !self.is_loading {
@ -78,15 +89,15 @@ impl Editor {
self.content = text_editor::Content::new();
}
Command::none()
Task::none()
}
Message::OpenFile => {
if self.is_loading {
Command::none()
Task::none()
} else {
self.is_loading = true;
Command::perform(open_file(), Message::FileOpened)
Task::perform(open_file(), Message::FileOpened)
}
}
Message::FileOpened(result) => {
@ -98,16 +109,24 @@ impl Editor {
self.content = text_editor::Content::with_text(&contents);
}
Command::none()
Task::none()
}
Message::SaveFile => {
if self.is_loading {
Command::none()
Task::none()
} else {
self.is_loading = true;
Command::perform(
save_file(self.file.clone(), self.content.text()),
let mut text = self.content.text();
if let Some(ending) = self.content.line_ending() {
if !text.ends_with(ending.as_str()) {
text.push_str(ending.as_str());
}
}
Task::perform(
save_file(self.file.clone(), text),
Message::FileSaved,
)
}
@ -120,20 +139,11 @@ impl Editor {
self.is_dirty = false;
}
Command::none()
Task::none()
}
}
}
fn subscription(&self) -> Subscription<Message> {
keyboard::on_key_press(|key, modifiers| match key.as_ref() {
keyboard::Key::Character("s") if modifiers.command() => {
Some(Message::SaveFile)
}
_ => None,
})
}
fn view(&self) -> Element<Message> {
let controls = row![
action(new_icon(), "New file", Some(Message::NewFile)),
@ -148,6 +158,9 @@ impl Editor {
self.is_dirty.then_some(Message::SaveFile)
),
horizontal_space(),
toggler(self.word_wrap)
.label("Word Wrap")
.on_toggle(Message::WordWrapToggled),
pick_list(
highlighter::Theme::ALL,
Some(self.theme),
@ -157,7 +170,7 @@ impl Editor {
.padding([5, 10])
]
.spacing(10)
.align_items(Alignment::Center);
.align_y(Center);
let status = row![
text(if let Some(path) = &self.file {
@ -183,21 +196,33 @@ impl Editor {
column![
controls,
text_editor(&self.content)
.height(Length::Fill)
.height(Fill)
.on_action(Message::ActionPerformed)
.highlight::<Highlighter>(
highlighter::Settings {
theme: self.theme,
extension: self
.file
.as_deref()
.and_then(Path::extension)
.and_then(ffi::OsStr::to_str)
.map(str::to_string)
.unwrap_or(String::from("rs")),
},
|highlight, _theme| highlight.to_format()
),
.wrapping(if self.word_wrap {
text::Wrapping::Word
} else {
text::Wrapping::None
})
.highlight(
self.file
.as_deref()
.and_then(Path::extension)
.and_then(ffi::OsStr::to_str)
.unwrap_or("rs"),
self.theme,
)
.key_binding(|key_press| {
match key_press.key.as_ref() {
keyboard::Key::Character("s")
if key_press.modifiers.command() =>
{
Some(text_editor::Binding::Custom(
Message::SaveFile,
))
}
_ => text_editor::Binding::from_key_press(key_press),
}
}),
status,
]
.spacing(10)
@ -214,12 +239,6 @@ impl Editor {
}
}
impl Default for Editor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum Error {
DialogClosed,
@ -277,7 +296,7 @@ fn action<'a, Message: Clone + 'a>(
label: &'a str,
on_press: Option<Message>,
) -> Element<'a, Message> {
let action = button(container(content).center_x(30));
let action = button(center_x(content).width(30));
if let Some(on_press) = on_press {
tooltip(

View file

@ -2,7 +2,7 @@
name = "events"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,11 +1,10 @@
use iced::alignment;
use iced::event::{self, Event};
use iced::widget::{button, center, checkbox, text, Column};
use iced::widget::{Column, button, center, checkbox, text};
use iced::window;
use iced::{Alignment, Command, Element, Length, Subscription};
use iced::{Center, Element, Fill, Subscription, Task};
pub fn main() -> iced::Result {
iced::program("Events - Iced", Events::update, Events::view)
iced::application("Events - Iced", Events::update, Events::view)
.subscription(Events::subscription)
.exit_on_close_request(false)
.run()
@ -25,7 +24,7 @@ enum Message {
}
impl Events {
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::EventOccurred(event) if self.enabled => {
self.last.push(event);
@ -34,22 +33,21 @@ impl Events {
let _ = self.last.remove(0);
}
Command::none()
Task::none()
}
Message::EventOccurred(event) => {
if let Event::Window(id, window::Event::CloseRequested) = event
{
window::close(id)
if let Event::Window(window::Event::CloseRequested) = event {
window::get_latest().and_then(window::close)
} else {
Command::none()
Task::none()
}
}
Message::Toggled(enabled) => {
self.enabled = enabled;
Command::none()
Task::none()
}
Message::Exit => window::close(window::Id::MAIN),
Message::Exit => window::get_latest().and_then(window::close),
}
}
@ -61,24 +59,20 @@ impl Events {
let events = Column::with_children(
self.last
.iter()
.map(|event| text(format!("{event:?}")).size(40))
.map(|event| text!("{event:?}").size(40))
.map(Element::from),
);
let toggle = checkbox("Listen to runtime events", self.enabled)
.on_toggle(Message::Toggled);
let exit = button(
text("Exit")
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center),
)
.width(100)
.padding(10)
.on_press(Message::Exit);
let exit = button(text("Exit").width(Fill).align_x(Center))
.width(100)
.padding(10)
.on_press(Message::Exit);
let content = Column::new()
.align_items(Alignment::Center)
.align_x(Center)
.spacing(20)
.push(events)
.push(toggle)

View file

@ -1,7 +1,7 @@
[package]
name = "exit"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,9 +1,9 @@
use iced::widget::{button, center, column};
use iced::window;
use iced::{Alignment, Command, Element};
use iced::{Center, Element, Task};
pub fn main() -> iced::Result {
iced::program("Exit - Iced", Exit::update, Exit::view).run()
iced::application("Exit - Iced", Exit::update, Exit::view).run()
}
#[derive(Default)]
@ -18,13 +18,13 @@ enum Message {
}
impl Exit {
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Confirm => window::close(window::Id::MAIN),
Message::Confirm => window::get_latest().and_then(window::close),
Message::Exit => {
self.show_confirm = true;
Command::none()
Task::none()
}
}
}
@ -44,7 +44,7 @@ impl Exit {
]
}
.spacing(10)
.align_items(Alignment::Center);
.align_x(Center);
center(content).padding(20).into()
}

View file

@ -2,7 +2,7 @@
name = "ferris"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -4,12 +4,12 @@ use iced::widget::{
};
use iced::window;
use iced::{
Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation,
Subscription, Theme,
Bottom, Center, Color, ContentFit, Degrees, Element, Fill, Radians,
Rotation, Subscription, Theme,
};
pub fn main() -> iced::Result {
iced::program("Ferris - Iced", Image::update, Image::view)
iced::application("Ferris - Iced", Image::update, Image::view)
.subscription(Image::subscription)
.theme(|_| Theme::TokyoNight)
.run()
@ -108,7 +108,7 @@ impl Image {
"I am Ferris!"
]
.spacing(20)
.align_items(Alignment::Center);
.align_x(Center);
let fit = row![
pick_list(
@ -122,7 +122,7 @@ impl Image {
Some(self.content_fit),
Message::ContentFitChanged
)
.width(Length::Fill),
.width(Fill),
pick_list(
[RotationStrategy::Floating, RotationStrategy::Solid],
Some(match self.rotation {
@ -131,10 +131,10 @@ impl Image {
}),
Message::RotationStrategyChanged,
)
.width(Length::Fill),
.width(Fill),
]
.spacing(10)
.align_items(Alignment::End);
.align_y(Bottom);
let properties = row![
with_value(
@ -159,12 +159,12 @@ impl Image {
.size(12)
]
.spacing(10)
.align_items(Alignment::Center),
.align_y(Center),
format!("Rotation: {:.0}°", f32::from(self.rotation.degrees()))
)
]
.spacing(10)
.align_items(Alignment::End);
.align_y(Bottom);
container(column![fit, center(i_am_ferris), properties].spacing(10))
.padding(10)
@ -206,6 +206,6 @@ fn with_value<'a>(
) -> Element<'a, Message> {
column![control.into(), text(value).size(12).line_height(1.0)]
.spacing(2)
.align_items(Alignment::Center)
.align_x(Center)
.into()
}

View file

@ -0,0 +1,26 @@
[package]
name = "gallery"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["tokio", "image", "web-colors", "debug"]
reqwest.version = "0.12"
reqwest.features = ["json"]
serde.version = "1.0"
serde.features = ["derive"]
bytes.workspace = true
image.workspace = true
sipper.workspace = true
tokio.workspace = true
blurhash = "0.2.3"
[lints]
workspace = true

View file

@ -0,0 +1,189 @@
use bytes::Bytes;
use serde::Deserialize;
use sipper::{Straw, sipper};
use tokio::task;
use std::fmt;
use std::io;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize)]
pub struct Image {
pub id: Id,
url: String,
hash: String,
}
impl Image {
pub const LIMIT: usize = 99;
pub async fn list() -> Result<Vec<Self>, Error> {
let client = reqwest::Client::new();
#[derive(Deserialize)]
struct Response {
items: Vec<Image>,
}
let response: Response = client
.get("https://civitai.com/api/v1/images")
.query(&[
("sort", "Most Reactions"),
("period", "Week"),
("nsfw", "None"),
("limit", &Image::LIMIT.to_string()),
])
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(response.items)
}
pub async fn blurhash(
self,
width: u32,
height: u32,
) -> Result<Blurhash, Error> {
task::spawn_blocking(move || {
let pixels = blurhash::decode(&self.hash, width, height, 1.0)?;
Ok::<_, Error>(Blurhash {
rgba: Rgba {
width,
height,
pixels: Bytes::from(pixels),
},
})
})
.await?
}
pub fn download(self, size: Size) -> impl Straw<Rgba, Blurhash, Error> {
sipper(async move |mut sender| {
let client = reqwest::Client::new();
if let Size::Thumbnail { width, height } = size {
let image = self.clone();
drop(task::spawn(async move {
if let Ok(blurhash) = image.blurhash(width, height).await {
sender.send(blurhash).await;
}
}));
}
let bytes = client
.get(match size {
Size::Original => self.url,
Size::Thumbnail { width, .. } => self
.url
.split("/")
.map(|part| {
if part.starts_with("width=") {
format!("width={}", width * 2) // High DPI
} else {
part.to_owned()
}
})
.collect::<Vec<_>>()
.join("/"),
})
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let image = task::spawn_blocking(move || {
Ok::<_, Error>(
image::ImageReader::new(io::Cursor::new(bytes))
.with_guessed_format()?
.decode()?
.to_rgba8(),
)
})
.await??;
Ok(Rgba {
width: image.width(),
height: image.height(),
pixels: Bytes::from(image.into_raw()),
})
})
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize,
)]
pub struct Id(u32);
#[derive(Debug, Clone)]
pub struct Blurhash {
pub rgba: Rgba,
}
#[derive(Clone)]
pub struct Rgba {
pub width: u32,
pub height: u32,
pub pixels: Bytes,
}
impl fmt::Debug for Rgba {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Rgba")
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub enum Size {
Original,
Thumbnail { width: u32, height: u32 },
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Error {
RequestFailed(Arc<reqwest::Error>),
IOFailed(Arc<io::Error>),
JoinFailed(Arc<task::JoinError>),
ImageDecodingFailed(Arc<image::ImageError>),
BlurhashDecodingFailed(Arc<blurhash::Error>),
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::RequestFailed(Arc::new(error))
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IOFailed(Arc::new(error))
}
}
impl From<task::JoinError> for Error {
fn from(error: task::JoinError) -> Self {
Self::JoinFailed(Arc::new(error))
}
}
impl From<image::ImageError> for Error {
fn from(error: image::ImageError) -> Self {
Self::ImageDecodingFailed(Arc::new(error))
}
}
impl From<blurhash::Error> for Error {
fn from(error: blurhash::Error) -> Self {
Self::BlurhashDecodingFailed(Arc::new(error))
}
}

View file

@ -0,0 +1,442 @@
//! A simple gallery that displays the daily featured images of Civitai.
//!
//! Showcases lazy loading of images in the background, as well as
//! some smooth animations.
mod civitai;
use crate::civitai::{Error, Id, Image, Rgba, Size};
use iced::animation;
use iced::time::{Instant, milliseconds};
use iced::widget::{
button, center_x, container, horizontal_space, image, mouse_area, opaque,
pop, row, scrollable, stack,
};
use iced::window;
use iced::{
Animation, ContentFit, Element, Fill, Function, Subscription, Task, Theme,
color,
};
use std::collections::HashMap;
fn main() -> iced::Result {
iced::application("Gallery - Iced", Gallery::update, Gallery::view)
.subscription(Gallery::subscription)
.theme(Gallery::theme)
.run_with(Gallery::new)
}
struct Gallery {
images: Vec<Image>,
previews: HashMap<Id, Preview>,
viewer: Viewer,
now: Instant,
}
#[derive(Debug, Clone)]
enum Message {
ImagesListed(Result<Vec<Image>, Error>),
ImagePoppedIn(Id),
ImageDownloaded(Result<Rgba, Error>),
ThumbnailDownloaded(Id, Result<Rgba, Error>),
ThumbnailHovered(Id, bool),
BlurhashDecoded(Id, civitai::Blurhash),
Open(Id),
Close,
Animate(Instant),
}
impl Gallery {
pub fn new() -> (Self, Task<Message>) {
(
Self {
images: Vec::new(),
previews: HashMap::new(),
viewer: Viewer::new(),
now: Instant::now(),
},
Task::perform(Image::list(), Message::ImagesListed),
)
}
pub fn theme(&self) -> Theme {
Theme::TokyoNight
}
pub fn subscription(&self) -> Subscription<Message> {
let is_animating = self
.previews
.values()
.any(|preview| preview.is_animating(self.now))
|| self.viewer.is_animating(self.now);
if is_animating {
window::frames().map(Message::Animate)
} else {
Subscription::none()
}
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ImagesListed(Ok(images)) => {
self.images = images;
Task::none()
}
Message::ImagePoppedIn(id) => {
let Some(image) = self
.images
.iter()
.find(|candidate| candidate.id == id)
.cloned()
else {
return Task::none();
};
Task::sip(
image.download(Size::Thumbnail {
width: Preview::WIDTH,
height: Preview::HEIGHT,
}),
Message::BlurhashDecoded.with(id),
Message::ThumbnailDownloaded.with(id),
)
}
Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba);
Task::none()
}
Message::ThumbnailDownloaded(id, Ok(rgba)) => {
let thumbnail = if let Some(preview) = self.previews.remove(&id)
{
preview.load(rgba)
} else {
Preview::ready(rgba)
};
let _ = self.previews.insert(id, thumbnail);
Task::none()
}
Message::ThumbnailHovered(id, is_hovered) => {
if let Some(preview) = self.previews.get_mut(&id) {
preview.toggle_zoom(is_hovered);
}
Task::none()
}
Message::BlurhashDecoded(id, blurhash) => {
if !self.previews.contains_key(&id) {
let _ = self
.previews
.insert(id, Preview::loading(blurhash.rgba));
}
Task::none()
}
Message::Open(id) => {
let Some(image) = self
.images
.iter()
.find(|candidate| candidate.id == id)
.cloned()
else {
return Task::none();
};
self.viewer.open();
Task::perform(
image.download(Size::Original),
Message::ImageDownloaded,
)
}
Message::Close => {
self.viewer.close();
Task::none()
}
Message::Animate(now) => {
self.now = now;
Task::none()
}
Message::ImagesListed(Err(error))
| Message::ImageDownloaded(Err(error))
| Message::ThumbnailDownloaded(_, Err(error)) => {
dbg!(error);
Task::none()
}
}
}
pub fn view(&self) -> Element<'_, Message> {
let gallery = if self.images.is_empty() {
row((0..=Image::LIMIT).map(|_| placeholder()))
} else {
row(self.images.iter().map(|image| {
card(image, self.previews.get(&image.id), self.now)
}))
}
.spacing(10)
.wrap();
let content =
container(scrollable(center_x(gallery)).spacing(10)).padding(10);
let viewer = self.viewer.view(self.now);
stack![content, viewer].into()
}
}
fn card<'a>(
metadata: &'a Image,
preview: Option<&'a Preview>,
now: Instant,
) -> Element<'a, Message> {
let image = if let Some(preview) = preview {
let thumbnail: Element<'_, _> =
if let Preview::Ready { thumbnail, .. } = &preview {
image(&thumbnail.handle)
.width(Fill)
.height(Fill)
.content_fit(ContentFit::Cover)
.opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
.scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
.into()
} else {
horizontal_space().into()
};
if let Some(blurhash) = preview.blurhash(now) {
let blurhash = image(&blurhash.handle)
.width(Fill)
.height(Fill)
.content_fit(ContentFit::Cover)
.opacity(blurhash.fade_in.interpolate(0.0, 1.0, now));
stack![blurhash, thumbnail].into()
} else {
thumbnail
}
} else {
horizontal_space().into()
};
let card = mouse_area(
container(image)
.width(Preview::WIDTH)
.height(Preview::HEIGHT)
.style(container::dark),
)
.on_enter(Message::ThumbnailHovered(metadata.id, true))
.on_exit(Message::ThumbnailHovered(metadata.id, false));
if let Some(preview) = preview {
let is_thumbnail = matches!(preview, Preview::Ready { .. });
button(card)
.on_press_maybe(is_thumbnail.then_some(Message::Open(metadata.id)))
.padding(0)
.style(button::text)
.into()
} else {
pop(card)
.on_show(|_| Message::ImagePoppedIn(metadata.id))
.into()
}
}
fn placeholder<'a>() -> Element<'a, Message> {
container(horizontal_space())
.width(Preview::WIDTH)
.height(Preview::HEIGHT)
.style(container::dark)
.into()
}
enum Preview {
Loading {
blurhash: Blurhash,
},
Ready {
blurhash: Option<Blurhash>,
thumbnail: Thumbnail,
},
}
struct Blurhash {
handle: image::Handle,
fade_in: Animation<bool>,
}
struct Thumbnail {
handle: image::Handle,
fade_in: Animation<bool>,
zoom: Animation<bool>,
}
impl Preview {
const WIDTH: u32 = 320;
const HEIGHT: u32 = 410;
fn loading(rgba: Rgba) -> Self {
Self::Loading {
blurhash: Blurhash {
fade_in: Animation::new(false)
.duration(milliseconds(700))
.easing(animation::Easing::EaseIn)
.go(true),
handle: image::Handle::from_rgba(
rgba.width,
rgba.height,
rgba.pixels,
),
},
}
}
fn ready(rgba: Rgba) -> Self {
Self::Ready {
blurhash: None,
thumbnail: Thumbnail::new(rgba),
}
}
fn load(self, rgba: Rgba) -> Self {
let Self::Loading { blurhash } = self else {
return self;
};
Self::Ready {
blurhash: Some(blurhash),
thumbnail: Thumbnail::new(rgba),
}
}
fn toggle_zoom(&mut self, enabled: bool) {
if let Self::Ready { thumbnail, .. } = self {
thumbnail.zoom.go_mut(enabled);
}
}
fn is_animating(&self, now: Instant) -> bool {
match &self {
Self::Loading { blurhash } => blurhash.fade_in.is_animating(now),
Self::Ready { thumbnail, .. } => {
thumbnail.fade_in.is_animating(now)
|| thumbnail.zoom.is_animating(now)
}
}
}
fn blurhash(&self, now: Instant) -> Option<&Blurhash> {
match self {
Self::Loading { blurhash, .. } => Some(blurhash),
Self::Ready {
blurhash: Some(blurhash),
thumbnail,
..
} if thumbnail.fade_in.is_animating(now) => Some(blurhash),
Self::Ready { .. } => None,
}
}
}
impl Thumbnail {
pub fn new(rgba: Rgba) -> Self {
Self {
handle: image::Handle::from_rgba(
rgba.width,
rgba.height,
rgba.pixels,
),
fade_in: Animation::new(false).slow().go(true),
zoom: Animation::new(false)
.quick()
.easing(animation::Easing::EaseInOut),
}
}
}
struct Viewer {
image: Option<image::Handle>,
background_fade_in: Animation<bool>,
image_fade_in: Animation<bool>,
}
impl Viewer {
fn new() -> Self {
Self {
image: None,
background_fade_in: Animation::new(false)
.very_slow()
.easing(animation::Easing::EaseInOut),
image_fade_in: Animation::new(false)
.very_slow()
.easing(animation::Easing::EaseInOut),
}
}
fn open(&mut self) {
self.image = None;
self.background_fade_in.go_mut(true);
}
fn show(&mut self, rgba: Rgba) {
self.image = Some(image::Handle::from_rgba(
rgba.width,
rgba.height,
rgba.pixels,
));
self.background_fade_in.go_mut(true);
self.image_fade_in.go_mut(true);
}
fn close(&mut self) {
self.background_fade_in.go_mut(false);
self.image_fade_in.go_mut(false);
}
fn is_animating(&self, now: Instant) -> bool {
self.background_fade_in.is_animating(now)
|| self.image_fade_in.is_animating(now)
}
fn view(&self, now: Instant) -> Element<'_, Message> {
let opacity = self.background_fade_in.interpolate(0.0, 0.8, now);
let image: Element<'_, _> = if let Some(handle) = &self.image {
image(handle)
.width(Fill)
.height(Fill)
.opacity(self.image_fade_in.interpolate(0.0, 1.0, now))
.scale(self.image_fade_in.interpolate(1.5, 1.0, now))
.into()
} else {
horizontal_space().into()
};
if opacity > 0.0 {
opaque(
mouse_area(
container(image)
.center(Fill)
.style(move |_theme| {
container::Style::default()
.background(color!(0x000000, opacity))
})
.padding(20),
)
.on_press(Message::Close),
)
} else {
horizontal_space().into()
}
}
}

View file

@ -2,7 +2,7 @@
name = "game_of_life"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -5,22 +5,25 @@ mod preset;
use grid::Grid;
use preset::Preset;
use iced::time;
use iced::time::{self, milliseconds};
use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text,
};
use iced::{Alignment, Command, Element, Length, Subscription, Theme};
use std::time::Duration;
use iced::{Center, Element, Fill, Function, Subscription, Task, Theme};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Game of Life - Iced", GameOfLife::update, GameOfLife::view)
.subscription(GameOfLife::subscription)
.theme(|_| Theme::Dark)
.antialiasing(true)
.centered()
.run()
iced::application(
"Game of Life - Iced",
GameOfLife::update,
GameOfLife::view,
)
.subscription(GameOfLife::subscription)
.theme(|_| Theme::Dark)
.antialiasing(true)
.centered()
.run()
}
struct GameOfLife {
@ -34,7 +37,7 @@ struct GameOfLife {
#[derive(Debug, Clone)]
enum Message {
Grid(grid::Message, usize),
Grid(usize, grid::Message),
Tick,
TogglePlayback,
ToggleGrid(bool),
@ -56,9 +59,9 @@ impl GameOfLife {
}
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Grid(message, version) => {
Message::Grid(version, message) => {
if version == self.version {
self.grid.update(message);
}
@ -75,9 +78,7 @@ impl GameOfLife {
let version = self.version;
return Command::perform(task, move |message| {
Message::Grid(message, version)
});
return Task::perform(task, Message::Grid.with(version));
}
}
Message::TogglePlayback => {
@ -103,12 +104,12 @@ impl GameOfLife {
}
}
Command::none()
Task::none()
}
fn subscription(&self) -> Subscription<Message> {
if self.is_playing {
time::every(Duration::from_millis(1000 / self.speed as u64))
time::every(milliseconds(1000 / self.speed as u64))
.map(|_| Message::Tick)
} else {
Subscription::none()
@ -126,17 +127,12 @@ impl GameOfLife {
);
let content = column![
self.grid
.view()
.map(move |message| Message::Grid(message, version)),
self.grid.view().map(Message::Grid.with(version)),
controls,
]
.height(Length::Fill);
.height(Fill);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.into()
container(content).width(Fill).height(Fill).into()
}
}
@ -163,9 +159,9 @@ fn view_controls<'a>(
let speed_controls = row![
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
text(format!("x{speed}")).size(16),
text!("x{speed}").size(16),
]
.align_items(Alignment::Center)
.align_y(Center)
.spacing(10);
row![
@ -182,7 +178,7 @@ fn view_controls<'a>(
]
.padding(10)
.spacing(20)
.align_items(Alignment::Center)
.align_y(Center)
.into()
}
@ -190,17 +186,17 @@ mod grid {
use crate::Preset;
use iced::alignment;
use iced::mouse;
use iced::time::{Duration, Instant};
use iced::touch;
use iced::widget::canvas;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text};
use iced::widget::canvas::{
Cache, Canvas, Event, Frame, Geometry, Path, Text,
};
use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector,
Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::future::Future;
use std::ops::RangeInclusive;
use std::time::{Duration, Instant};
pub struct Grid {
state: State,
@ -264,7 +260,7 @@ mod grid {
pub fn tick(
&mut self,
amount: usize,
) -> Option<impl Future<Output = Message>> {
) -> Option<impl Future<Output = Message> + use<>> {
let tick = self.state.tick(amount)?;
self.last_queued_ticks = amount;
@ -329,10 +325,7 @@ mod grid {
}
pub fn view(&self) -> Element<Message> {
Canvas::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
Canvas::new(self).width(Fill).height(Fill).into()
}
pub fn clear(&mut self) {
@ -382,17 +375,15 @@ mod grid {
fn update(
&self,
interaction: &mut Interaction,
event: Event,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
) -> Option<canvas::Action<Message>> {
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
*interaction = Interaction::None;
}
let Some(cursor_position) = cursor.position_in(bounds) else {
return (event::Status::Ignored, None);
};
let cursor_position = cursor.position_in(bounds)?;
let cell = Cell::at(self.project(cursor_position, bounds.size()));
let is_populated = self.state.contains(&cell);
@ -415,7 +406,12 @@ mod grid {
populate.or(unpopulate)
};
(event::Status::Captured, message)
Some(
message
.map(canvas::Action::publish)
.unwrap_or(canvas::Action::request_redraw())
.and_capture(),
)
}
Event::Mouse(mouse_event) => match mouse_event {
mouse::Event::ButtonPressed(button) => {
@ -440,7 +436,12 @@ mod grid {
_ => None,
};
(event::Status::Captured, message)
Some(
message
.map(canvas::Action::publish)
.unwrap_or(canvas::Action::request_redraw())
.and_capture(),
)
}
mouse::Event::CursorMoved { .. } => {
let message = match *interaction {
@ -456,14 +457,16 @@ mod grid {
Interaction::None => None,
};
let event_status = match interaction {
Interaction::None => event::Status::Ignored,
_ => event::Status::Captured,
};
let action = message
.map(canvas::Action::publish)
.unwrap_or(canvas::Action::request_redraw());
(event_status, message)
Some(match interaction {
Interaction::None => action,
_ => action.and_capture(),
})
}
mouse::Event::WheelScrolled { delta } => match delta {
mouse::Event::WheelScrolled { delta } => match *delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
if y < 0.0 && self.scaling > Self::MIN_SCALING
@ -498,18 +501,21 @@ mod grid {
None
};
(
event::Status::Captured,
Some(Message::Scaled(scaling, translation)),
Some(
canvas::Action::publish(Message::Scaled(
scaling,
translation,
))
.and_capture(),
)
} else {
(event::Status::Captured, None)
Some(canvas::Action::capture())
}
}
},
_ => (event::Status::Ignored, None),
_ => None,
},
_ => (event::Status::Ignored, None),
_ => None,
}
}
@ -715,7 +721,8 @@ mod grid {
fn tick(
&mut self,
amount: usize,
) -> Option<impl Future<Output = Result<Life, TickError>>> {
) -> Option<impl Future<Output = Result<Life, TickError>> + use<>>
{
if self.is_ticking {
return None;
}

View file

@ -2,7 +2,7 @@
name = "geometry"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -47,10 +47,10 @@ mod rainbow {
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
use iced::advanced::Renderer as _;
use iced::advanced::graphics::mesh::{
self, Mesh, Renderer as _, SolidVertex2D,
};
use iced::advanced::Renderer as _;
let bounds = layout.bounds();
@ -145,15 +145,15 @@ mod rainbow {
}
}
impl<'a, Message> From<Rainbow> for Element<'a, Message> {
impl<Message> From<Rainbow> for Element<'_, Message> {
fn from(rainbow: Rainbow) -> Self {
Self::new(rainbow)
}
}
}
use iced::widget::{column, container, scrollable};
use iced::{Element, Length};
use iced::Element;
use iced::widget::{center_x, center_y, column, scrollable};
use rainbow::rainbow;
pub fn main() -> iced::Result {
@ -176,7 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> {
.spacing(20)
.max_width(500);
let scrollable = scrollable(container(content).center_x(Length::Fill));
let scrollable = scrollable(center_x(content));
container(scrollable).center_y(Length::Fill).into()
center_y(scrollable).into()
}

View file

@ -1,7 +1,7 @@
[package]
name = "gradient"
version = "0.1.0"
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,14 +1,14 @@
use iced::gradient;
use iced::program;
use iced::theme;
use iced::widget::{
checkbox, column, container, horizontal_space, row, slider, text,
};
use iced::{Alignment, Color, Element, Length, Radians, Theme};
use iced::{Center, Color, Element, Fill, Radians, Theme, color};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Gradient - Iced", Gradient::update, Gradient::view)
iced::application("Gradient - Iced", Gradient::update, Gradient::view)
.style(Gradient::style)
.transparent(true)
.run()
@ -34,7 +34,7 @@ impl Gradient {
fn new() -> Self {
Self {
start: Color::WHITE,
end: Color::new(0.0, 0.0, 1.0, 1.0),
end: color!(0x0000ff),
angle: Radians(0.0),
transparent: false,
}
@ -67,8 +67,8 @@ impl Gradient {
gradient.into()
})
.width(Length::Fill)
.height(Length::Fill);
.width(Fill)
.height(Fill);
let angle_picker = row![
text("Angle").width(64),
@ -77,7 +77,7 @@ impl Gradient {
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center);
.align_y(Center);
let transparency_toggle = iced::widget::Container::new(
checkbox("Transparent window", transparent)
@ -95,16 +95,14 @@ impl Gradient {
.into()
}
fn style(&self, theme: &Theme) -> program::Appearance {
use program::DefaultStyle;
fn style(&self, theme: &Theme) -> theme::Style {
if self.transparent {
program::Appearance {
theme::Style {
background_color: Color::TRANSPARENT,
text_color: theme.palette().text,
}
} else {
Theme::default_style(theme)
theme::default(theme)
}
}
}
@ -129,6 +127,6 @@ fn color_picker(label: &str, color: Color) -> Element<'_, Color> {
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center)
.align_y(Center)
.into()
}

View file

@ -2,13 +2,15 @@
name = "integration"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced_winit.workspace = true
iced_wgpu.workspace = true
iced_widget.workspace = true
iced_widget.features = ["wgpu"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"

View file

@ -1,8 +1,6 @@
use iced_wgpu::Renderer;
use iced_widget::{column, container, row, slider, text, text_input};
use iced_winit::core::alignment;
use iced_winit::core::{Color, Element, Length, Theme};
use iced_winit::runtime::{Command, Program};
use iced_widget::{bottom, column, row, slider, text, text_input};
use iced_winit::core::{Color, Element, Theme};
pub struct Controls {
background_color: Color,
@ -28,12 +26,8 @@ impl Controls {
}
}
impl Program for Controls {
type Theme = Theme;
type Message = Message;
type Renderer = Renderer;
fn update(&mut self, message: Message) -> Command<Message> {
impl Controls {
pub fn update(&mut self, message: Message) {
match message {
Message::BackgroundColorChanged(color) => {
self.background_color = color;
@ -42,11 +36,9 @@ impl Program for Controls {
self.input = input;
}
}
Command::none()
}
fn view(&self) -> Element<Message, Theme, Renderer> {
pub fn view(&self) -> Element<Message, Theme, Renderer> {
let background_color = self.background_color;
let sliders = row![
@ -75,21 +67,17 @@ impl Program for Controls {
.width(500)
.spacing(20);
container(
bottom(
column![
text("Background color").color(Color::WHITE),
text(format!("{background_color:?}"))
.size(14)
.color(Color::WHITE),
text_input("Placeholder", &self.input)
.on_input(Message::InputChanged),
text!("{background_color:?}").size(14).color(Color::WHITE),
sliders,
text_input("Type something...", &self.input)
.on_input(Message::InputChanged),
]
.spacing(10),
)
.padding(10)
.height(Length::Fill)
.align_y(alignment::Vertical::Bottom)
.into()
}
}

View file

@ -5,16 +5,17 @@ use controls::Controls;
use scene::Scene;
use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Engine, Renderer};
use iced_wgpu::{Engine, Renderer, wgpu};
use iced_winit::Clipboard;
use iced_winit::conversion;
use iced_winit::core::mouse;
use iced_winit::core::renderer;
use iced_winit::core::time::Instant;
use iced_winit::core::window;
use iced_winit::core::{Color, Font, Pixels, Size, Theme};
use iced_winit::core::{Event, Font, Pixels, Size, Theme};
use iced_winit::futures;
use iced_winit::runtime::program;
use iced_winit::runtime::user_interface::{self, UserInterface};
use iced_winit::winit;
use iced_winit::Clipboard;
use winit::{
event::WindowEvent,
@ -42,8 +43,10 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
engine: Engine,
renderer: Renderer,
scene: Scene,
state: program::State<Controls>,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
controls: Controls,
events: Vec<Event>,
cursor: mouse::Cursor,
cache: user_interface::Cache,
clipboard: Clipboard,
viewport: Viewport,
modifiers: ModifiersState,
@ -67,7 +70,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
Size::new(physical_size.width, physical_size.height),
window.scale_factor(),
);
let clipboard = Clipboard::connect(&window);
let clipboard = Clipboard::connect(window.clone());
let backend =
wgpu::util::backend_bits_from_env().unwrap_or_default();
@ -101,6 +104,8 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
required_features: adapter_features
& wgpu::Features::default(),
required_limits: wgpu::Limits::default(),
memory_hints:
wgpu::MemoryHints::MemoryUsage,
},
None,
)
@ -144,19 +149,13 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
// Initialize iced
let engine =
Engine::new(&adapter, &device, &queue, format, None);
let mut renderer = Renderer::new(
let renderer = Renderer::new(
&device,
&engine,
Font::default(),
Pixels::from(16),
);
let state = program::State::new(
controls,
viewport.logical_size(),
&mut renderer,
);
// You should change this if you want to render continuously
event_loop.set_control_flow(ControlFlow::Wait);
@ -169,9 +168,11 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
engine,
renderer,
scene,
state,
cursor_position: None,
controls,
events: Vec::new(),
cursor: mouse::Cursor::Unavailable,
modifiers: ModifiersState::default(),
cache: user_interface::Cache::new(),
clipboard,
viewport,
resized: false,
@ -194,11 +195,13 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
engine,
renderer,
scene,
state,
controls,
events,
viewport,
cursor_position,
cursor,
modifiers,
clipboard,
cache,
resized,
} = self
else {
@ -238,8 +241,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
&wgpu::CommandEncoderDescriptor { label: None },
);
let program = state.program();
let view = frame.texture.create_view(
&wgpu::TextureViewDescriptor::default(),
);
@ -249,7 +250,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
let mut render_pass = Scene::clear(
&view,
&mut encoder,
program.background_color(),
controls.background_color(),
);
// Draw the scene
@ -257,6 +258,33 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
}
// And then iced on top
let mut interface = UserInterface::build(
controls.view(),
viewport.logical_size(),
std::mem::take(cache),
renderer,
);
let _ = interface.update(
&[Event::Window(
window::Event::RedrawRequested(
Instant::now(),
),
)],
*cursor,
renderer,
clipboard,
&mut Vec::new(),
);
let mouse_interaction = interface.draw(
renderer,
&Theme::Dark,
&renderer::Style::default(),
*cursor,
);
*cache = interface.into_cache();
renderer.present(
engine,
device,
@ -273,17 +301,15 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
frame.present();
// Update the mouse cursor
window.set_cursor(
iced_winit::conversion::mouse_interaction(
state.mouse_interaction(),
),
);
window.set_cursor(conversion::mouse_interaction(
mouse_interaction,
));
}
Err(error) => match error {
wgpu::SurfaceError::OutOfMemory => {
panic!(
"Swapchain error: {error}. \
Rendering cannot continue."
Rendering cannot continue."
)
}
_ => {
@ -294,7 +320,11 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
}
}
WindowEvent::CursorMoved { position, .. } => {
*cursor_position = Some(position);
*cursor =
mouse::Cursor::Available(conversion::cursor_position(
position,
viewport.scale_factor(),
));
}
WindowEvent::ModifiersChanged(new_modifiers) => {
*modifiers = new_modifiers.state();
@ -309,37 +339,42 @@ pub fn main() -> Result<(), winit::error::EventLoopError> {
}
// Map window event to iced event
if let Some(event) = iced_winit::conversion::window_event(
window::Id::MAIN,
if let Some(event) = conversion::window_event(
event,
window.scale_factor(),
*modifiers,
) {
state.queue_event(event);
events.push(event);
}
// If there are events pending
if !state.is_queue_empty() {
// We update iced
let _ = state.update(
if !events.is_empty() {
// We process them
let mut interface = UserInterface::build(
controls.view(),
viewport.logical_size(),
cursor_position
.map(|p| {
conversion::cursor_position(
p,
viewport.scale_factor(),
)
})
.map(mouse::Cursor::Available)
.unwrap_or(mouse::Cursor::Unavailable),
std::mem::take(cache),
renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
clipboard,
);
let mut messages = Vec::new();
let _ = interface.update(
events,
*cursor,
renderer,
clipboard,
&mut messages,
);
events.clear();
*cache = interface.into_cache();
// update our UI with any messages
for message in messages {
controls.update(message);
}
// and request a redraw
window.request_redraw();
}

View file

@ -72,12 +72,13 @@ fn build_pipeline(
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs_module,
entry_point: "main",
entry_point: Some("main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &fs_module,
entry_point: "main",
entry_point: Some("main"),
targets: &[Some(wgpu::ColorTargetState {
format: texture_format,
blend: Some(wgpu::BlendState {
@ -86,6 +87,7 @@ fn build_pipeline(
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
@ -99,5 +101,6 @@ fn build_pipeline(
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
})
}

View file

@ -2,7 +2,7 @@
name = "layout"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,16 +1,18 @@
use iced::border;
use iced::keyboard;
use iced::mouse;
use iced::widget::{
button, canvas, center, checkbox, column, container, horizontal_space,
pick_list, row, scrollable, text,
button, canvas, center, center_y, checkbox, column, container,
horizontal_rule, horizontal_space, pick_list, pin, row, scrollable, stack,
text, vertical_rule,
};
use iced::{
color, Alignment, Element, Font, Length, Point, Rectangle, Renderer,
Subscription, Theme,
Center, Element, Fill, Font, Length, Point, Rectangle, Renderer, Shrink,
Subscription, Theme, color,
};
pub fn main() -> iced::Result {
iced::program(Layout::title, Layout::update, Layout::view)
iced::application(Layout::title, Layout::update, Layout::view)
.subscription(Layout::subscription)
.theme(Layout::theme)
.run()
@ -74,7 +76,7 @@ impl Layout {
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected),
]
.spacing(20)
.align_items(Alignment::Center);
.align_y(Center);
let example = center(if self.explain {
self.example.view().explain(color!(0x0000ff))
@ -85,7 +87,7 @@ impl Layout {
let palette = theme.extended_palette();
container::Style::default()
.with_border(palette.background.strong.color, 4.0)
.border(border::color(palette.background.strong.color).width(4))
})
.padding(4);
@ -146,6 +148,14 @@ impl Example {
title: "Application",
view: application,
},
Self {
title: "Quotes",
view: quotes,
},
Self {
title: "Pinning",
view: pinning,
},
];
fn is_first(self) -> bool {
@ -234,45 +244,92 @@ fn application<'a>() -> Element<'a, Message> {
square(40),
]
.padding(10)
.align_items(Alignment::Center),
.align_y(Center),
)
.style(|theme| {
let palette = theme.extended_palette();
container::Style::default()
.with_border(palette.background.strong.color, 1)
.border(border::color(palette.background.strong.color).width(1))
});
let sidebar = container(
let sidebar = center_y(
column!["Sidebar!", square(50), square(50)]
.spacing(40)
.padding(10)
.width(200)
.align_items(Alignment::Center),
.align_x(Center),
)
.style(container::rounded_box)
.center_y(Length::Fill);
.style(container::rounded_box);
let content = container(
scrollable(
column![
"Content!",
square(400),
square(200),
square(400),
row((1..10).map(|i| square(if i % 2 == 0 { 80 } else { 160 })))
.spacing(20)
.align_y(Center)
.wrap(),
"The end"
]
.spacing(40)
.align_items(Alignment::Center)
.width(Length::Fill),
.align_x(Center)
.width(Fill),
)
.height(Length::Fill),
.height(Fill),
)
.padding(10);
column![header, row![sidebar, content]].into()
}
fn quotes<'a>() -> Element<'a, Message> {
fn quote<'a>(
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![vertical_rule(2), content.into()]
.spacing(10)
.height(Shrink)
.into()
}
fn reply<'a>(
original: impl Into<Element<'a, Message>>,
reply: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
column![quote(original), reply.into()].spacing(10).into()
}
column![
reply(
reply("This is the original message", "This is a reply"),
"This is another reply",
),
horizontal_rule(1),
"A separator ↑",
]
.width(Shrink)
.spacing(10)
.into()
}
fn pinning<'a>() -> Element<'a, Message> {
column![
"The pin widget can be used to position a widget \
at some fixed coordinates inside some other widget.",
stack![
container(pin("• (50, 50)").x(50).y(50))
.width(500)
.height(500)
.style(container::bordered_box),
pin("• (300, 300)").x(300).y(300),
]
]
.align_x(Center)
.spacing(10)
.into()
}
fn square<'a>(size: impl Into<Length> + Copy) -> Element<'a, Message> {
struct Square;

View file

@ -2,7 +2,7 @@
name = "lazy"
version = "0.1.0"
authors = ["Nick Senger <dev@nsenger.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -2,7 +2,7 @@ use iced::widget::{
button, column, horizontal_space, lazy, pick_list, row, scrollable, text,
text_input,
};
use iced::{Element, Length};
use iced::{Element, Fill};
use std::collections::HashSet;
use std::hash::Hash;
@ -187,12 +187,12 @@ impl App {
});
column![
scrollable(options).height(Length::Fill),
scrollable(options).height(Fill),
row![
text_input("Add a new option", &self.input)
.on_input(Message::InputChanged)
.on_submit(Message::AddItem(self.input.clone())),
button(text(format!("Toggle Order ({})", self.order)))
button(text!("Toggle Order ({})", self.order))
.on_press(Message::ToggleOrder)
]
.spacing(10)

View file

@ -2,7 +2,7 @@
name = "loading_spinners"
version = "0.1.0"
authors = ["Nick Senger <dev@nsenger.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
@ -10,4 +10,3 @@ iced.workspace = true
iced.features = ["advanced", "canvas"]
lyon_algorithms = "1.0"
once_cell.workspace = true

View file

@ -3,11 +3,10 @@ use iced::advanced::layout;
use iced::advanced::renderer;
use iced::advanced::widget::tree::{self, Tree};
use iced::advanced::{self, Clipboard, Layout, Shell, Widget};
use iced::event;
use iced::mouse;
use iced::time::Instant;
use iced::widget::canvas;
use iced::window::{self, RedrawRequest};
use iced::window;
use iced::{
Background, Color, Element, Event, Length, Radians, Rectangle, Renderer,
Size, Vector,
@ -89,7 +88,7 @@ where
}
}
impl<'a, Theme> Default for Circular<'a, Theme>
impl<Theme> Default for Circular<'_, Theme>
where
Theme: StyleSheet,
{
@ -139,8 +138,8 @@ impl Animation {
progress: 0.0,
rotation: rotation.wrapping_add(
BASE_ROTATION_SPEED.wrapping_add(
(f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::MAX)
as u32,
(f64::from(WRAP_ANGLE / (2.0 * Radians::PI))
* u32::MAX as f64) as u32,
),
),
last: now,
@ -262,31 +261,29 @@ where
layout::atomic(limits, self.size, self.size)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
if let Event::Window(window::Event::RedrawRequested(now)) = event {
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.rotation_duration,
now,
*now,
);
state.cache.clear();
shell.request_redraw(RedrawRequest::NextFrame);
shell.request_redraw();
}
event::Status::Ignored
}
fn draw(

View file

@ -1,41 +1,42 @@
use iced::Point;
use lyon_algorithms::measure::PathMeasurements;
use lyon_algorithms::path::{builder::NoAttributes, path::BuilderImpl, Path};
use once_cell::sync::Lazy;
use lyon_algorithms::path::{Path, builder::NoAttributes, path::BuilderImpl};
pub static EMPHASIZED: Lazy<Easing> = Lazy::new(|| {
use std::sync::LazyLock;
pub static EMPHASIZED: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4])
.cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0])
.build()
});
pub static EMPHASIZED_DECELERATE: Lazy<Easing> = Lazy::new(|| {
pub static EMPHASIZED_DECELERATE: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0])
.build()
});
pub static EMPHASIZED_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
pub static EMPHASIZED_ACCELERATE: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0])
.build()
});
pub static STANDARD: Lazy<Easing> = Lazy::new(|| {
pub static STANDARD: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0])
.build()
});
pub static STANDARD_DECELERATE: Lazy<Easing> = Lazy::new(|| {
pub static STANDARD_DECELERATE: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0])
.build()
});
pub static STANDARD_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
pub static STANDARD_ACCELERATE: LazyLock<Easing> = LazyLock::new(|| {
Easing::builder()
.cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0])
.build()
@ -119,10 +120,7 @@ impl Builder {
fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> {
let p: Point = p.into();
lyon_algorithms::geom::point(
p.x.min(1.0).max(0.0),
p.y.min(1.0).max(0.0),
)
lyon_algorithms::geom::point(p.x.clamp(0.0, 1.0), p.y.clamp(0.0, 1.0))
}
}

View file

@ -3,10 +3,9 @@ use iced::advanced::layout;
use iced::advanced::renderer::{self, Quad};
use iced::advanced::widget::tree::{self, Tree};
use iced::advanced::{self, Clipboard, Layout, Shell, Widget};
use iced::event;
use iced::mouse;
use iced::time::Instant;
use iced::window::{self, RedrawRequest};
use iced::window;
use iced::{Background, Color, Element, Event, Length, Rectangle, Size};
use super::easing::{self, Easing};
@ -71,7 +70,7 @@ where
}
}
impl<'a, Theme> Default for Linear<'a, Theme>
impl<Theme> Default for Linear<'_, Theme>
where
Theme: StyleSheet,
{
@ -176,26 +175,24 @@ where
layout::atomic(limits, self.width, self.height)
}
fn on_event(
fn update(
&mut self,
tree: &mut Tree,
event: Event,
event: &Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
) {
let state = tree.state.downcast_mut::<State>();
if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
*state = state.timed_transition(self.cycle_duration, now);
if let Event::Window(window::Event::RedrawRequested(now)) = event {
*state = state.timed_transition(self.cycle_duration, *now);
shell.request_redraw(RedrawRequest::NextFrame);
shell.request_redraw();
}
event::Status::Ignored
}
fn draw(

View file

@ -1,5 +1,5 @@
use iced::widget::{center, column, row, slider, text};
use iced::Element;
use iced::{Center, Element};
use std::time::Duration;
@ -11,7 +11,7 @@ use circular::Circular;
use linear::Linear;
pub fn main() -> iced::Result {
iced::program(
iced::application(
"Loading Spinners - Iced",
LoadingSpinners::update,
LoadingSpinners::view,
@ -67,7 +67,7 @@ impl LoadingSpinners {
Duration::from_secs_f32(self.cycle_duration)
)
]
.align_items(iced::Alignment::Center)
.align_y(Center)
.spacing(20.0),
)
})
@ -81,9 +81,9 @@ impl LoadingSpinners {
Message::CycleDurationChanged(x / 100.0)
})
.width(200.0),
text(format!("{:.2}s", self.cycle_duration)),
text!("{:.2}s", self.cycle_duration),
]
.align_items(iced::Alignment::Center)
.align_y(Center)
.spacing(20.0),
),
)

View file

@ -2,7 +2,7 @@
name = "loupe"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,5 +1,5 @@
use iced::widget::{button, center, column, text};
use iced::{Alignment, Element};
use iced::{Center, Element};
use loupe::loupe;
@ -39,17 +39,17 @@ impl Loupe {
button("Decrement").on_press(Message::Decrement)
]
.padding(20)
.align_items(Alignment::Center),
.align_x(Center),
))
.into()
}
}
mod loupe {
use iced::advanced::Renderer as _;
use iced::advanced::layout::{self, Layout};
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
use iced::advanced::Renderer as _;
use iced::mouse;
use iced::{
Color, Element, Length, Rectangle, Renderer, Size, Theme,
@ -74,7 +74,7 @@ mod loupe {
content: Element<'a, Message>,
}
impl<'a, Message> Widget<Message, Theme, Renderer> for Loupe<'a, Message> {
impl<Message> Widget<Message, Theme, Renderer> for Loupe<'_, Message> {
fn tag(&self) -> widget::tree::Tag {
self.content.as_widget().tag()
}

View file

@ -0,0 +1,23 @@
[package]
name = "markdown"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["markdown", "highlighter", "image", "tokio", "debug"]
reqwest.version = "0.12"
reqwest.features = ["json"]
image.workspace = true
tokio.workspace = true
open = "5.3"
# Disabled to keep amount of build dependencies low
# This can be re-enabled on demand
# [build-dependencies]
# iced_fontello = "0.13"

View file

@ -0,0 +1,5 @@
pub fn main() {
// println!("cargo::rerun-if-changed=fonts/markdown-icons.toml");
// iced_fontello::build("fonts/markdown-icons.toml")
// .expect("Build icons font");
}

View file

@ -0,0 +1,4 @@
module = "icon"
[glyphs]
copy = "fontawesome-docs"

Binary file not shown.

View file

@ -0,0 +1,93 @@
# Overview
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:
```rust
#[derive(Default)]
struct Counter {
value: i32,
}
```
Next, we need to define the possible user interactions of our counter: the button presses. These interactions are our __messages__:
```rust
#[derive(Debug, Clone, Copy)]
pub enum Message {
Increment,
Decrement,
}
```
Now, let's show the actual counter by putting it all together in our __view logic__:
```rust
use iced::widget::{button, column, text, Column};
impl Counter {
pub fn view(&self) -> Column<Message> {
// We use a column: a simple vertical layout
column![
// The increment button. We tell it to produce an
// `Increment` message when pressed
button("+").on_press(Message::Increment),
// We show the value of the counter here
text(self.value).size(50),
// The decrement button. We tell it to produce a
// `Decrement` message when pressed
button("-").on_press(Message::Decrement),
]
}
}
```
Finally, we need to be able to react to any produced __messages__ and change our __state__ accordingly in our __update logic__:
```rust
impl Counter {
// ...
pub fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.value += 1;
}
Message::Decrement => {
self.value -= 1;
}
}
}
}
```
And that's everything! We just wrote a whole user interface. Let's run it:
```rust
fn main() -> iced::Result {
iced::run("A cool counter", Counter::update, Counter::view)
}
```
Iced will automatically:
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.
Read the [book], the [documentation], and the [examples] to learn more!
[book]: https://book.iced.rs/
[documentation]: https://docs.rs/iced/
[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples
[The Elm Architecture]: https://guide.elm-lang.org/architecture/

View file

@ -0,0 +1,15 @@
// Generated automatically by iced_fontello at build time.
// Do not edit manually. Source: ../fonts/markdown-icons.toml
// dcd2f0c969d603e2ee9237a4b70fa86b1a6e84d86f4689046d8fdd10440b06b9
use iced::Font;
use iced::widget::{Text, text};
pub const FONT: &[u8] = include_bytes!("../fonts/markdown-icons.ttf");
pub fn copy<'a>() -> Text<'a> {
icon("\u{F0C5}")
}
fn icon(codepoint: &str) -> Text<'_> {
text(codepoint).font(Font::with_name("markdown-icons"))
}

View file

@ -0,0 +1,362 @@
mod icon;
use iced::animation;
use iced::clipboard;
use iced::highlighter;
use iced::time::{self, Instant, milliseconds};
use iced::widget::{
self, button, center_x, container, horizontal_space, hover, image,
markdown, pop, right, row, scrollable, text_editor, toggler,
};
use iced::window;
use iced::{
Animation, Element, Fill, Font, Function, Subscription, Task, Theme,
};
use std::collections::HashMap;
use std::io;
use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
.font(icon::FONT)
.subscription(Markdown::subscription)
.theme(Markdown::theme)
.run_with(Markdown::new)
}
struct Markdown {
content: markdown::Content,
raw: text_editor::Content,
images: HashMap<markdown::Url, Image>,
mode: Mode,
theme: Theme,
now: Instant,
}
enum Mode {
Preview,
Stream { pending: String },
}
enum Image {
Loading,
Ready {
handle: image::Handle,
fade_in: Animation<bool>,
},
#[allow(dead_code)]
Errored(Error),
}
#[derive(Debug, Clone)]
enum Message {
Edit(text_editor::Action),
Copy(String),
LinkClicked(markdown::Url),
ImageShown(markdown::Url),
ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
ToggleStream(bool),
NextToken,
Animate(Instant),
}
impl Markdown {
fn new() -> (Self, Task<Message>) {
const INITIAL_CONTENT: &str = include_str!("../overview.md");
(
Self {
content: markdown::Content::parse(INITIAL_CONTENT),
raw: text_editor::Content::with_text(INITIAL_CONTENT),
images: HashMap::new(),
mode: Mode::Preview,
theme: Theme::TokyoNight,
now: Instant::now(),
},
widget::focus_next(),
)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Edit(action) => {
let is_edit = action.is_edit();
self.raw.perform(action);
if is_edit {
self.content = markdown::Content::parse(&self.raw.text());
self.mode = Mode::Preview;
}
Task::none()
}
Message::Copy(content) => clipboard::write(content),
Message::LinkClicked(link) => {
let _ = open::that_in_background(link.to_string());
Task::none()
}
Message::ImageShown(url) => {
if self.images.contains_key(&url) {
return Task::none();
}
let _ = self.images.insert(url.clone(), Image::Loading);
Task::perform(
download_image(url.clone()),
Message::ImageDownloaded.with(url),
)
}
Message::ImageDownloaded(url, result) => {
let _ = self.images.insert(
url,
result
.map(|handle| Image::Ready {
handle,
fade_in: Animation::new(false)
.quick()
.easing(animation::Easing::EaseInOut)
.go(true),
})
.unwrap_or_else(Image::Errored),
);
Task::none()
}
Message::ToggleStream(enable_stream) => {
if enable_stream {
self.content = markdown::Content::new();
self.mode = Mode::Stream {
pending: self.raw.text(),
};
scrollable::snap_to(
"preview",
scrollable::RelativeOffset::END,
)
} else {
self.mode = Mode::Preview;
Task::none()
}
}
Message::NextToken => {
match &mut self.mode {
Mode::Preview => {}
Mode::Stream { pending } => {
if pending.is_empty() {
self.mode = Mode::Preview;
} else {
let mut tokens = pending.split(' ');
if let Some(token) = tokens.next() {
self.content.push_str(&format!("{token} "));
}
*pending = tokens.collect::<Vec<_>>().join(" ");
}
}
}
Task::none()
}
Message::Animate(now) => {
self.now = now;
Task::none()
}
}
}
fn view(&self) -> Element<Message> {
let editor = text_editor(&self.raw)
.placeholder("Type your Markdown here...")
.on_action(Message::Edit)
.height(Fill)
.padding(10)
.font(Font::MONOSPACE)
.highlight("markdown", highlighter::Theme::Base16Ocean);
let preview = markdown::view_with(
self.content.items(),
&self.theme,
&CustomViewer {
images: &self.images,
now: self.now,
},
);
row![
editor,
hover(
scrollable(preview)
.spacing(10)
.width(Fill)
.height(Fill)
.id("preview"),
right(
toggler(matches!(self.mode, Mode::Stream { .. }))
.label("Stream")
.on_toggle(Message::ToggleStream)
)
.padding([0, 20])
)
]
.spacing(10)
.padding(10)
.into()
}
fn theme(&self) -> Theme {
self.theme.clone()
}
fn subscription(&self) -> Subscription<Message> {
let listen_stream = match self.mode {
Mode::Preview => Subscription::none(),
Mode::Stream { .. } => {
time::every(milliseconds(10)).map(|_| Message::NextToken)
}
};
let animate = {
let is_animating = self.images.values().any(|image| match image {
Image::Ready { fade_in, .. } => fade_in.is_animating(self.now),
_ => false,
});
if is_animating {
window::frames().map(Message::Animate)
} else {
Subscription::none()
}
};
Subscription::batch([listen_stream, animate])
}
}
struct CustomViewer<'a> {
images: &'a HashMap<markdown::Url, Image>,
now: Instant,
}
impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> {
fn on_link_click(url: markdown::Url) -> Message {
Message::LinkClicked(url)
}
fn image(
&self,
_settings: markdown::Settings,
url: &'a markdown::Url,
_title: &'a str,
_alt: &markdown::Text,
) -> Element<'a, Message> {
if let Some(Image::Ready { handle, fade_in }) = self.images.get(url) {
center_x(
image(handle)
.opacity(fade_in.interpolate(0.0, 1.0, self.now))
.scale(fade_in.interpolate(1.2, 1.0, self.now)),
)
.into()
} else {
pop(horizontal_space())
.key(url.as_str())
.delay(milliseconds(500))
.on_show(|_size| Message::ImageShown(url.clone()))
.into()
}
}
fn code_block(
&self,
settings: markdown::Settings,
_language: Option<&'a str>,
code: &'a str,
lines: &'a [markdown::Text],
) -> Element<'a, Message> {
let code_block =
markdown::code_block(settings, lines, Message::LinkClicked);
let copy = button(icon::copy().size(12))
.padding(2)
.on_press_with(|| Message::Copy(code.to_owned()))
.style(button::text);
hover(
code_block,
right(container(copy).style(container::dark))
.padding(settings.spacing / 2),
)
}
}
async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> {
use std::io;
use tokio::task;
println!("Trying to download image: {url}");
let client = reqwest::Client::new();
let bytes = client
.get(url)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let image = task::spawn_blocking(move || {
Ok::<_, Error>(
::image::ImageReader::new(io::Cursor::new(bytes))
.with_guessed_format()?
.decode()?
.to_rgba8(),
)
})
.await??;
Ok(image::Handle::from_rgba(
image.width(),
image.height(),
image.into_raw(),
))
}
#[derive(Debug, Clone)]
pub enum Error {
RequestFailed(Arc<reqwest::Error>),
IOFailed(Arc<io::Error>),
JoinFailed(Arc<tokio::task::JoinError>),
ImageDecodingFailed(Arc<::image::ImageError>),
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::RequestFailed(Arc::new(error))
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IOFailed(Arc::new(error))
}
}
impl From<tokio::task::JoinError> for Error {
fn from(error: tokio::task::JoinError) -> Self {
Self::JoinFailed(Arc::new(error))
}
}
impl From<::image::ImageError> for Error {
fn from(error: ::image::ImageError) -> Self {
Self::ImageDecodingFailed(Arc::new(error))
}
}

View file

@ -2,7 +2,7 @@
name = "modal"
version = "0.1.0"
authors = ["tarkah <admin@tarkah.dev>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -5,12 +5,12 @@ use iced::widget::{
self, button, center, column, container, horizontal_space, mouse_area,
opaque, pick_list, row, stack, text, text_input,
};
use iced::{Alignment, Color, Command, Element, Length, Subscription};
use iced::{Bottom, Color, Element, Fill, Subscription, Task};
use std::fmt;
pub fn main() -> iced::Result {
iced::program("Modal - Iced", App::update, App::view)
iced::application("Modal - Iced", App::update, App::view)
.subscription(App::subscription)
.run()
}
@ -39,7 +39,7 @@ impl App {
event::listen().map(Message::Event)
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ShowModal => {
self.show_modal = true;
@ -47,26 +47,26 @@ impl App {
}
Message::HideModal => {
self.hide_modal();
Command::none()
Task::none()
}
Message::Email(email) => {
self.email = email;
Command::none()
Task::none()
}
Message::Password(password) => {
self.password = password;
Command::none()
Task::none()
}
Message::Plan(plan) => {
self.plan = plan;
Command::none()
Task::none()
}
Message::Submit => {
if !self.email.is_empty() && !self.password.is_empty() {
self.hide_modal();
}
Command::none()
Task::none()
}
Message::Event(event) => match event {
Event::Keyboard(keyboard::Event::KeyPressed {
@ -85,9 +85,9 @@ impl App {
..
}) => {
self.hide_modal();
Command::none()
Task::none()
}
_ => Command::none(),
_ => Task::none(),
},
}
}
@ -96,18 +96,17 @@ impl App {
let content = container(
column![
row![text("Top Left"), horizontal_space(), text("Top Right")]
.align_items(Alignment::Start)
.height(Length::Fill),
.height(Fill),
center(button(text("Show Modal")).on_press(Message::ShowModal)),
row![
text("Bottom Left"),
horizontal_space(),
text("Bottom Right")
]
.align_items(Alignment::End)
.height(Length::Fill),
.align_y(Bottom)
.height(Fill),
]
.height(Length::Fill),
.height(Fill),
)
.padding(10);
@ -202,19 +201,21 @@ where
{
stack![
base.into(),
mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
.into(),
),
..container::Style::default()
}
}))
.on_press(on_blur)
opaque(
mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
.into(),
),
..container::Style::default()
}
}))
.on_press(on_blur)
)
]
.into()
}

View file

@ -2,8 +2,8 @@
name = "multi_window"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug", "multi-window"] }
iced = { path = "../..", features = ["debug"] }

View file

@ -1,25 +1,24 @@
use iced::event;
use iced::executor;
use iced::multi_window::{self, Application};
use iced::widget::{
button, center, column, container, scrollable, text, text_input,
button, center, center_x, column, horizontal_space, scrollable, text,
text_input,
};
use iced::window;
use iced::{
Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
Vector,
Center, Element, Fill, Function, Subscription, Task, Theme, Vector,
};
use std::collections::HashMap;
use std::collections::BTreeMap;
fn main() -> iced::Result {
Example::run(Settings::default())
iced::daemon(Example::title, Example::update, Example::view)
.subscription(Example::subscription)
.theme(Example::theme)
.scale_factor(Example::scale_factor)
.run_with(Example::new)
}
#[derive(Default)]
struct Example {
windows: HashMap<window::Id, Window>,
next_window_pos: window::Position,
windows: BTreeMap<window::Id, Window>,
}
#[derive(Debug)]
@ -28,33 +27,27 @@ struct Window {
scale_input: String,
current_scale: f64,
theme: Theme,
input_id: iced::widget::text_input::Id,
}
#[derive(Debug, Clone)]
enum Message {
OpenWindow,
WindowOpened(window::Id),
WindowClosed(window::Id),
ScaleInputChanged(window::Id, String),
ScaleChanged(window::Id, String),
TitleChanged(window::Id, String),
CloseWindow(window::Id),
WindowOpened(window::Id, Option<Point>),
WindowClosed(window::Id),
NewWindow,
}
impl multi_window::Application for Example {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
impl Example {
fn new() -> (Self, Task<Message>) {
let (_id, open) = window::open(window::Settings::default());
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Example {
windows: HashMap::from([(window::Id::MAIN, Window::new(1))]),
next_window_pos: window::Position::Default,
Self {
windows: BTreeMap::new(),
},
Command::none(),
open.map(Message::WindowOpened),
)
}
@ -62,79 +55,94 @@ impl multi_window::Application for Example {
self.windows
.get(&window)
.map(|window| window.title.clone())
.unwrap_or("Example".to_string())
.unwrap_or_default()
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ScaleInputChanged(id, scale) => {
let window =
self.windows.get_mut(&id).expect("Window not found!");
window.scale_input = scale;
Message::OpenWindow => {
let Some(last_window) = self.windows.keys().last() else {
return Task::none();
};
Command::none()
window::get_position(*last_window)
.then(|last_position| {
let position = last_position.map_or(
window::Position::Default,
|last_position| {
window::Position::Specific(
last_position + Vector::new(20.0, 20.0),
)
},
);
let (_id, open) = window::open(window::Settings {
position,
..window::Settings::default()
});
open
})
.map(Message::WindowOpened)
}
Message::ScaleChanged(id, scale) => {
let window =
self.windows.get_mut(&id).expect("Window not found!");
Message::WindowOpened(id) => {
let window = Window::new(self.windows.len() + 1);
let focus_input = text_input::focus(format!("input-{id}"));
window.current_scale = scale
.parse::<f64>()
.unwrap_or(window.current_scale)
.clamp(0.5, 5.0);
self.windows.insert(id, window);
Command::none()
focus_input
}
Message::TitleChanged(id, title) => {
let window =
self.windows.get_mut(&id).expect("Window not found.");
window.title = title;
Command::none()
}
Message::CloseWindow(id) => window::close(id),
Message::WindowClosed(id) => {
self.windows.remove(&id);
Command::none()
}
Message::WindowOpened(id, position) => {
if let Some(position) = position {
self.next_window_pos = window::Position::Specific(
position + Vector::new(20.0, 20.0),
);
}
if let Some(window) = self.windows.get(&id) {
text_input::focus(window.input_id.clone())
if self.windows.is_empty() {
iced::exit()
} else {
Command::none()
Task::none()
}
}
Message::NewWindow => {
let count = self.windows.len() + 1;
Message::ScaleInputChanged(id, scale) => {
if let Some(window) = self.windows.get_mut(&id) {
window.scale_input = scale;
}
let (id, spawn_window) = window::spawn(window::Settings {
position: self.next_window_pos,
exit_on_close_request: count % 2 == 0,
..Default::default()
});
Task::none()
}
Message::ScaleChanged(id, scale) => {
if let Some(window) = self.windows.get_mut(&id) {
window.current_scale = scale
.parse::<f64>()
.unwrap_or(window.current_scale)
.clamp(0.5, 5.0);
}
self.windows.insert(id, Window::new(count));
Task::none()
}
Message::TitleChanged(id, title) => {
if let Some(window) = self.windows.get_mut(&id) {
window.title = title;
}
spawn_window
Task::none()
}
}
}
fn view(&self, window: window::Id) -> Element<Message> {
let content = self.windows.get(&window).unwrap().view(window);
center(content).into()
fn view(&self, window_id: window::Id) -> Element<Message> {
if let Some(window) = self.windows.get(&window_id) {
center(window.view(window_id)).into()
} else {
horizontal_space().into()
}
}
fn theme(&self, window: window::Id) -> Self::Theme {
self.windows.get(&window).unwrap().theme.clone()
fn theme(&self, window: window::Id) -> Theme {
if let Some(window) = self.windows.get(&window) {
window.theme.clone()
} else {
Theme::default()
}
}
fn scale_factor(&self, window: window::Id) -> f64 {
@ -144,23 +152,8 @@ impl multi_window::Application for Example {
.unwrap_or(1.0)
}
fn subscription(&self) -> Subscription<Self::Message> {
event::listen_with(|event, _| {
if let iced::Event::Window(id, window_event) = event {
match window_event {
window::Event::CloseRequested => {
Some(Message::CloseWindow(id))
}
window::Event::Opened { position, .. } => {
Some(Message::WindowOpened(id, position))
}
window::Event::Closed => Some(Message::WindowClosed(id)),
_ => None,
}
} else {
None
}
})
fn subscription(&self) -> Subscription<Message> {
window::close_events().map(Message::WindowClosed)
}
}
@ -170,12 +163,7 @@ impl Window {
title: format!("Window_{}", count),
scale_input: "1.0".to_string(),
current_scale: 1.0,
theme: if count % 2 == 0 {
Theme::Light
} else {
Theme::Dark
},
input_id: text_input::Id::unique(),
theme: Theme::ALL[count % Theme::ALL.len()].clone(),
}
}
@ -183,7 +171,7 @@ impl Window {
let scale_input = column![
text("Window scale factor:"),
text_input("Window Scale", &self.scale_input)
.on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
.on_input(Message::ScaleInputChanged.with(id))
.on_submit(Message::ScaleChanged(
id,
self.scale_input.to_string()
@ -193,20 +181,20 @@ impl Window {
let title_input = column![
text("Window title:"),
text_input("Window Title", &self.title)
.on_input(move |msg| { Message::TitleChanged(id, msg) })
.id(self.input_id.clone())
.on_input(Message::TitleChanged.with(id))
.id(format!("input-{id}"))
];
let new_window_button =
button(text("New Window")).on_press(Message::NewWindow);
button(text("New Window")).on_press(Message::OpenWindow);
let content = scrollable(
column![scale_input, title_input, new_window_button]
.spacing(50)
.width(Length::Fill)
.align_items(Alignment::Center),
.width(Fill)
.align_x(Center),
);
container(content).center_x(200).into()
center_x(content).width(200).into()
}
}

View file

@ -2,7 +2,7 @@
name = "multitouch"
version = "0.1.0"
authors = ["Artur Sapek <artur@kraken.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -3,17 +3,16 @@
//! computers like Microsoft Surface.
use iced::mouse;
use iced::touch;
use iced::widget::canvas::event;
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{self, Canvas, Geometry};
use iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme};
use iced::widget::canvas::{self, Canvas, Event, Geometry};
use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme};
use std::collections::HashMap;
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Multitouch - Iced", Multitouch::update, Multitouch::view)
iced::application("Multitouch - Iced", Multitouch::update, Multitouch::view)
.antialiasing(true)
.centered()
.run()
@ -46,10 +45,7 @@ impl Multitouch {
}
fn view(&self) -> Element<Message> {
Canvas::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
Canvas::new(self).width(Fill).height(Fill).into()
}
}
@ -59,25 +55,25 @@ impl canvas::Program<Message> for Multitouch {
fn update(
&self,
_state: &mut Self::State,
event: event::Event,
event: &Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
match event {
event::Event::Touch(touch_event) => match touch_event {
) -> Option<canvas::Action<Message>> {
let message = match event.clone() {
Event::Touch(
touch::Event::FingerPressed { id, position }
| touch::Event::FingerMoved { id, position } => (
event::Status::Captured,
Some(Message::FingerPressed { id, position }),
),
| touch::Event::FingerMoved { id, position },
) => Some(Message::FingerPressed { id, position }),
Event::Touch(
touch::Event::FingerLifted { id, .. }
| touch::Event::FingerLost { id, .. } => (
event::Status::Captured,
Some(Message::FingerLifted { id }),
),
},
_ => (event::Status::Ignored, None),
}
| touch::Event::FingerLost { id, .. },
) => Some(Message::FingerLifted { id }),
_ => None,
};
message
.map(canvas::Action::publish)
.map(canvas::Action::and_capture)
}
fn draw(
@ -129,7 +125,7 @@ impl canvas::Program<Message> for Multitouch {
let path = builder.build();
let color_r = (10 % zone.0) as f32 / 20.0;
let color_r = (10 % (zone.0 + 1)) as f32 / 20.0;
let color_g = (10 % (zone.0 + 8)) as f32 / 20.0;
let color_b = (10 % (zone.0 + 3)) as f32 / 20.0;

View file

@ -2,7 +2,7 @@
name = "pane_grid"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,13 +1,12 @@
use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{
button, column, container, responsive, row, scrollable, text,
button, center_y, column, container, responsive, row, scrollable, text,
};
use iced::{Color, Element, Length, Size, Subscription};
use iced::{Center, Color, Element, Fill, Size, Subscription};
pub fn main() -> iced::Result {
iced::program("Pane Grid - Iced", Example::update, Example::view)
iced::application("Pane Grid - Iced", Example::update, Example::view)
.subscription(Example::subscription)
.run()
}
@ -155,11 +154,23 @@ impl Example {
.spacing(5);
let title_bar = pane_grid::TitleBar::new(title)
.controls(view_controls(
id,
total_panes,
pane.is_pinned,
is_maximized,
.controls(pane_grid::Controls::dynamic(
view_controls(
id,
total_panes,
pane.is_pinned,
is_maximized,
),
button(text("X").size(14))
.style(button::danger)
.padding(3)
.on_press_maybe(
if total_panes > 1 && !pane.is_pinned {
Some(Message::Close(id))
} else {
None
},
),
))
.padding(10)
.style(if is_focused {
@ -178,18 +189,14 @@ impl Example {
style::pane_active
})
})
.width(Length::Fill)
.height(Length::Fill)
.width(Fill)
.height(Fill)
.spacing(10)
.on_click(Message::Clicked)
.on_drag(Message::Dragged)
.on_resize(10, Message::Resized);
container(pane_grid)
.width(Length::Fill)
.height(Length::Fill)
.padding(10)
.into()
container(pane_grid).padding(10).into()
}
}
@ -255,15 +262,10 @@ fn view_content<'a>(
size: Size,
) -> Element<'a, Message> {
let button = |label, message| {
button(
text(label)
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.size(16),
)
.width(Length::Fill)
.padding(8)
.on_press(message)
button(text(label).width(Fill).align_x(Center).size(16))
.width(Fill)
.padding(8)
.on_press(message)
};
let controls = column![
@ -284,17 +286,12 @@ fn view_content<'a>(
.spacing(5)
.max_width(160);
let content = column![
text(format!("{}x{}", size.width, size.height)).size(24),
controls,
]
.spacing(10)
.align_items(Alignment::Center);
let content =
column![text!("{}x{}", size.width, size.height).size(24), controls,]
.spacing(10)
.align_x(Center);
container(scrollable(content))
.center_y(Length::Fill)
.padding(5)
.into()
center_y(scrollable(content)).padding(5).into()
}
fn view_controls<'a>(

View file

@ -2,7 +2,7 @@
name = "pick_list"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,5 +1,5 @@
use iced::widget::{column, pick_list, scrollable, vertical_space};
use iced::{Alignment, Element, Length};
use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Pick List - Iced", Example::update, Example::view)
@ -38,8 +38,8 @@ impl Example {
pick_list,
vertical_space().height(600),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.width(Fill)
.align_x(Center)
.spacing(10);
scrollable(content).into()

View file

@ -2,7 +2,7 @@
name = "pokedex"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
@ -16,9 +16,8 @@ version = "1.0"
features = ["derive"]
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["json", "rustls-tls"]
version = "0.12"
features = ["json"]
[dependencies.rand]
version = "0.8"

View file

@ -1,20 +1,16 @@
use iced::futures;
use iced::widget::{self, center, column, image, row, text};
use iced::{Alignment, Command, Element, Length};
use iced::{Center, Element, Fill, Right, Task};
pub fn main() -> iced::Result {
iced::program(Pokedex::title, Pokedex::update, Pokedex::view)
.load(Pokedex::search)
.run()
iced::application(Pokedex::title, Pokedex::update, Pokedex::view)
.run_with(Pokedex::new)
}
#[derive(Debug, Default)]
#[derive(Debug)]
enum Pokedex {
#[default]
Loading,
Loaded {
pokemon: Pokemon,
},
Loaded { pokemon: Pokemon },
Errored,
}
@ -25,8 +21,12 @@ enum Message {
}
impl Pokedex {
fn search() -> Command<Message> {
Command::perform(Pokemon::search(), Message::PokemonFound)
fn new() -> (Self, Task<Message>) {
(Self::Loading, Self::search())
}
fn search() -> Task<Message> {
Task::perform(Pokemon::search(), Message::PokemonFound)
}
fn title(&self) -> String {
@ -39,20 +39,20 @@ impl Pokedex {
format!("{subtitle} - Pokédex")
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::PokemonFound(Ok(pokemon)) => {
*self = Pokedex::Loaded { pokemon };
Command::none()
Task::none()
}
Message::PokemonFound(Err(_error)) => {
*self = Pokedex::Errored;
Command::none()
Task::none()
}
Message::Search => match self {
Pokedex::Loading => Command::none(),
Pokedex::Loading => Task::none(),
_ => {
*self = Pokedex::Loading;
@ -63,10 +63,9 @@ impl Pokedex {
}
fn view(&self) -> Element<Message> {
let content = match self {
let content: Element<_> = match self {
Pokedex::Loading => {
column![text("Searching for Pokémon...").size(40),]
.width(Length::Shrink)
text("Searching for Pokémon...").size(40).into()
}
Pokedex::Loaded { pokemon } => column![
pokemon.view(),
@ -74,13 +73,15 @@ impl Pokedex {
]
.max_width(500)
.spacing(20)
.align_items(Alignment::End),
.align_x(Right)
.into(),
Pokedex::Errored => column![
text("Whoops! Something went wrong...").size(40),
button("Try again").on_press(Message::Search)
]
.spacing(20)
.align_items(Alignment::End),
.align_x(Right)
.into(),
};
center(content).into()
@ -103,19 +104,17 @@ impl Pokemon {
image::viewer(self.image.clone()),
column![
row![
text(&self.name).size(30).width(Length::Fill),
text(format!("#{}", self.number))
.size(20)
.color([0.5, 0.5, 0.5]),
text(&self.name).size(30).width(Fill),
text!("#{}", self.number).size(20).color([0.5, 0.5, 0.5]),
]
.align_items(Alignment::Center)
.align_y(Center)
.spacing(20),
self.description.as_ref(),
]
.spacing(20),
]
.spacing(20)
.align_items(Alignment::Center)
.align_y(Center)
.into()
}

View file

@ -2,7 +2,7 @@
name = "progress_bar"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,5 +1,8 @@
use iced::widget::{column, progress_bar, slider};
use iced::Element;
use iced::widget::{
center, center_x, checkbox, column, progress_bar, row, slider,
vertical_slider,
};
pub fn main() -> iced::Result {
iced::run("Progress Bar - Iced", Progress::update, Progress::view)
@ -8,25 +11,58 @@ pub fn main() -> iced::Result {
#[derive(Default)]
struct Progress {
value: f32,
is_vertical: bool,
}
#[derive(Debug, Clone, Copy)]
enum Message {
SliderChanged(f32),
ToggleVertical(bool),
}
impl Progress {
fn update(&mut self, message: Message) {
match message {
Message::SliderChanged(x) => self.value = x,
Message::ToggleVertical(is_vertical) => {
self.is_vertical = is_vertical
}
}
}
fn view(&self) -> Element<Message> {
let bar = progress_bar(0.0..=100.0, self.value);
column![
progress_bar(0.0..=100.0, self.value),
slider(0.0..=100.0, self.value, Message::SliderChanged).step(0.01)
if self.is_vertical {
center(
row![
bar.vertical(),
vertical_slider(
0.0..=100.0,
self.value,
Message::SliderChanged
)
.step(0.01)
]
.spacing(20),
)
} else {
center(
column![
bar,
slider(0.0..=100.0, self.value, Message::SliderChanged)
.step(0.01)
]
.spacing(20),
)
},
center_x(
checkbox("Vertical", self.is_vertical)
.on_toggle(Message::ToggleVertical)
),
]
.spacing(20)
.padding(20)
.into()
}

View file

@ -2,7 +2,7 @@
name = "qr_code"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,8 +1,12 @@
use iced::widget::{center, column, pick_list, qr_code, row, text, text_input};
use iced::{Alignment, Element, Theme};
use iced::widget::{
center, column, pick_list, qr_code, row, slider, text, text_input, toggler,
};
use iced::{Center, Element, Theme};
use std::ops::RangeInclusive;
pub fn main() -> iced::Result {
iced::program(
iced::application(
"QR Code Generator - Iced",
QRGenerator::update,
QRGenerator::view,
@ -15,16 +19,21 @@ pub fn main() -> iced::Result {
struct QRGenerator {
data: String,
qr_code: Option<qr_code::Data>,
total_size: Option<f32>,
theme: Theme,
}
#[derive(Debug, Clone)]
enum Message {
DataChanged(String),
ToggleTotalSize(bool),
TotalSizeChanged(f32),
ThemeChanged(Theme),
}
impl QRGenerator {
const SIZE_RANGE: RangeInclusive<f32> = 200.0..=400.0;
fn update(&mut self, message: Message) {
match message {
Message::DataChanged(mut data) => {
@ -38,6 +47,16 @@ impl QRGenerator {
self.data = data;
}
Message::ToggleTotalSize(enabled) => {
self.total_size = enabled.then_some(
Self::SIZE_RANGE.start()
+ (Self::SIZE_RANGE.end() - Self::SIZE_RANGE.start())
/ 2.0,
);
}
Message::TotalSizeChanged(total_size) => {
self.total_size = Some(total_size);
}
Message::ThemeChanged(theme) => {
self.theme = theme;
}
@ -53,22 +72,37 @@ impl QRGenerator {
.size(30)
.padding(15);
let toggle_total_size = toggler(self.total_size.is_some())
.on_toggle(Message::ToggleTotalSize)
.label("Limit Total Size");
let choose_theme = row![
text("Theme:"),
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,)
]
.spacing(10)
.align_items(Alignment::Center);
.align_y(Center);
let content = column![title, input, choose_theme]
.push_maybe(
self.qr_code
.as_ref()
.map(|data| qr_code(data).cell_size(10)),
)
.width(700)
.spacing(20)
.align_items(Alignment::Center);
let content = column![
title,
input,
row![toggle_total_size, choose_theme]
.spacing(20)
.align_y(Center)
]
.push_maybe(self.total_size.map(|total_size| {
slider(Self::SIZE_RANGE, total_size, Message::TotalSizeChanged)
}))
.push_maybe(self.qr_code.as_ref().map(|data| {
if let Some(total_size) = self.total_size {
qr_code(data).total_size(total_size)
} else {
qr_code(data).cell_size(10.0)
}
}))
.width(700)
.spacing(20)
.align_x(Center);
center(content).padding(20).into()
}

View file

@ -2,7 +2,7 @@
name = "screenshot"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]

View file

@ -1,10 +1,12 @@
use iced::alignment;
use iced::keyboard;
use iced::widget::{button, column, container, image, row, text, text_input};
use iced::widget::{
button, center_y, column, container, image, row, text, text_input,
};
use iced::window;
use iced::window::screenshot::{self, Screenshot};
use iced::{
Alignment, Command, ContentFit, Element, Length, Rectangle, Subscription,
Center, ContentFit, Element, Fill, FillPortion, Rectangle, Subscription,
Task,
};
use ::image as img;
@ -13,14 +15,14 @@ use ::image::ColorType;
fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Screenshot - Iced", Example::update, Example::view)
iced::application("Screenshot - Iced", Example::update, Example::view)
.subscription(Example::subscription)
.run()
}
#[derive(Default)]
struct Example {
screenshot: Option<Screenshot>,
screenshot: Option<(Screenshot, image::Handle)>,
saved_png_path: Option<Result<String, PngError>>,
png_saving: bool,
crop_error: Option<screenshot::CropError>,
@ -34,7 +36,7 @@ struct Example {
enum Message {
Crop,
Screenshot,
ScreenshotData(Screenshot),
Screenshotted(Screenshot),
Png,
PngSaved(Result<String, PngError>),
XInputChanged(Option<u32>),
@ -44,22 +46,28 @@ enum Message {
}
impl Example {
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Screenshot => {
return iced::window::screenshot(
window::Id::MAIN,
Message::ScreenshotData,
);
return window::get_latest()
.and_then(window::screenshot)
.map(Message::Screenshotted);
}
Message::ScreenshotData(screenshot) => {
self.screenshot = Some(screenshot);
Message::Screenshotted(screenshot) => {
self.screenshot = Some((
screenshot.clone(),
image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.bytes,
),
));
}
Message::Png => {
if let Some(screenshot) = &self.screenshot {
if let Some((screenshot, _handle)) = &self.screenshot {
self.png_saving = true;
return Command::perform(
return Task::perform(
save_to_png(screenshot.clone()),
Message::PngSaved,
);
@ -82,7 +90,7 @@ impl Example {
self.height_input_value = new_value;
}
Message::Crop => {
if let Some(screenshot) = &self.screenshot {
if let Some((screenshot, _handle)) = &self.screenshot {
let cropped = screenshot.crop(Rectangle::<u32> {
x: self.x_input_value.unwrap_or(0),
y: self.y_input_value.unwrap_or(0),
@ -92,7 +100,14 @@ impl Example {
match cropped {
Ok(screenshot) => {
self.screenshot = Some(screenshot);
self.screenshot = Some((
screenshot.clone(),
image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.bytes,
),
));
self.crop_error = None;
}
Err(crop_error) => {
@ -103,67 +118,55 @@ impl Example {
}
}
Command::none()
Task::none()
}
fn view(&self) -> Element<'_, Message> {
let image: Element<Message> = if let Some(screenshot) = &self.screenshot
{
image(image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.clone(),
))
.content_fit(ContentFit::Contain)
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
text("Press the button to take a screenshot!").into()
};
let image: Element<Message> =
if let Some((_screenshot, handle)) = &self.screenshot {
image(handle)
.content_fit(ContentFit::Contain)
.width(Fill)
.height(Fill)
.into()
} else {
text("Press the button to take a screenshot!").into()
};
let image = container(image)
.center_y(Length::FillPortion(2))
let image = center_y(image)
.height(FillPortion(2))
.padding(10)
.style(container::rounded_box);
let crop_origin_controls = row![
text("X:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
text("X:").width(30),
numeric_input("0", self.x_input_value).map(Message::XInputChanged),
text("Y:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
text("Y:").width(30),
numeric_input("0", self.y_input_value).map(Message::YInputChanged)
]
.spacing(10)
.align_items(Alignment::Center);
.align_y(Center);
let crop_dimension_controls = row![
text("W:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
text("W:").width(30),
numeric_input("0", self.width_input_value)
.map(Message::WidthInputChanged),
text("H:")
.vertical_alignment(alignment::Vertical::Center)
.width(30),
text("H:").width(30),
numeric_input("0", self.height_input_value)
.map(Message::HeightInputChanged)
]
.spacing(10)
.align_items(Alignment::Center);
.align_y(Center);
let crop_controls =
column![crop_origin_controls, crop_dimension_controls]
.push_maybe(
self.crop_error
.as_ref()
.map(|error| text(format!("Crop error! \n{error}"))),
.map(|error| text!("Crop error! \n{error}")),
)
.spacing(10)
.align_items(Alignment::Center);
.align_x(Center);
let controls = {
let save_result =
@ -179,8 +182,8 @@ impl Example {
column![
column![
button(centered_text("Screenshot!"))
.padding([10, 20, 10, 20])
.width(Length::Fill)
.padding([10, 20])
.width(Fill)
.on_press(Message::Screenshot),
if !self.png_saving {
button(centered_text("Save as png")).on_press_maybe(
@ -191,8 +194,8 @@ impl Example {
.style(button::secondary)
}
.style(button::secondary)
.padding([10, 20, 10, 20])
.width(Length::Fill)
.padding([10, 20])
.width(Fill)
]
.spacing(10),
column![
@ -200,23 +203,23 @@ impl Example {
button(centered_text("Crop"))
.on_press(Message::Crop)
.style(button::danger)
.padding([10, 20, 10, 20])
.width(Length::Fill),
.padding([10, 20])
.width(Fill),
]
.spacing(10)
.align_items(Alignment::Center),
.align_x(Center),
]
.push_maybe(save_result.map(text))
.spacing(40)
};
let side_content = container(controls).center_y(Length::Fill);
let side_content = center_y(controls);
let content = row![side_content, image]
.spacing(10)
.width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::Center);
.width(Fill)
.height(Fill)
.align_y(Center);
container(content).padding(10).into()
}
@ -277,8 +280,5 @@ fn numeric_input(
}
fn centered_text(content: &str) -> Element<'_, Message> {
text(content)
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.into()
text(content).width(Fill).align_x(Center).into()
}

View file

@ -2,11 +2,9 @@
name = "scrollable"
version = "0.1.0"
authors = ["Clark Moody <clark@clarkmoody.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["debug"]
once_cell.workspace = true

View file

@ -1,16 +1,16 @@
use iced::widget::scrollable::Properties;
use iced::widget::{
button, column, container, horizontal_space, progress_bar, radio, row,
scrollable, slider, text, vertical_space, Scrollable,
scrollable, slider, text, vertical_space,
};
use iced::{Alignment, Border, Color, Command, Element, Length, Theme};
use iced::{Border, Center, Color, Element, Fill, Task, Theme};
use once_cell::sync::Lazy;
use std::sync::LazyLock;
static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);
static SCROLLABLE_ID: LazyLock<scrollable::Id> =
LazyLock::new(scrollable::Id::unique);
pub fn main() -> iced::Result {
iced::program(
iced::application(
"Scrollable - Iced",
ScrollableDemo::update,
ScrollableDemo::view,
@ -21,11 +21,11 @@ pub fn main() -> iced::Result {
struct ScrollableDemo {
scrollable_direction: Direction,
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
scrollbar_width: u32,
scrollbar_margin: u32,
scroller_width: u32,
current_scroll_offset: scrollable::RelativeOffset,
alignment: scrollable::Alignment,
anchor: scrollable::Anchor,
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
@ -38,10 +38,10 @@ enum Direction {
#[derive(Debug, Clone)]
enum Message {
SwitchDirection(Direction),
AlignmentChanged(scrollable::Alignment),
ScrollbarWidthChanged(u16),
ScrollbarMarginChanged(u16),
ScrollerWidthChanged(u16),
AlignmentChanged(scrollable::Anchor),
ScrollbarWidthChanged(u32),
ScrollbarMarginChanged(u32),
ScrollerWidthChanged(u32),
ScrollToBeginning,
ScrollToEnd,
Scrolled(scrollable::Viewport),
@ -55,11 +55,11 @@ impl ScrollableDemo {
scrollbar_margin: 0,
scroller_width: 10,
current_scroll_offset: scrollable::RelativeOffset::START,
alignment: scrollable::Alignment::Start,
anchor: scrollable::Anchor::Start,
}
}
fn update(&mut self, message: Message) -> Command<Message> {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::SwitchDirection(direction) => {
self.current_scroll_offset = scrollable::RelativeOffset::START;
@ -72,7 +72,7 @@ impl ScrollableDemo {
}
Message::AlignmentChanged(alignment) => {
self.current_scroll_offset = scrollable::RelativeOffset::START;
self.alignment = alignment;
self.anchor = alignment;
scrollable::snap_to(
SCROLLABLE_ID.clone(),
@ -82,17 +82,17 @@ impl ScrollableDemo {
Message::ScrollbarWidthChanged(width) => {
self.scrollbar_width = width;
Command::none()
Task::none()
}
Message::ScrollbarMarginChanged(margin) => {
self.scrollbar_margin = margin;
Command::none()
Task::none()
}
Message::ScrollerWidthChanged(width) => {
self.scroller_width = width;
Command::none()
Task::none()
}
Message::ScrollToBeginning => {
self.current_scroll_offset = scrollable::RelativeOffset::START;
@ -113,7 +113,7 @@ impl ScrollableDemo {
Message::Scrolled(viewport) => {
self.current_scroll_offset = viewport.relative_offset();
Command::none()
Task::none()
}
}
}
@ -169,14 +169,14 @@ impl ScrollableDemo {
text("Scrollable alignment:"),
radio(
"Start",
scrollable::Alignment::Start,
Some(self.alignment),
scrollable::Anchor::Start,
Some(self.anchor),
Message::AlignmentChanged,
),
radio(
"End",
scrollable::Alignment::End,
Some(self.alignment),
scrollable::Anchor::End,
Some(self.anchor),
Message::AlignmentChanged,
)
]
@ -203,7 +203,7 @@ impl ScrollableDemo {
let scrollable_content: Element<Message> =
Element::from(match self.scrollable_direction {
Direction::Vertical => Scrollable::with_direction(
Direction::Vertical => scrollable(
column![
scroll_to_end_button(),
text("Beginning!"),
@ -213,22 +213,22 @@ impl ScrollableDemo {
text("End!"),
scroll_to_beginning_button(),
]
.align_items(Alignment::Center)
.padding([40, 0, 40, 0])
.align_x(Center)
.padding([40, 0])
.spacing(40),
scrollable::Direction::Vertical(
Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.alignment(self.alignment),
),
)
.width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Vertical(
scrollable::Scrollbar::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.anchor(self.anchor),
))
.width(Fill)
.height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
Direction::Horizontal => Scrollable::with_direction(
Direction::Horizontal => scrollable(
row![
scroll_to_end_button(),
text("Beginning!"),
@ -239,22 +239,22 @@ impl ScrollableDemo {
scroll_to_beginning_button(),
]
.height(450)
.align_items(Alignment::Center)
.padding([0, 40, 0, 40])
.align_y(Center)
.padding([0, 40])
.spacing(40),
scrollable::Direction::Horizontal(
Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.alignment(self.alignment),
),
)
.width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Horizontal(
scrollable::Scrollbar::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.anchor(self.anchor),
))
.width(Fill)
.height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
Direction::Multi => Scrollable::with_direction(
Direction::Multi => scrollable(
//horizontal content
row![
column![
@ -281,24 +281,24 @@ impl ScrollableDemo {
text("Horizontal - End!"),
scroll_to_beginning_button(),
]
.align_items(Alignment::Center)
.padding([0, 40, 0, 40])
.align_y(Center)
.padding([0, 40])
.spacing(40),
{
let properties = Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.alignment(self.alignment);
scrollable::Direction::Both {
horizontal: properties,
vertical: properties,
}
},
)
.width(Length::Fill)
.height(Length::Fill)
.direction({
let scrollbar = scrollable::Scrollbar::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
.scroller_width(self.scroller_width)
.anchor(self.anchor);
scrollable::Direction::Both {
horizontal: scrollbar,
vertical: scrollbar,
}
})
.width(Fill)
.height(Fill)
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
});
@ -323,7 +323,7 @@ impl ScrollableDemo {
let content: Element<Message> =
column![scroll_controls, scrollable_content, progress_bars]
.align_items(Alignment::Center)
.align_x(Center)
.spacing(10)
.into();

View file

@ -1,14 +1,13 @@
use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas, Geometry};
use iced::widget::canvas::{self, Canvas, Event, Geometry};
use iced::widget::{column, row, slider, text};
use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme};
use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme};
use rand::Rng;
use std::fmt::Debug;
fn main() -> iced::Result {
iced::program(
iced::application(
"Sierpinski Triangle - Iced",
SierpinskiEmulator::update,
SierpinskiEmulator::view,
@ -50,17 +49,15 @@ impl SierpinskiEmulator {
fn view(&self) -> iced::Element<'_, Message> {
column![
Canvas::new(&self.graph)
.width(Length::Fill)
.height(Length::Fill),
Canvas::new(&self.graph).width(Fill).height(Fill),
row![
text(format!("Iteration: {:?}", self.graph.iteration)),
text!("Iteration: {:?}", self.graph.iteration),
slider(0..=10000, self.graph.iteration, Message::IterationSet)
]
.padding(10)
.spacing(20),
]
.align_items(iced::Alignment::Center)
.align_x(Center)
.into()
}
}
@ -79,29 +76,25 @@ impl canvas::Program<Message> for SierpinskiGraph {
fn update(
&self,
_state: &mut Self::State,
event: Event,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
let Some(cursor_position) = cursor.position_in(bounds) else {
return (event::Status::Ignored, None);
};
) -> Option<canvas::Action<Message>> {
let cursor_position = cursor.position_in(bounds)?;
match event {
Event::Mouse(mouse_event) => {
let message = match mouse_event {
iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Left,
) => Some(Message::PointAdded(cursor_position)),
iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Right,
) => Some(Message::PointRemoved),
_ => None,
};
(event::Status::Captured, message)
}
_ => (event::Status::Ignored, None),
Event::Mouse(mouse::Event::ButtonPressed(button)) => match button {
mouse::Button::Left => Some(canvas::Action::publish(
Message::PointAdded(cursor_position),
)),
mouse::Button::Right => {
Some(canvas::Action::publish(Message::PointRemoved))
}
_ => None,
},
_ => None,
}
.map(canvas::Action::and_capture)
}
fn draw(

View file

@ -2,8 +2,9 @@
name = "slider"
version = "0.1.0"
authors = ["Casper Rogild Storm<casper@rogildstorm.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["svg"]

View file

@ -1,5 +1,5 @@
use iced::widget::{center, column, container, slider, text, vertical_slider};
use iced::{Element, Length};
use iced::widget::{column, container, iced, slider, text, vertical_slider};
use iced::{Center, Element, Fill};
pub fn main() -> iced::Result {
iced::run("Slider - Iced", Slider::update, Slider::view)
@ -12,19 +12,11 @@ pub enum Message {
pub struct Slider {
value: u8,
default: u8,
step: u8,
shift_step: u8,
}
impl Slider {
fn new() -> Self {
Slider {
value: 50,
default: 50,
step: 5,
shift_step: 1,
}
Slider { value: 50 }
}
fn update(&mut self, message: Message) {
@ -37,32 +29,27 @@ impl Slider {
fn view(&self) -> Element<Message> {
let h_slider = container(
slider(0..=100, self.value, Message::SliderChanged)
.default(self.default)
.step(self.step)
.shift_step(self.shift_step),
slider(1..=100, self.value, Message::SliderChanged)
.default(50)
.shift_step(5),
)
.width(250);
let v_slider = container(
vertical_slider(0..=100, self.value, Message::SliderChanged)
.default(self.default)
.step(self.step)
.shift_step(self.shift_step),
vertical_slider(1..=100, self.value, Message::SliderChanged)
.default(50)
.shift_step(5),
)
.height(200);
let text = text(self.value);
center(
column![
container(v_slider).center_x(Length::Fill),
container(h_slider).center_x(Length::Fill),
container(text).center_x(Length::Fill)
]
.spacing(25),
)
.into()
column![v_slider, h_slider, text, iced(self.value as f32),]
.width(Fill)
.align_x(Center)
.spacing(20)
.padding(20)
.into()
}
}

View file

@ -2,12 +2,12 @@
name = "solar_system"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
edition = "2024"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["debug", "canvas", "tokio"]
iced.features = ["debug", "canvas", "image", "tokio"]
rand = "0.8.3"
tracing-subscriber = "0.3"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -7,13 +7,12 @@
//!
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
use iced::mouse;
use iced::widget::canvas;
use iced::widget::canvas::gradient;
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{Geometry, Path};
use iced::widget::{canvas, image};
use iced::window;
use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription,
Color, Element, Fill, Point, Rectangle, Renderer, Size, Subscription,
Theme, Vector,
};
@ -22,7 +21,7 @@ use std::time::Instant;
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program(
iced::application(
"Solar System - Iced",
SolarSystem::update,
SolarSystem::view,
@ -52,10 +51,7 @@ impl SolarSystem {
}
fn view(&self) -> Element<Message> {
canvas(&self.state)
.width(Length::Fill)
.height(Length::Fill)
.into()
canvas(&self.state).width(Fill).height(Fill).into()
}
fn theme(&self) -> Theme {
@ -69,6 +65,9 @@ impl SolarSystem {
#[derive(Debug)]
struct State {
sun: image::Handle,
earth: image::Handle,
moon: image::Handle,
space_cache: canvas::Cache,
system_cache: canvas::Cache,
start: Instant,
@ -88,6 +87,15 @@ impl State {
let size = window::Settings::default().size;
State {
sun: image::Handle::from_bytes(
include_bytes!("../assets/sun.png").as_slice(),
),
earth: image::Handle::from_bytes(
include_bytes!("../assets/earth.png").as_slice(),
),
moon: image::Handle::from_bytes(
include_bytes!("../assets/moon.png").as_slice(),
),
space_cache: canvas::Cache::default(),
system_cache: canvas::Cache::default(),
start: now,
@ -135,6 +143,8 @@ impl<Message> canvas::Program<Message> for State {
let background =
self.space_cache.draw(renderer, bounds.size(), |frame| {
frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK);
let stars = Path::new(|path| {
for (p, size) in &self.stars {
path.rectangle(*p, Size::new(*size, *size));
@ -147,17 +157,18 @@ impl<Message> canvas::Program<Message> for State {
let system = self.system_cache.draw(renderer, bounds.size(), |frame| {
let center = frame.center();
frame.translate(Vector::new(center.x, center.y));
let sun = Path::circle(center, Self::SUN_RADIUS);
let orbit = Path::circle(center, Self::ORBIT_RADIUS);
frame.draw_image(
Rectangle::with_radius(Self::SUN_RADIUS),
&self.sun,
);
frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C));
let orbit = Path::circle(Point::ORIGIN, Self::ORBIT_RADIUS);
frame.stroke(
&orbit,
Stroke {
style: stroke::Style::Solid(Color::from_rgba8(
0, 153, 255, 0.1,
)),
style: stroke::Style::Solid(Color::WHITE.scale_alpha(0.1)),
width: 1.0,
line_dash: canvas::LineDash {
offset: 0,
@ -171,30 +182,21 @@ impl<Message> canvas::Program<Message> for State {
let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32
+ (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32;
frame.with_save(|frame| {
frame.translate(Vector::new(center.x, center.y));
frame.rotate(rotation);
frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
frame.rotate(rotation);
frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
frame.draw_image(
Rectangle::with_radius(Self::EARTH_RADIUS),
canvas::Image::new(&self.earth).rotation(-rotation * 20.0),
);
let earth_fill = gradient::Linear::new(
Point::new(-Self::EARTH_RADIUS, 0.0),
Point::new(Self::EARTH_RADIUS, 0.0),
)
.add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0))
.add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47));
frame.rotate(rotation * 10.0);
frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
frame.fill(&earth, earth_fill);
frame.with_save(|frame| {
frame.rotate(rotation * 10.0);
frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS);
frame.fill(&moon, Color::WHITE);
});
});
frame.draw_image(
Rectangle::with_radius(Self::MOON_RADIUS),
&self.moon,
);
});
vec![background, system]

Some files were not shown because too many files have changed in this diff Show more