Merge branch 'master' into beacon
This commit is contained in:
commit
8bd5de72ea
371 changed files with 33138 additions and 12950 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "arc"
|
||||
version = "0.1.0"
|
||||
authors = ["ThatsNoMoon <git@thatsnomoon.dev>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
26
examples/changelog/Cargo.toml
Normal file
26
examples/changelog/Cargo.toml
Normal 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"]
|
||||
386
examples/changelog/src/changelog.rs
Normal file
386
examples/changelog/src/changelog.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
10
examples/changelog/src/icon.rs
Normal file
10
examples/changelog/src/icon.rs
Normal 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)
|
||||
}
|
||||
375
examples/changelog/src/main.rs
Normal file
375
examples/changelog/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name = "checkbox"
|
||||
version = "0.1.0"
|
||||
authors = ["Casper Rogild Storm<casper@rogildstorm.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "color_palette"
|
||||
version = "0.1.0"
|
||||
authors = ["Clark Moody <clark@clarkmoody.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "custom_quad"
|
||||
version = "0.1.0"
|
||||
authors = ["Robert Krahn"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
12
examples/download_progress/index.html
Normal file
12
examples/download_progress/index.html
Normal 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>
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "exit"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
26
examples/gallery/Cargo.toml
Normal file
26
examples/gallery/Cargo.toml
Normal 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
|
||||
189
examples/gallery/src/civitai.rs
Normal file
189
examples/gallery/src/civitai.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
442
examples/gallery/src/main.rs
Normal file
442
examples/gallery/src/main.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "gradient"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "lazy"
|
||||
version = "0.1.0"
|
||||
authors = ["Nick Senger <dev@nsenger.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
23
examples/markdown/Cargo.toml
Normal file
23
examples/markdown/Cargo.toml
Normal 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"
|
||||
5
examples/markdown/build.rs
Normal file
5
examples/markdown/build.rs
Normal 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");
|
||||
}
|
||||
4
examples/markdown/fonts/markdown-icons.toml
Normal file
4
examples/markdown/fonts/markdown-icons.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module = "icon"
|
||||
|
||||
[glyphs]
|
||||
copy = "fontawesome-docs"
|
||||
BIN
examples/markdown/fonts/markdown-icons.ttf
Normal file
BIN
examples/markdown/fonts/markdown-icons.ttf
Normal file
Binary file not shown.
93
examples/markdown/overview.md
Normal file
93
examples/markdown/overview.md
Normal 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/
|
||||
15
examples/markdown/src/icon.rs
Normal file
15
examples/markdown/src/icon.rs
Normal 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"))
|
||||
}
|
||||
362
examples/markdown/src/main.rs
Normal file
362
examples/markdown/src/main.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name = "modal"
|
||||
version = "0.1.0"
|
||||
authors = ["tarkah <admin@tarkah.dev>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "multitouch"
|
||||
version = "0.1.0"
|
||||
authors = ["Artur Sapek <artur@kraken.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "screenshot"
|
||||
version = "0.1.0"
|
||||
authors = ["Bingus <shankern@protonmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
BIN
examples/solar_system/assets/earth.png
Normal file
BIN
examples/solar_system/assets/earth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
BIN
examples/solar_system/assets/moon.png
Normal file
BIN
examples/solar_system/assets/moon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
examples/solar_system/assets/sun.png
Normal file
BIN
examples/solar_system/assets/sun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue