Merge branch 'master' into feat/multi-window-support

This commit is contained in:
Héctor Ramón Jiménez 2023-11-29 22:28:31 +01:00
commit e09b4e24dd
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
331 changed files with 12085 additions and 3976 deletions

53
.cargo/config.toml Normal file
View file

@ -0,0 +1,53 @@
[alias]
lint = """
clippy --workspace --no-deps -- \
-D warnings \
-A clippy::type_complexity \
-D clippy::semicolon_if_nothing_returned \
-D clippy::trivially-copy-pass-by-ref \
-D clippy::default_trait_access \
-D clippy::match-wildcard-for-single-variants \
-D clippy::redundant-closure-for-method-calls \
-D clippy::filter_map_next \
-D clippy::manual_let_else \
-D clippy::unused_async \
-D clippy::from_over_into \
-D clippy::needless_borrow \
-D clippy::new_without_default \
-D clippy::useless_conversion
"""
nitpick = """
clippy --workspace --no-deps -- \
-D warnings \
-D clippy::pedantic \
-A clippy::type_complexity \
-A clippy::must_use_candidate \
-A clippy::return_self_not_must_use \
-A clippy::needless_pass_by_value \
-A clippy::cast_precision_loss \
-A clippy::cast_sign_loss \
-A clippy::cast_possible_truncation \
-A clippy::match_same_arms \
-A clippy::missing-errors-doc \
-A clippy::missing-panics-doc \
-A clippy::cast_lossless \
-A clippy::doc_markdown \
-A clippy::items_after_statements \
-A clippy::too_many_lines \
-A clippy::module_name_repetitions \
-A clippy::if_not_else \
-A clippy::redundant_else \
-A clippy::used_underscore_binding \
-A clippy::cast_possible_wrap \
-A clippy::unnecessary_wraps \
-A clippy::struct-excessive-bools \
-A clippy::float-cmp \
-A clippy::single_match_else \
-A clippy::unreadable_literal \
-A clippy::explicit_deref_methods \
-A clippy::map_unwrap_or \
-A clippy::unnested_or_patterns \
-A clippy::similar_names \
-A clippy::unused_self
"""

View file

@ -1,11 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: I have a question - name: I have a question
url: https://github.com/iced-rs/iced/discussions/new?category=q-a url: https://discourse.iced.rs/c/learn/6
about: Open a discussion with a Q&A format. about: Ask and learn from others in the Discourse forum.
- name: I want to start a discussion - name: I want to start a discussion
url: https://github.com/iced-rs/iced/discussions/new url: https://discourse.iced.rs/c/request-feedback/7
about: Open a new discussion if you have any suggestions, ideas, feature requests, or simply want to show off something you've made. about: Share your idea and gather feedback in the Discourse forum.
- name: I want to chat with other users of the library - name: I want to chat with other users of the library
url: https://discord.com/invite/3xZJ65GAhd url: https://discord.com/invite/3xZJ65GAhd
about: Join the Discord Server and get involved with the community! about: Join the Discord server and get involved with the community!

3
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,3 @@
The core team is busy and does not have time to mentor nor babysit new contributors. If a member of the core team thinks that reviewing and understanding your work will take more time and effort than writing it from scratch by themselves, your contribution will be dismissed. It is your responsibility to communicate and figure out how to reduce the likelihood of this!
Read the contributing guidelines for more details: https://github.com/iced-rs/iced/blob/master/CONTRIBUTING.md

View file

@ -1,12 +1,30 @@
name: Audit name: Audit
on: [push] on:
push: {}
pull_request: {}
schedule:
- cron: '0 0 * * *'
jobs: jobs:
dependencies: vulnerabilities:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
- name: Install cargo-audit - name: Install cargo-audit
run: cargo install cargo-audit run: cargo install cargo-audit
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Audit dependencies - name: Resolve dependencies
run: cargo update
- name: Audit vulnerabilities
run: cargo audit run: cargo audit
artifacts:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v1
- name: Install cargo-outdated
run: cargo install cargo-outdated
- uses: actions/checkout@master
- name: Delete `web-sys` dependency from `integration` example
run: sed -i '$d' examples/integration/Cargo.toml
- name: Find outdated dependencies
run: cargo outdated --workspace --exit-code 1

View file

@ -40,7 +40,6 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Enable static CRT linkage - name: Enable static CRT linkage
run: | run: |
mkdir .cargo
echo '[target.x86_64-pc-windows-msvc]' >> .cargo/config echo '[target.x86_64-pc-windows-msvc]' >> .cargo/config
echo 'rustflags = ["-Ctarget-feature=+crt-static"]' >> .cargo/config echo 'rustflags = ["-Ctarget-feature=+crt-static"]' >> .cargo/config
- name: Run the application without starting the shell - name: Run the application without starting the shell

29
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Check
on: [push, pull_request]
jobs:
widget:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v1
- uses: actions/checkout@master
- name: Check standalone `iced_widget` crate
run: cargo check --package iced_widget --features image,svg,canvas
wasm:
runs-on: ubuntu-latest
env:
RUSTFLAGS: --cfg=web_sys_unstable_apis
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
targets: wasm32-unknown-unknown
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
- name: Check compilation of `tour` example
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown

View file

@ -1,8 +1,5 @@
name: Document name: Document
on: on: [push, pull_request]
push:
branches:
- master
jobs: jobs:
all: all:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@ -18,6 +15,7 @@ jobs:
RUSTDOCFLAGS="--cfg docsrs" \ RUSTDOCFLAGS="--cfg docsrs" \
cargo doc --no-deps --all-features \ cargo doc --no-deps --all-features \
-p iced_core \ -p iced_core \
-p iced_highlighter \
-p iced_style \ -p iced_style \
-p iced_futures \ -p iced_futures \
-p iced_runtime \ -p iced_runtime \
@ -31,6 +29,7 @@ jobs:
- name: Write CNAME file - name: Write CNAME file
run: echo 'docs.iced.rs' > ./target/doc/CNAME run: echo 'docs.iced.rs' > ./target/doc/CNAME
- name: Publish documentation - name: Publish documentation
if: github.ref == 'refs/heads/master'
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:
deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }}

View file

@ -2,11 +2,11 @@ name: Lint
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
all: all:
runs-on: ubuntu-latest runs-on: macOS-latest
steps: steps:
- uses: hecrj/setup-rust-action@v1 - uses: hecrj/setup-rust-action@v1
with: with:
components: clippy components: clippy
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Check lints - name: Check lints
run: cargo clippy --workspace --all-features --all-targets --no-deps -- -D warnings run: cargo lint

View file

@ -1,8 +1,10 @@
name: Test name: Test
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
native: all:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
RUSTFLAGS: --deny warnings
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
@ -17,25 +19,8 @@ jobs:
run: | run: |
export DEBIAN_FRONTED=noninteractive export DEBIAN_FRONTED=noninteractive
sudo apt-get -qq update sudo apt-get -qq update
sudo apt-get install -y libxkbcommon-dev sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
- name: Run tests - name: Run tests
run: | run: |
cargo test --verbose --workspace cargo test --verbose --workspace
cargo test --verbose --workspace --all-features cargo test --verbose --workspace --all-features
web:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v1
with:
rust-version: stable
targets: wasm32-unknown-unknown
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
- name: Check compilation of `tour` example
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown

1
.gitignore vendored
View file

@ -2,6 +2,5 @@ target/
pkg/ pkg/
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
.cargo/
dist/ dist/
traces/ traces/

View file

@ -5,6 +5,135 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- Explicit text caching. [#2058](https://github.com/iced-rs/iced/pull/2058)
- `Theme::Custom::with_fn` for custom extended palette generation. [#2067](https://github.com/iced-rs/iced/pull/2067)
### Changed
- Updated `wgpu` to `0.17`. [#2065](https://github.com/iced-rs/iced/pull/2065)
- Changed `Button::style` to take an `impl Into<...>` for consistency. [#2046](https://github.com/iced-rs/iced/pull/2046)
### Fixed
- Missing `width` attribute in `styling` example. [#2062](https://github.com/iced-rs/iced/pull/2062)
Many thanks to...
- @akshayr-mecha
- @dtzxporter
## [0.10.0] - 2023-07-28
### Added
- Text shaping, font fallback, and `iced_wgpu` overhaul. [#1697](https://github.com/iced-rs/iced/pull/1697)
- Software renderer, runtime renderer fallback, and core consolidation. [#1748](https://github.com/iced-rs/iced/pull/1748)
- Incremental rendering for `iced_tiny_skia`. [#1811](https://github.com/iced-rs/iced/pull/1811)
- Configurable `LineHeight` support for text widgets. [#1828](https://github.com/iced-rs/iced/pull/1828)
- `text::Shaping` strategy selection. [#1822](https://github.com/iced-rs/iced/pull/1822)
- Subpixel glyph positioning and layout linearity. [#1921](https://github.com/iced-rs/iced/pull/1921)
- Background gradients. [#1846](https://github.com/iced-rs/iced/pull/1846)
- Offscreen rendering and screenshots. [#1845](https://github.com/iced-rs/iced/pull/1845)
- Nested overlays. [#1719](https://github.com/iced-rs/iced/pull/1719)
- Cursor availability. [#1904](https://github.com/iced-rs/iced/pull/1904)
- Backend-specific primitives. [#1932](https://github.com/iced-rs/iced/pull/1932)
- `ComboBox` widget. [#1954](https://github.com/iced-rs/iced/pull/1954)
- `web-colors` feature flag to enable "sRGB linear" blending. [#1888](https://github.com/iced-rs/iced/pull/1888)
- `PaneGrid` logic to split panes by drag & drop. [#1856](https://github.com/iced-rs/iced/pull/1856)
- `PaneGrid` logic to drag & drop panes to the edges. [#1865](https://github.com/iced-rs/iced/pull/1865)
- Type-safe `Scrollable` direction. [#1878](https://github.com/iced-rs/iced/pull/1878)
- `Scrollable` alignment. [#1912](https://github.com/iced-rs/iced/pull/1912)
- Helpers to change viewport alignment of a `Scrollable`. [#1953](https://github.com/iced-rs/iced/pull/1953)
- `scroll_to` widget operation. [#1796](https://github.com/iced-rs/iced/pull/1796)
- `scroll_to` helper. [#1804](https://github.com/iced-rs/iced/pull/1804)
- `visible_bounds` widget operation for `Container`. [#1971](https://github.com/iced-rs/iced/pull/1971)
- Command to fetch window size. [#1927](https://github.com/iced-rs/iced/pull/1927)
- Conversion support from `Fn` trait to custom theme. [#1861](https://github.com/iced-rs/iced/pull/1861)
- Configurable border radii on relevant widgets. [#1869](https://github.com/iced-rs/iced/pull/1869)
- `border_radius` styling to `Slider` rail. [#1892](https://github.com/iced-rs/iced/pull/1892)
- `application_id` in `PlatformSpecific` settings for Linux. [#1963](https://github.com/iced-rs/iced/pull/1963)
- Aliased entries in `text::Cache`. [#1934](https://github.com/iced-rs/iced/pull/1934)
- Text cache modes. [#1938](https://github.com/iced-rs/iced/pull/1938)
- `operate` method for `program::State`. [#1913](https://github.com/iced-rs/iced/pull/1913)
- `Viewport` argument to `Widget::on_event`. [#1956](https://github.com/iced-rs/iced/pull/1956)
- Nix instructions to `DEPENDENCIES.md`. [#1859](https://github.com/iced-rs/iced/pull/1859)
- Loading spinners example. [#1902](https://github.com/iced-rs/iced/pull/1902)
- Workflow that verifies `CHANGELOG` is always up-to-date. [#1970](https://github.com/iced-rs/iced/pull/1970)
- Outdated mentions of `iced_native` in `README`. [#1979](https://github.com/iced-rs/iced/pull/1979)
### Changed
- Updated `wgpu` to `0.16`. [#1807](https://github.com/iced-rs/iced/pull/1807)
- Updated `glam` to `0.24`. [#1840](https://github.com/iced-rs/iced/pull/1840)
- Updated `winit` to `0.28`. [#1738](https://github.com/iced-rs/iced/pull/1738)
- Updated `palette` to `0.7`. [#1875](https://github.com/iced-rs/iced/pull/1875)
- Updated `ouroboros` to `0.17`. [#1925](https://github.com/iced-rs/iced/pull/1925)
- Updated `resvg` to `0.35` and `tiny-skia` to `0.10`. [#1907](https://github.com/iced-rs/iced/pull/1907)
- Changed `mouse::Button::Other` to take `u16` instead of `u8`. [#1797](https://github.com/iced-rs/iced/pull/1797)
- Changed `subscription::channel` to take a `FnOnce` non-`Sync` closure. [#1917](https://github.com/iced-rs/iced/pull/1917)
- Removed `Copy` requirement for text `StyleSheet::Style`. [#1814](https://github.com/iced-rs/iced/pull/1814)
- Removed `min_width` of 1 from scrollbar & scroller for `Scrollable`. [#1844](https://github.com/iced-rs/iced/pull/1844)
- Used `Widget::overlay` for `Tooltip`. [#1692](https://github.com/iced-rs/iced/pull/1692)
### Fixed
- `Responsive` layout not invalidated when shell layout is invalidated. [#1799](https://github.com/iced-rs/iced/pull/1799)
- `Responsive` layout not invalidated when size changes without a `view` call. [#1890](https://github.com/iced-rs/iced/pull/1890)
- Broken link in `ROADMAP.md`. [#1815](https://github.com/iced-rs/iced/pull/1815)
- `bounds` of selected option background in `Menu`. [#1831](https://github.com/iced-rs/iced/pull/1831)
- Border radius logic in `iced_tiny_skia`. [#1842](https://github.com/iced-rs/iced/pull/1842)
- `Svg` filtered color not premultiplied. [#1841](https://github.com/iced-rs/iced/pull/1841)
- Race condition when growing an `image::Atlas`. [#1847](https://github.com/iced-rs/iced/pull/1847)
- Clearing damaged surface with background color in `iced_tiny_skia`. [#1854](https://github.com/iced-rs/iced/pull/1854)
- Private gradient pack logic for `iced_graphics::Gradient`. [#1871](https://github.com/iced-rs/iced/pull/1871)
- Unordered quads of different background types. [#1873](https://github.com/iced-rs/iced/pull/1873)
- Panic in `glyphon` when glyphs are missing. [#1883](https://github.com/iced-rs/iced/pull/1883)
- Empty scissor rectangle in `iced_wgpu::triangle` pipeline. [#1893](https://github.com/iced-rs/iced/pull/1893)
- `Scrollable` scrolling when mouse not over it. [#1910](https://github.com/iced-rs/iced/pull/1910)
- `translation` in `layout` of `Nested` overlay. [#1924](https://github.com/iced-rs/iced/pull/1924)
- Build when using vendored dependencies. [#1928](https://github.com/iced-rs/iced/pull/1928)
- Minor grammar mistake. [#1931](https://github.com/iced-rs/iced/pull/1931)
- Quad rendering including border only inside of the bounds. [#1843](https://github.com/iced-rs/iced/pull/1843)
- Redraw requests not being forwarded for `Component` overlays. [#1949](https://github.com/iced-rs/iced/pull/1949)
- Blinking input cursor when window loses focus. [#1955](https://github.com/iced-rs/iced/pull/1955)
- `BorderRadius` not exposed in root crate. [#1972](https://github.com/iced-rs/iced/pull/1972)
- Outdated `ROADMAP`. [#1958](https://github.com/iced-rs/iced/pull/1958)
### Patched
- Keybinds to cycle `ComboBox` options. [#1991](https://github.com/iced-rs/iced/pull/1991)
- `Tooltip` overlay position inside `Scrollable`. [#1978](https://github.com/iced-rs/iced/pull/1978)
- `iced_wgpu` freezing on empty layers. [#1996](https://github.com/iced-rs/iced/pull/1996)
- `image::Viewer` reacting to any scroll event. [#1998](https://github.com/iced-rs/iced/pull/1998)
- `TextInput` pasting text when `Alt` key is pressed. [#2006](https://github.com/iced-rs/iced/pull/2006)
- Broken link to old `iced_native` crate in `README`. [#2024](https://github.com/iced-rs/iced/pull/2024)
- `Rectangle::contains` being non-exclusive. [#2017](https://github.com/iced-rs/iced/pull/2017)
- Documentation for `Arc` and `arc::Elliptical`. [#2008](https://github.com/iced-rs/iced/pull/2008)
Many thanks to...
- @a1phyr
- @alec-deason
- @AustinMReppert
- @bbb651
- @bungoboingo
- @casperstorm
- @clarkmoody
- @Davidster
- @Drakulix
- @genusistimelord
- @GyulyVGC
- @ids1024
- @jhff
- @JonathanLindsey
- @kr105
- @marienz
- @malramsay64
- @nicksenger
- @nicoburns
- @NyxAlexandra
- @Redhawk18
- @RGBCube
- @rs017991
- @tarkah
- @thunderstorm010
- @ua-kxie
- @wash2
- @wiiznokes
## [0.9.0] - 2023-04-13 ## [0.9.0] - 2023-04-13
### Added ### Added
@ -467,7 +596,8 @@ Many thanks to...
### Added ### Added
- First release! :tada: - First release! :tada:
[Unreleased]: https://github.com/iced-rs/iced/compare/0.9.0...HEAD [Unreleased]: https://github.com/iced-rs/iced/compare/0.10.0...HEAD
[0.10.0]: https://github.com/iced-rs/iced/compare/0.9.0...0.10.0
[0.9.0]: https://github.com/iced-rs/iced/compare/0.8.0...0.9.0 [0.9.0]: https://github.com/iced-rs/iced/compare/0.8.0...0.9.0
[0.8.0]: https://github.com/iced-rs/iced/compare/0.7.0...0.8.0 [0.8.0]: https://github.com/iced-rs/iced/compare/0.7.0...0.8.0
[0.7.0]: https://github.com/iced-rs/iced/compare/0.6.0...0.7.0 [0.7.0]: https://github.com/iced-rs/iced/compare/0.6.0...0.7.0

View file

@ -2,32 +2,21 @@
Thank you for considering contributing to Iced! Feel free to read [the ecosystem overview] and [the roadmap] to get an idea of the current state of the library. Thank you for considering contributing to Iced! Feel free to read [the ecosystem overview] and [the roadmap] to get an idea of the current state of the library.
The main advice for new contributors is to share your ideas with the community. Introduce yourself over our [Discord server] or [start a discussion in an issue](https://github.com/iced-rs/iced/issues) explaining what you have in mind (do not be afraid of duplicated issues!). If you want to talk directly to me (@hecrj), you can also find me on Discord (`lone_scientist#9554`). The core team is busy and does not have time to mentor nor babysit new contributors. If a member of the core team thinks that reviewing and understanding your work will take more time and effort than writing it from scratch by themselves, your contribution will be dismissed. It is your responsibility to communicate and figure out how to reduce the likelihood of this!
This is a very important step. It helps to coordinate work, get on the same page, and start building trust. Please, do not skip it! Remember that [Code is the Easy Part] and also [The Hard Parts of Open Source]! The general advice for new contributors is to share your ideas with the community. You can share your ideas and gather feedback in [our Discourse forum]. This is a very important step. It helps to coordinate work, get on the same page, and start building trust. Remember that [Code is the Easy Part] and also [The Hard Parts of Open Source]!
Provided you get in touch first, all kinds of contributions are welcome! Here are a few interesting ideas: Once you have started a channel of communication, you must wait until someone from the core team chimes in. If the core team is busy, this can take a long time (maybe months!). Your idea may need a bunch of iteration, or it may turn into something completely different, or it may be completely discarded! You will have to be patient and humble. Remember that open-source is a gift.
- New widgets: toggle, table, grid, color picker, video...
- New renderers: `iced_piet` (already in the works!), `iced_skia`, `iced_raqote`, `iced_pathfinder`...
- New shells: `iced_sdl` could be useful for gamedev!
- Better style generation for `iced_web`
- Optimizations for `iced_wgpu`: tiling, incremental rendering...
- Alternative to [`wgpu_glyph`] for proper (shaped), efficient text rendering
- Time travelling debugger built on top of Iced itself
- Testing library
- Cool website to serve on https://iced.rs
Besides directly writing code, there are many other different ways you can contribute. To name a few: Besides directly writing code, there are many other different ways you can contribute. To name a few:
- Writing tutorials or blog posts - Writing tutorials or blog posts
- Improving the documentation - Improving the documentation
- Submitting bug reports and use cases - Submitting bug reports and use cases
- Sharing, discussing, researching and exploring new ideas - Sharing, discussing, researching and exploring new ideas or crates
[the ecosystem overview]: ECOSYSTEM.md [the ecosystem overview]: ECOSYSTEM.md
[the roadmap]: ROADMAP.md [the roadmap]: ROADMAP.md
[Discord server]: https://discord.gg/3xZJ65GAhd [our Discourse forum]: https://discourse.iced.rs/
[Code is the Easy Part]: https://youtu.be/DSjbTC-hvqQ?t=1138 [Code is the Easy Part]: https://youtu.be/DSjbTC-hvqQ?t=1138
[The Hard Parts of Open Source]: https://www.youtube.com/watch?v=o_4EX4dPppA [The Hard Parts of Open Source]: https://www.youtube.com/watch?v=o_4EX4dPppA
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph

View file

@ -1,22 +1,28 @@
[package] [package]
name = "iced" name = "iced"
version = "0.9.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A cross-platform GUI library inspired by Elm" description = "A cross-platform GUI library inspired by Elm"
license = "MIT" version.workspace = true
repository = "https://github.com/iced-rs/iced" edition.workspace = true
documentation = "https://docs.rs/iced" authors.workspace = true
readme = "README.md" license.workspace = true
keywords = ["gui", "ui", "graphics", "interface", "widgets"] repository.workspace = true
categories = ["gui"] homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
[badges]
maintenance = { status = "actively-developed" }
[features] [features]
default = ["wgpu"] default = ["wgpu"]
# Enable the `wgpu` GPU-accelerated renderer backend # Enable the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu"] wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enables the `Image` widget # Enables the `Image` widget
image = ["iced_widget/image", "image_rs"] image = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget # Enables the `Svg` widget
svg = ["iced_widget/svg"] svg = ["iced_widget/svg"]
# Enables the `Canvas` widget # Enables the `Canvas` widget
@ -39,45 +45,30 @@ palette = ["iced_core/palette"]
system = ["iced_winit/system"] system = ["iced_winit/system"]
# Enables broken "sRGB linear" blending to reproduce color management of the Web # Enables broken "sRGB linear" blending to reproduce color management of the Web
web-colors = ["iced_renderer/web-colors"] web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module
highlighter = ["iced_highlighter"]
# Enables the advanced module # Enables the advanced module
advanced = [] advanced = []
# Enables experimental multi-window support. # Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"] multi-window = ["iced_winit/multi-window"]
[badges]
maintenance = { status = "actively-developed" }
[workspace]
members = [
"core",
"futures",
"graphics",
"runtime",
"renderer",
"style",
"tiny_skia",
"wgpu",
"widget",
"winit",
"examples/*",
]
[dependencies] [dependencies]
iced_core = { version = "0.9", path = "core" } iced_core.workspace = true
iced_futures = { version = "0.6", path = "futures" } iced_futures.workspace = true
iced_renderer = { version = "0.1", path = "renderer" } iced_renderer.workspace = true
iced_widget = { version = "0.1", path = "widget" } iced_widget.workspace = true
iced_winit = { version = "0.9", path = "winit", features = ["application"] } iced_winit.features = ["application"]
thiserror = "1" iced_winit.workspace = true
[dependencies.image_rs] iced_highlighter.workspace = true
version = "0.24" iced_highlighter.optional = true
package = "image"
optional = true
[package.metadata.docs.rs] thiserror.workspace = true
rustdoc-args = ["--cfg", "docsrs"]
features = ["image", "svg", "canvas", "qr_code"] image.workspace = true
image.optional = true
[profile.release-opt] [profile.release-opt]
inherits = "release" inherits = "release"
@ -88,3 +79,85 @@ incremental = false
opt-level = 3 opt-level = 3
overflow-checks = false overflow-checks = false
strip = "debuginfo" strip = "debuginfo"
[workspace]
members = [
"core",
"futures",
"graphics",
"highlighter",
"renderer",
"runtime",
"style",
"tiny_skia",
"wgpu",
"widget",
"winit",
"examples/*",
]
[workspace.package]
version = "0.12.0"
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
edition = "2021"
license = "MIT"
repository = "https://github.com/iced-rs/iced"
homepage = "https://iced.rs"
categories = ["gui"]
keywords = ["gui", "ui", "graphics", "interface", "widgets"]
[workspace.dependencies]
iced = { version = "0.12", path = "." }
iced_core = { version = "0.12", path = "core" }
iced_futures = { version = "0.12", path = "futures" }
iced_graphics = { version = "0.12", path = "graphics" }
iced_highlighter = { version = "0.12", path = "highlighter" }
iced_renderer = { version = "0.12", path = "renderer" }
iced_runtime = { version = "0.12", path = "runtime" }
iced_style = { version = "0.12", path = "style" }
iced_tiny_skia = { version = "0.12", path = "tiny_skia" }
iced_wgpu = { version = "0.12", path = "wgpu" }
iced_widget = { version = "0.12", path = "widget" }
iced_winit = { version = "0.12", path = "winit" }
async-std = "1.0"
bitflags = "1.0"
bytemuck = { version = "1.0", features = ["derive"] }
cosmic-text = "0.10"
futures = "0.3"
glam = "0.24"
glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" }
guillotiere = "0.6"
half = "2.2"
image = "0.24"
instant = "0.1"
kamadak-exif = "0.5"
kurbo = "0.9"
log = "0.4"
lyon = "1.0"
lyon_path = "1.0"
num-traits = "0.2"
once_cell = "1.0"
ouroboros = "0.17"
palette = "0.7"
qrcode = { version = "0.12", default-features = false }
raw-window-handle = "0.5"
resvg = "0.36"
rustc-hash = "1.0"
smol = "1.0"
softbuffer = "0.2"
syntect = "5.1"
sysinfo = "0.28"
thiserror = "1.0"
tiny-skia = "0.11"
tokio = "1.0"
tracing = "0.1"
xxhash-rust = { version = "0.8", features = ["xxh3"] }
unicode-segmentation = "1.0"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
web-sys = "0.3"
wgpu = "0.18"
winapi = "0.3"
window_clipboard = "0.3"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false }

View file

@ -9,16 +9,17 @@
[![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) [![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
[![Downloads](https://img.shields.io/crates/d/iced.svg)](https://crates.io/crates/iced) [![Downloads](https://img.shields.io/crates/d/iced.svg)](https://crates.io/crates/iced)
[![Test Status](https://img.shields.io/github/actions/workflow/status/iced-rs/iced/test.yml?branch=master&event=push&label=test)](https://github.com/iced-rs/iced/actions) [![Test Status](https://img.shields.io/github/actions/workflow/status/iced-rs/iced/test.yml?branch=master&event=push&label=test)](https://github.com/iced-rs/iced/actions)
[![Discourse](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.iced.rs%2Fsite%2Fstatistics.json&query=%24.users_count&suffix=%20users&label=discourse&color=5e7ce2)](https://discourse.iced.rs/)
[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) [![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
A cross-platform GUI library for Rust focused on simplicity and type-safety. A cross-platform GUI library for Rust focused on simplicity and type-safety.
Inspired by [Elm]. Inspired by [Elm].
<a href="https://gfycat.com/littlesanehalicore"> <a href="https://iced.rs/examples/todos.mp4">
<img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" width="275px"> <img src="https://iced.rs/examples/todos.gif" width="275px">
</a> </a>
<a href="https://gfycat.com/politeadorableiberianmole"> <a href="https://iced.rs/examples/tour.mp4">
<img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif" width="273px"> <img src="https://iced.rs/examples/tour.gif" width="273px">
</a> </a>
</div> </div>
@ -46,11 +47,11 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
[Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg [Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg
[the Web]: https://github.com/iced-rs/iced_web [the Web]: https://github.com/iced-rs/iced_web
[text inputs]: https://gfycat.com/alertcalmcrow-rust-gui [text inputs]: https://iced.rs/examples/text_input.mp4
[scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui [scrollables]: https://iced.rs/examples/scrollable.mp4
[Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee [Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4
[Modular ecosystem]: ECOSYSTEM.md [Modular ecosystem]: ECOSYSTEM.md
[renderer-agnostic native runtime]: native/ [renderer-agnostic native runtime]: runtime/
[`wgpu`]: https://github.com/gfx-rs/wgpu [`wgpu`]: https://github.com/gfx-rs/wgpu
[`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
[`iced_wgpu`]: wgpu/ [`iced_wgpu`]: wgpu/
@ -68,7 +69,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
Add `iced` as a dependency in your `Cargo.toml`: Add `iced` as a dependency in your `Cargo.toml`:
```toml ```toml
iced = "0.9" iced = "0.10"
``` ```
If your project is using a Rust edition older than 2021, then you will need to If your project is using a Rust edition older than 2021, then you will need to
@ -201,10 +202,8 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular:
Contributions are greatly appreciated! If you want to contribute, please Contributions are greatly appreciated! If you want to contribute, please
read our [contributing guidelines] for more details. read our [contributing guidelines] for more details.
Feedback is also welcome! You can open an issue or, if you want to talk, Feedback is also welcome! You can create a new topic in [our Discourse forum] or
come chat to our [Discord server]. Moreover, you can find me (and a bunch of come chat to [our Discord server].
awesome folks) over the `#games-and-graphics` and `#gui-and-ui` channels in
the [Rust Community Discord]. I go by `lone_scientist#9554` there.
## Sponsors ## Sponsors
@ -217,7 +216,7 @@ The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com]
[The Elm Architecture]: https://guide.elm-lang.org/architecture/ [The Elm Architecture]: https://guide.elm-lang.org/architecture/
[the current issues]: https://github.com/iced-rs/iced/issues [the current issues]: https://github.com/iced-rs/iced/issues
[contributing guidelines]: https://github.com/iced-rs/iced/blob/master/CONTRIBUTING.md [contributing guidelines]: https://github.com/iced-rs/iced/blob/master/CONTRIBUTING.md
[Discord server]: https://discord.gg/3xZJ65GAhd [our Discourse forum]: https://discourse.iced.rs/
[Rust Community Discord]: https://bit.ly/rust-community [our Discord server]: https://discord.gg/3xZJ65GAhd
[Cryptowatch]: https://cryptowat.ch/charts [Cryptowatch]: https://cryptowat.ch/charts
[Kraken.com]: https://kraken.com/ [Kraken.com]: https://kraken.com/

View file

@ -1,120 +1,6 @@
# Roadmap # Roadmap
This document describes the current state of Iced and some of the most important next steps we should take before it can become a production-ready GUI library. This list keeps the short term new features in sight in order to coordinate work and discussion. Therefore, it is not meant to be exhaustive. We have [a detailed graphical roadmap now](https://whimsical.com/roadmap-iced-7vhq6R35Lp3TmYH4WeYwLM)!
Before diving into the roadmap, check out [the ecosystem overview] to get an idea of the current state of the library. Before diving into the roadmap, check out [the ecosystem overview] to get an idea of the current state of the library.
[the ecosystem overview]: ECOSYSTEM.md [the ecosystem overview]: ECOSYSTEM.md
## Next steps
Most of the work related to these features needs to happen in the __native__ path of the ecosystem, as the web already supports many of them.
Once a step is completed, it is collapsed and added to this list:
* [x] Scrollables / Clippables ([#24])
* [x] Text input widget ([#25])
* [x] TodoMVC example ([#26])
* [x] Async actions ([#28])
* [x] Custom layout engine ([#52])
* [x] Event subscriptions ([#122])
* [x] Custom styling ([#146])
* [x] Canvas for 2D graphics ([#193])
* [x] Basic overlay support ([#444])
* [x] Animations [#31]
[#24]: https://github.com/iced-rs/iced/issues/24
[#25]: https://github.com/iced-rs/iced/issues/25
[#26]: https://github.com/iced-rs/iced/issues/26
[#28]: https://github.com/iced-rs/iced/issues/28
[#52]: https://github.com/iced-rs/iced/pull/52
[#122]: https://github.com/iced-rs/iced/pull/122
[#146]: https://github.com/iced-rs/iced/pull/146
[#193]: https://github.com/iced-rs/iced/pull/193
[#444]: https://github.com/iced-rs/iced/pull/444
[#31]: https://github.com/iced-rs/iced/issues/31
### Multi-window support ([#27])
Open and control multiple windows at runtime.
I think this could be achieved by implementing an additional trait in `iced_winit` similar to `Application` but with a slightly different `view` method, allowing users to control what is shown in each window.
This approach should also allow us to perform custom optimizations for this particular use case.
[#27]: https://github.com/iced-rs/iced/issues/27
### Canvas widget for 3D graphics (~~[#32]~~ [#343])
A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc.
As a first approach, we could expose the underlying renderer directly here, and couple this widget with it ([`wgpu`] for now). Once [`wgpu`] gets WebGL or WebGPU support, this widget will be able to run on the web too. The renderer primitive could be a simple texture that the widget draws to.
In the long run, we could expose a renderer-agnostic abstraction to perform the drawing.
[#32]: https://github.com/iced-rs/iced/issues/32
[#343]: https://github.com/iced-rs/iced/issues/343
### Text shaping and font fallback ([#33])
[`wgpu_glyph`] uses [`glyph_brush`], which in turn uses [`rusttype`]. While the current implementation is able to layout text quite nicely, it does not perform any [text shaping].
[Text shaping] with font fallback is a necessary feature for any serious GUI toolkit. It unlocks support to truly localize your application, supporting many different scripts.
The only available library that does a great job at shaping is [HarfBuzz], which is implemented in C++. [`skribo`] seems to be a nice [HarfBuzz] wrapper for Rust.
This feature will probably imply rewriting [`wgpu_glyph`] entirely, as caching will be more complicated and the API will probably need to ask for more data.
[#33]: https://github.com/iced-rs/iced/issues/33
[`rusttype`]: https://github.com/redox-os/rusttype
[text shaping]: https://en.wikipedia.org/wiki/Complex_text_layout
[HarfBuzz]: https://github.com/harfbuzz/harfbuzz
[`skribo`]: https://github.com/linebender/skribo
### Grid layout and text layout ([#34])
Currently, `iced_native` only supports flexbox items. For instance, it is not possible to create a grid of items or make text float around an image.
We will need to enhance the layouting engine to support different strategies and improve the way we measure text to lay it out in a more flexible way.
[#34]: https://github.com/iced-rs/iced/issues/34
## Ideas that may be worth exploring
### Reuse existing 2D renderers
While I believe [`wgpu`] has a great future ahead of it, implementing `iced_wgpu` and making it performant will definitely be a challenge.
We should keep an eye on existing 2D graphic libraries, like [`piet`] or [`pathfinder`], and give them a try once/if they mature a bit more.
The good news here is that most of Iced is renderer-agnostic, so changing the rendering strategy, if we deem it worth it, should be really easy. Also, a 2D graphics library will expose a higher-level API than [`wgpu`], so implementing a new renderer on top of it should be fairly straightforward.
[`piet`]: https://github.com/linebender/piet
[`pathfinder`]: https://github.com/servo/pathfinder
### Remove explicit state handling and lifetimes
Currently, `iced_native` forces users to provide the local state of each widget. While this could be considered a really pure form of describing a GUI, it makes some optimizations harder because of the borrow checker.
The current borrow checker is not able to detect a drop was performed before reassigning a value to a mutable variable. Thus, keeping the generated widgets in `Application::view` alive between iterations of the event loop is not possible, which forces us to call this method quite often. `unsafe` could be used to workaround this, but it would feel fishy.
We could take a different approach, and keep some kind of state tree decoupled from the actual widget definitions. This would force us to perform diffing of nodes, as the widgets will represent the desired state and not the whole state.
Once the state lifetime of widgets is removed, we could keep them alive between iterations and even make `Application::view` take a non-mutable reference. This would also improve the end-user API, as it will not be necessary to constantly provide mutable state to widgets.
This is a big undertaking and introduces a new set of problems. We should research and consider the implications of this approach in detail before going for it.
### Try a different font rasterizer
[`wgpu_glyph`] depends indirectly on [`rusttype`]. We may be able to gain performance by using a different font rasterizer. [`fontdue`], for instance, has reported noticeable speedups.
[`fontdue`]: https://github.com/mooman219/fontdue
### Connect `iced_web` with `web-view`
It may be interesting to try to connect `iced_web` with [`web-view`]. It would give users a feature-complete renderer for free, and applications would still be leaner than with Electron.
[`web-view`]: https://github.com/Boscop/web-view
### Implement a lazy widget
Once we remove state lifetimes from widgets, we should be able to implement a widget storing a function that generates additional widgets. The runtime would then be able to control when to call this function and cache the generated widgets while some given value does not change.
This could be very useful to build very performant user interfaces with a lot of different items.
[Elm does it very well!](https://guide.elm-lang.org/optimization/lazy.html)
[Elm]: https://elm-lang.org/
[`winit`]: https://github.com/rust-windowing/winit
[`wgpu`]: https://github.com/gfx-rs/wgpu
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
[`glyph_brush`]: https://github.com/alexheretic/glyph-brush

View file

@ -1 +1,2 @@
too-many-arguments-threshold = 20 too-many-arguments-threshold = 20
enum-variant-name-threshold = 10

View file

@ -1,24 +1,27 @@
[package] [package]
name = "iced_core" name = "iced_core"
version = "0.9.0" description = "The essential ideas of iced"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] version.workspace = true
edition = "2021" edition.workspace = true
description = "The essential concepts of Iced" authors.workspace = true
license = "MIT" license.workspace = true
repository = "https://github.com/iced-rs/iced" repository.workspace = true
homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[dependencies] [dependencies]
bitflags = "1.2" bitflags.workspace = true
thiserror = "1" log.workspace = true
log = "0.4.17" thiserror.workspace = true
twox-hash = { version = "1.5", default-features = false } xxhash-rust.workspace = true
num-traits.workspace = true
[dependencies.palette] palette.workspace = true
version = "0.7" palette.optional = true
optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant = "0.1" instant.workspace = true
[target.'cfg(windows)'.dependencies.raw-window-handle] [target.'cfg(windows)'.dependencies.raw-window-handle]
version = "0.5.2" version = "0.5.2"

View file

@ -1,32 +1,72 @@
use crate::{Point, Rectangle, Vector}; use crate::{Point, Rectangle, Vector};
use std::f32::consts::PI;
#[derive(Debug, Copy, Clone, PartialEq)] use std::f32::consts::{FRAC_PI_2, PI};
use std::ops::RangeInclusive;
/// Degrees /// Degrees
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Degrees(pub f32); pub struct Degrees(pub f32);
#[derive(Debug, Copy, Clone, PartialEq)]
/// Radians /// Radians
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Radians(pub f32); pub struct Radians(pub f32);
impl Radians {
/// The range of radians of a circle.
pub const RANGE: RangeInclusive<Radians> = Radians(0.0)..=Radians(2.0 * PI);
}
impl From<Degrees> for Radians { impl From<Degrees> for Radians {
fn from(degrees: Degrees) -> Self { fn from(degrees: Degrees) -> Self {
Radians(degrees.0 * PI / 180.0) Self(degrees.0 * PI / 180.0)
}
}
impl From<f32> for Radians {
fn from(radians: f32) -> Self {
Self(radians)
}
}
impl From<u8> for Radians {
fn from(radians: u8) -> Self {
Self(f32::from(radians))
}
}
impl From<Radians> for f64 {
fn from(radians: Radians) -> Self {
Self::from(radians.0)
}
}
impl num_traits::FromPrimitive for Radians {
fn from_i64(n: i64) -> Option<Self> {
Some(Self(n as f32))
}
fn from_u64(n: u64) -> Option<Self> {
Some(Self(n as f32))
}
fn from_f64(n: f64) -> Option<Self> {
Some(Self(n as f32))
} }
} }
impl Radians { impl Radians {
/// Calculates the line in which the [`Angle`] intercepts the `bounds`. /// Calculates the line in which the angle intercepts the `bounds`.
pub fn to_distance(&self, bounds: &Rectangle) -> (Point, Point) { pub fn to_distance(&self, bounds: &Rectangle) -> (Point, Point) {
let v1 = Vector::new(f32::cos(self.0), f32::sin(self.0)); let angle = self.0 - FRAC_PI_2;
let r = Vector::new(f32::cos(angle), f32::sin(angle));
let distance_to_rect = f32::min( let distance_to_rect = f32::max(
f32::abs((bounds.y - bounds.center().y) / v1.y), f32::abs(r.x * bounds.width / 2.0),
f32::abs(((bounds.x + bounds.width) - bounds.center().x) / v1.x), f32::abs(r.y * bounds.height / 2.0),
); );
let start = bounds.center() + v1 * distance_to_rect; let start = bounds.center() - r * distance_to_rect;
let end = bounds.center() - v1 * distance_to_rect; let end = bounds.center() + r * distance_to_rect;
(start, end) (start, end)
} }

View file

@ -1,7 +1,7 @@
#[cfg(feature = "palette")] #[cfg(feature = "palette")]
use palette::rgb::{Srgb, Srgba}; use palette::rgb::{Srgb, Srgba};
/// A color in the sRGB color space. /// A color in the `sRGB` color space.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Color { pub struct Color {
/// Red component, 0.0 - 1.0 /// Red component, 0.0 - 1.0
@ -89,6 +89,26 @@ impl Color {
} }
} }
/// Creates a [`Color`] from its linear RGBA components.
pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
// As described in:
// https://en.wikipedia.org/wiki/SRGB
fn gamma_component(u: f32) -> f32 {
if u < 0.0031308 {
12.92 * u
} else {
1.055 * u.powf(1.0 / 2.4) - 0.055
}
}
Self {
r: gamma_component(r),
g: gamma_component(g),
b: gamma_component(b),
a,
}
}
/// Converts the [`Color`] into its RGBA8 equivalent. /// Converts the [`Color`] into its RGBA8 equivalent.
#[must_use] #[must_use]
pub fn into_rgba8(self) -> [u8; 4] { pub fn into_rgba8(self) -> [u8; 4] {

View file

@ -5,7 +5,9 @@ use crate::overlay;
use crate::renderer; use crate::renderer;
use crate::widget; use crate::widget;
use crate::widget::tree::{self, Tree}; use crate::widget::tree::{self, Tree};
use crate::{Clipboard, Color, Layout, Length, Rectangle, Shell, Widget}; use crate::{
Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget,
};
use std::any::Any; use std::any::Any;
use std::borrow::Borrow; use std::borrow::Borrow;
@ -291,7 +293,7 @@ where
} }
fn diff(&self, tree: &mut Tree) { fn diff(&self, tree: &mut Tree) {
self.widget.diff(tree) self.widget.diff(tree);
} }
fn width(&self) -> Length { fn width(&self) -> Length {
@ -304,10 +306,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.widget.layout(renderer, limits) self.widget.layout(tree, renderer, limits)
} }
fn operate( fn operate(
@ -325,11 +328,12 @@ where
fn container( fn container(
&mut self, &mut self,
id: Option<&widget::Id>, id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut( operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>, &mut dyn widget::Operation<T>,
), ),
) { ) {
self.operation.container(id, &mut |operation| { self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation }); operate_on_children(&mut MapOperation { operation });
}); });
} }
@ -346,8 +350,10 @@ where
&mut self, &mut self,
state: &mut dyn widget::operation::Scrollable, state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>, id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) { ) {
self.operation.scrollable(state, id); self.operation.scrollable(state, id, bounds, translation);
} }
fn text_input( fn text_input(
@ -380,6 +386,7 @@ where
renderer: &Renderer, renderer: &Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, B>, shell: &mut Shell<'_, B>,
viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
let mut local_messages = Vec::new(); let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages); let mut local_shell = Shell::new(&mut local_messages);
@ -392,6 +399,7 @@ where
renderer, renderer,
clipboard, clipboard,
&mut local_shell, &mut local_shell,
viewport,
); );
shell.merge(local_shell, &self.mapper); shell.merge(local_shell, &self.mapper);
@ -410,7 +418,7 @@ where
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
self.widget self.widget
.draw(tree, renderer, theme, style, layout, cursor, viewport) .draw(tree, renderer, theme, style, layout, cursor, viewport);
} }
fn mouse_interaction( fn mouse_interaction(
@ -484,10 +492,11 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.element.widget.layout(renderer, limits) self.element.widget.layout(tree, renderer, limits)
} }
fn operate( fn operate(
@ -499,7 +508,7 @@ where
) { ) {
self.element self.element
.widget .widget
.operate(state, layout, renderer, operation) .operate(state, layout, renderer, operation);
} }
fn on_event( fn on_event(
@ -511,10 +520,11 @@ where
renderer: &Renderer, renderer: &Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
self.element self.element.widget.on_event(
.widget state, event, layout, cursor, renderer, clipboard, shell, viewport,
.on_event(state, event, layout, cursor, renderer, clipboard, shell) )
} }
fn draw( fn draw(

View file

@ -10,8 +10,8 @@ pub struct Font {
pub weight: Weight, pub weight: Weight,
/// The [`Stretch`] of the [`Font`]. /// The [`Stretch`] of the [`Font`].
pub stretch: Stretch, pub stretch: Stretch,
/// Whether if the [`Font`] is monospaced or not. /// The [`Style`] of the [`Font`].
pub monospaced: bool, pub style: Style,
} }
impl Font { impl Font {
@ -20,13 +20,12 @@ impl Font {
family: Family::SansSerif, family: Family::SansSerif,
weight: Weight::Normal, weight: Weight::Normal,
stretch: Stretch::Normal, stretch: Stretch::Normal,
monospaced: false, style: Style::Normal,
}; };
/// A monospaced font with normal [`Weight`]. /// A monospaced font with normal [`Weight`].
pub const MONOSPACE: Font = Font { pub const MONOSPACE: Font = Font {
family: Family::Monospace, family: Family::Monospace,
monospaced: true,
..Self::DEFAULT ..Self::DEFAULT
}; };
@ -100,3 +99,13 @@ pub enum Stretch {
ExtraExpanded, ExtraExpanded,
UltraExpanded, UltraExpanded,
} }
/// The style of some text.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Style {
#[default]
Normal,
Italic,
Oblique,
}

View file

@ -6,10 +6,8 @@ use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), /// A fill which transitions colors progressively along a direction, either linearly, radially (TBD),
/// or conically (TBD). /// or conically (TBD).
///
/// For a gradient which can be used as a fill on a canvas, see [`iced_graphics::Gradient`].
pub enum Gradient { pub enum Gradient {
/// A linear gradient interpolates colors along a direction at a specific [`Angle`]. /// A linear gradient interpolates colors along a direction at a specific angle.
Linear(Linear), Linear(Linear),
} }
@ -96,8 +94,8 @@ impl Linear {
mut self, mut self,
stops: impl IntoIterator<Item = ColorStop>, stops: impl IntoIterator<Item = ColorStop>,
) -> Self { ) -> Self {
for stop in stops.into_iter() { for stop in stops {
self = self.add_stop(stop.offset, stop.color) self = self.add_stop(stop.offset, stop.color);
} }
self self

View file

@ -1,10 +1,11 @@
/// The hasher used to compare layouts. /// The hasher used to compare layouts.
#[derive(Debug, Default)] #[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways.
pub struct Hasher(twox_hash::XxHash64); #[derive(Default)]
pub struct Hasher(xxhash_rust::xxh3::Xxh3);
impl core::hash::Hasher for Hasher { impl core::hash::Hasher for Hasher {
fn write(&mut self, bytes: &[u8]) { fn write(&mut self, bytes: &[u8]) {
self.0.write(bytes) self.0.write(bytes);
} }
fn finish(&self) -> u64 { fn finish(&self) -> u64 {

View file

@ -164,6 +164,16 @@ impl std::fmt::Debug for Data {
} }
} }
/// Image filtering strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FilterMethod {
/// Bilinear interpolation.
#[default]
Linear,
/// Nearest neighbor.
Nearest,
}
/// A [`Renderer`] that can render raster graphics. /// A [`Renderer`] that can render raster graphics.
/// ///
/// [renderer]: crate::renderer /// [renderer]: crate::renderer
@ -178,5 +188,10 @@ pub trait Renderer: crate::Renderer {
/// Draws an image with the given [`Handle`] and inside the provided /// Draws an image with the given [`Handle`] and inside the provided
/// `bounds`. /// `bounds`.
fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); fn draw(
&mut self,
handle: Self::Handle,
filter_method: FilterMethod,
bounds: Rectangle,
);
} }

View file

@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits; pub use limits::Limits;
pub use node::Node; pub use node::Node;
use crate::{Point, Rectangle, Vector}; use crate::{Point, Rectangle, Size, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates. /// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -63,3 +63,36 @@ impl<'a> Layout<'a> {
}) })
} }
} }
/// Produces a [`Node`] with two children nodes one right next to each other.
pub fn next_to_each_other(
limits: &Limits,
spacing: f32,
left: impl FnOnce(&Limits) -> Node,
right: impl FnOnce(&Limits) -> Node,
) -> Node {
let mut left_node = left(limits);
let left_size = left_node.size();
let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0));
let mut right_node = right(&right_limits);
let right_size = right_node.size();
let (left_y, right_y) = if left_size.height > right_size.height {
(0.0, (left_size.height - right_size.height) / 2.0)
} else {
((right_size.height - left_size.height) / 2.0, 0.0)
};
left_node.move_to(Point::new(0.0, left_y));
right_node.move_to(Point::new(left_size.width + spacing, right_y));
Node::with_children(
Size::new(
left_size.width + spacing + right_size.width,
left_size.height.max(right_size.height),
),
vec![left_node, right_node],
)
}

View file

@ -19,6 +19,7 @@
use crate::Element; use crate::Element;
use crate::layout::{Limits, Node}; use crate::layout::{Limits, Node};
use crate::widget;
use crate::{Alignment, Padding, Point, Size}; use crate::{Alignment, Padding, Point, Size};
/// The main axis of a flex layout. /// The main axis of a flex layout.
@ -66,6 +67,7 @@ pub fn resolve<Message, Renderer>(
spacing: f32, spacing: f32,
align_items: Alignment, align_items: Alignment,
items: &[Element<'_, Message, Renderer>], items: &[Element<'_, Message, Renderer>],
trees: &mut [widget::Tree],
) -> Node ) -> Node
where where
Renderer: crate::Renderer, Renderer: crate::Renderer,
@ -81,7 +83,7 @@ where
let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
nodes.resize(items.len(), Node::default()); nodes.resize(items.len(), Node::default());
for (i, child) in items.iter().enumerate() { for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
let fill_factor = match axis { let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(), Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(), Axis::Vertical => child.as_widget().height(),
@ -94,7 +96,8 @@ where
let child_limits = let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height)); Limits::new(Size::ZERO, Size::new(max_width, max_height));
let layout = child.as_widget().layout(renderer, &child_limits); let layout =
child.as_widget().layout(tree, renderer, &child_limits);
let size = layout.size(); let size = layout.size();
available -= axis.main(size); available -= axis.main(size);
@ -108,7 +111,7 @@ where
let remaining = available.max(0.0); let remaining = available.max(0.0);
for (i, child) in items.iter().enumerate() { for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
let fill_factor = match axis { let fill_factor = match axis {
Axis::Horizontal => child.as_widget().width(), Axis::Horizontal => child.as_widget().width(),
Axis::Vertical => child.as_widget().height(), Axis::Vertical => child.as_widget().height(),
@ -133,7 +136,8 @@ where
Size::new(max_width, max_height), Size::new(max_width, max_height),
); );
let layout = child.as_widget().layout(renderer, &child_limits); let layout =
child.as_widget().layout(tree, renderer, &child_limits);
cross = cross.max(axis.cross(layout.size())); cross = cross.max(axis.cross(layout.size()));
nodes[i] = layout; nodes[i] = layout;

View file

@ -2,7 +2,7 @@
use crate::{Length, Padding, Size}; use crate::{Length, Padding, Size};
/// A set of size constraints for layouting. /// A set of size constraints for layouting.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Limits { pub struct Limits {
min: Size, min: Size,
max: Size, max: Size,

View file

@ -1,29 +1,21 @@
//! The core library of [Iced]. //! The core library of [Iced].
//! //!
//! This library holds basic types that can be reused and re-exported in //! This library holds basic types that can be reused and re-exported in
//! different runtime implementations. For instance, both [`iced_native`] and //! different runtime implementations.
//! [`iced_web`] are built on top of `iced_core`.
//! //!
//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true) //! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
//! //!
//! [Iced]: https://github.com/iced-rs/iced //! [Iced]: https://github.com/iced-rs/iced
//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
//! [`iced_web`]: https://github.com/iced-rs/iced_web
#![doc( #![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)] )]
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny( #![deny(
missing_debug_implementations, missing_debug_implementations,
missing_docs, missing_docs,
unused_results, unused_results,
clippy::extra_unused_lifetimes, rustdoc::broken_intra_doc_links
clippy::from_over_into,
clippy::needless_borrow,
clippy::new_without_default,
clippy::useless_conversion
)] )]
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
pub mod alignment; pub mod alignment;
pub mod clipboard; pub mod clipboard;
pub mod event; pub mod event;

View file

@ -24,7 +24,7 @@ pub enum Kind {
} }
impl Kind { impl Kind {
fn next(&self) -> Kind { fn next(self) -> Kind {
match self { match self {
Kind::Single => Kind::Double, Kind::Single => Kind::Double,
Kind::Double => Kind::Triple, Kind::Double => Kind::Triple,
@ -61,6 +61,11 @@ impl Click {
self.kind self.kind
} }
/// Returns the position of the [`Click`].
pub fn position(&self) -> Point {
self.position
}
fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
let duration = if time > self.time { let duration = if time > self.time {
Some(time - self.time) Some(time - self.time)

View file

@ -11,7 +11,7 @@ use crate::mouse;
use crate::renderer; use crate::renderer;
use crate::widget; use crate::widget;
use crate::widget::Tree; use crate::widget::Tree;
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
/// An interactive component that can be displayed on top of other widgets. /// An interactive component that can be displayed on top of other widgets.
pub trait Overlay<Message, Renderer> pub trait Overlay<Message, Renderer>
@ -25,10 +25,11 @@ where
/// ///
/// [`Node`]: layout::Node /// [`Node`]: layout::Node
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
translation: Vector,
) -> layout::Node; ) -> layout::Node;
/// Draws the [`Overlay`] using the associated `Renderer`. /// Draws the [`Overlay`] using the associated `Renderer`.

View file

@ -13,6 +13,7 @@ use std::any::Any;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Element<'a, Message, Renderer> { pub struct Element<'a, Message, Renderer> {
position: Point, position: Point,
translation: Vector,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>, overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
} }
@ -25,7 +26,11 @@ where
position: Point, position: Point,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>, overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
) -> Self { ) -> Self {
Self { position, overlay } Self {
position,
overlay,
translation: Vector::ZERO,
}
} }
/// Returns the position of the [`Element`]. /// Returns the position of the [`Element`].
@ -36,6 +41,7 @@ where
/// Translates the [`Element`]. /// Translates the [`Element`].
pub fn translate(mut self, translation: Vector) -> Self { pub fn translate(mut self, translation: Vector) -> Self {
self.position = self.position + translation; self.position = self.position + translation;
self.translation = self.translation + translation;
self self
} }
@ -48,19 +54,24 @@ where
{ {
Element { Element {
position: self.position, position: self.position,
translation: self.translation,
overlay: Box::new(Map::new(self.overlay, f)), overlay: Box::new(Map::new(self.overlay, f)),
} }
} }
/// Computes the layout of the [`Element`] in the given bounds. /// Computes the layout of the [`Element`] in the given bounds.
pub fn layout( pub fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
translation: Vector, translation: Vector,
) -> layout::Node { ) -> layout::Node {
self.overlay self.overlay.layout(
.layout(renderer, bounds, self.position + translation) renderer,
bounds,
self.position + translation,
self.translation + translation,
)
} }
/// Processes a runtime [`Event`]. /// Processes a runtime [`Event`].
@ -98,7 +109,7 @@ where
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) { ) {
self.overlay.draw(renderer, theme, style, layout, cursor) self.overlay.draw(renderer, theme, style, layout, cursor);
} }
/// Applies a [`widget::Operation`] to the [`Element`]. /// Applies a [`widget::Operation`] to the [`Element`].
@ -150,12 +161,13 @@ where
Renderer: crate::Renderer, Renderer: crate::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, position: Point,
translation: Vector,
) -> layout::Node { ) -> layout::Node {
self.content.layout(renderer, bounds, position) self.content.layout(renderer, bounds, position, translation)
} }
fn operate( fn operate(
@ -172,11 +184,12 @@ where
fn container( fn container(
&mut self, &mut self,
id: Option<&widget::Id>, id: Option<&widget::Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut( operate_on_children: &mut dyn FnMut(
&mut dyn widget::Operation<T>, &mut dyn widget::Operation<T>,
), ),
) { ) {
self.operation.container(id, &mut |operation| { self.operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapOperation { operation }); operate_on_children(&mut MapOperation { operation });
}); });
} }
@ -193,8 +206,10 @@ where
&mut self, &mut self,
state: &mut dyn widget::operation::Scrollable, state: &mut dyn widget::operation::Scrollable,
id: Option<&widget::Id>, id: Option<&widget::Id>,
bounds: Rectangle,
translation: Vector,
) { ) {
self.operation.scrollable(state, id); self.operation.scrollable(state, id, bounds, translation);
} }
fn text_input( fn text_input(
@ -202,7 +217,7 @@ where
state: &mut dyn widget::operation::TextInput, state: &mut dyn widget::operation::TextInput,
id: Option<&widget::Id>, id: Option<&widget::Id>,
) { ) {
self.operation.text_input(state, id) self.operation.text_input(state, id);
} }
fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
@ -259,7 +274,7 @@ where
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) { ) {
self.content.draw(renderer, theme, style, layout, cursor) self.content.draw(renderer, theme, style, layout, cursor);
} }
fn is_over( fn is_over(

View file

@ -4,7 +4,9 @@ use crate::mouse;
use crate::overlay; use crate::overlay;
use crate::renderer; use crate::renderer;
use crate::widget; use crate::widget;
use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; use crate::{
Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector,
};
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
/// children. /// children.
@ -61,17 +63,16 @@ where
Renderer: crate::Renderer, Renderer: crate::Renderer,
{ {
fn layout( fn layout(
&self, &mut self,
renderer: &Renderer, renderer: &Renderer,
bounds: Size, bounds: Size,
position: Point, _position: Point,
translation: Vector,
) -> layout::Node { ) -> layout::Node {
let translation = position - Point::ORIGIN;
layout::Node::with_children( layout::Node::with_children(
bounds, bounds,
self.children self.children
.iter() .iter_mut()
.map(|child| child.layout(renderer, bounds, translation)) .map(|child| child.layout(renderer, bounds, translation))
.collect(), .collect(),
) )
@ -138,12 +139,12 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>, operation: &mut dyn widget::Operation<Message>,
) { ) {
operation.container(None, &mut |operation| { operation.container(None, layout.bounds(), &mut |operation| {
self.children.iter_mut().zip(layout.children()).for_each( self.children.iter_mut().zip(layout.children()).for_each(
|(child, layout)| { |(child, layout)| {
child.operate(layout, renderer, operation); child.operate(layout, renderer, operation);
}, },
) );
}); });
} }

View file

@ -74,9 +74,9 @@ impl Rectangle<f32> {
/// Returns true if the given [`Point`] is contained in the [`Rectangle`]. /// Returns true if the given [`Point`] is contained in the [`Rectangle`].
pub fn contains(&self, point: Point) -> bool { pub fn contains(&self, point: Point) -> bool {
self.x <= point.x self.x <= point.x
&& point.x <= self.x + self.width && point.x < self.x + self.width
&& self.y <= point.y && self.y <= point.y
&& point.y <= self.y + self.height && point.y < self.y + self.height
} }
/// Returns true if the current [`Rectangle`] is completely within the given /// Returns true if the current [`Rectangle`] is completely within the given
@ -197,3 +197,18 @@ where
} }
} }
} }
impl<T> std::ops::Sub<Vector<T>> for Rectangle<T>
where
T: std::ops::Sub<Output = T>,
{
type Output = Rectangle<T>;
fn sub(self, translation: Vector<T>) -> Self {
Rectangle {
x: self.x - translation.x,
y: self.y - translation.y,
..self
}
}
}

View file

@ -5,26 +5,13 @@ mod null;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub use null::Null; pub use null::Null;
use crate::layout; use crate::{Background, BorderRadius, Color, Rectangle, Vector};
use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector};
/// A component that can be used by widgets to draw themselves on a screen. /// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized { pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`]. /// The supported theme of the [`Renderer`].
type Theme; type Theme;
/// Lays out the elements of a user interface.
///
/// You should override this if you need to perform any operations before or
/// after layouting. For instance, trimming the measurements cache.
fn layout<Message>(
&mut self,
element: &Element<'_, Message, Self>,
limits: &layout::Limits,
) -> layout::Node {
element.as_widget().layout(self, limits)
}
/// Draws the primitives recorded in the given closure in a new layer. /// Draws the primitives recorded in the given closure in a new layer.
/// ///
/// The layer will clip its contents to the provided `bounds`. /// The layer will clip its contents to the provided `bounds`.

View file

@ -1,6 +1,7 @@
use crate::alignment;
use crate::renderer::{self, Renderer}; use crate::renderer::{self, Renderer};
use crate::text::{self, Text}; use crate::text::{self, Text};
use crate::{Background, Font, Point, Rectangle, Size, Vector}; use crate::{Background, Color, Font, Pixels, Point, Rectangle, Size, Vector};
use std::borrow::Cow; use std::borrow::Cow;
@ -41,6 +42,8 @@ impl Renderer for Null {
impl text::Renderer for Null { impl text::Renderer for Null {
type Font = Font; type Font = Font;
type Paragraph = ();
type Editor = ();
const ICON_FONT: Font = Font::DEFAULT; const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0'; const CHECKMARK_ICON: char = '0';
@ -50,37 +53,117 @@ impl text::Renderer for Null {
Font::default() Font::default()
} }
fn default_size(&self) -> f32 { fn default_size(&self) -> Pixels {
16.0 Pixels(16.0)
} }
fn load_font(&mut self, _font: Cow<'static, [u8]>) {} fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn measure( fn fill_paragraph(
&self, &mut self,
_content: &str, _paragraph: &Self::Paragraph,
_size: f32, _position: Point,
_line_height: text::LineHeight, _color: Color,
_font: Font, ) {
_bounds: Size,
_shaping: text::Shaping,
) -> Size {
Size::new(0.0, 20.0)
} }
fn hit_test( fn fill_editor(
&self, &mut self,
_contents: &str, _editor: &Self::Editor,
_size: f32, _position: Point,
_line_height: text::LineHeight, _color: Color,
_font: Self::Font, ) {
_bounds: Size, }
_shaping: text::Shaping,
_point: Point, fn fill_text(
_nearest_only: bool, &mut self,
) -> Option<text::Hit> { _paragraph: Text<'_, Self::Font>,
_position: Point,
_color: Color,
) {
}
}
impl text::Paragraph for () {
type Font = Font;
fn with_text(_text: Text<'_, Self::Font>) -> Self {}
fn resize(&mut self, _new_bounds: Size) {}
fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference {
text::Difference::None
}
fn horizontal_alignment(&self) -> alignment::Horizontal {
alignment::Horizontal::Left
}
fn vertical_alignment(&self) -> alignment::Vertical {
alignment::Vertical::Top
}
fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {
None None
} }
fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} fn min_bounds(&self) -> Size {
Size::ZERO
}
fn hit_test(&self, _point: Point) -> Option<text::Hit> {
None
}
}
impl text::Editor for () {
type Font = Font;
fn with_text(_text: &str) -> Self {}
fn cursor(&self) -> text::editor::Cursor {
text::editor::Cursor::Caret(Point::ORIGIN)
}
fn cursor_position(&self) -> (usize, usize) {
(0, 0)
}
fn selection(&self) -> Option<String> {
None
}
fn line(&self, _index: usize) -> Option<&str> {
None
}
fn line_count(&self) -> usize {
0
}
fn perform(&mut self, _action: text::editor::Action) {}
fn bounds(&self) -> Size {
Size::ZERO
}
fn update(
&mut self,
_new_bounds: Size,
_new_font: Self::Font,
_new_size: Pixels,
_new_line_height: text::LineHeight,
_new_highlighter: &mut impl text::Highlighter,
) {
}
fn highlight<H: text::Highlighter>(
&mut self,
_font: Self::Font,
_highlighter: &mut H,
_format_highlight: impl Fn(
&H::Highlight,
) -> text::highlighter::Format<Self::Font>,
) {
}
} }

View file

@ -35,7 +35,7 @@ impl<'a, Message> Shell<'a, Message> {
self.messages.push(message); self.messages.push(message);
} }
/// Requests a new frame to be drawn at the given [`Instant`]. /// Requests a new frame to be drawn.
pub fn request_redraw(&mut self, request: window::RedrawRequest) { pub fn request_redraw(&mut self, request: window::RedrawRequest) {
match self.redraw_request { match self.redraw_request {
None => { None => {
@ -48,7 +48,7 @@ impl<'a, Message> Shell<'a, Message> {
} }
} }
/// Returns the requested [`Instant`] a redraw should happen, if any. /// Returns the request a redraw should happen, if any.
pub fn redraw_request(&self) -> Option<window::RedrawRequest> { pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
self.redraw_request self.redraw_request
} }
@ -71,7 +71,7 @@ impl<'a, Message> Shell<'a, Message> {
if self.is_layout_invalid { if self.is_layout_invalid {
self.is_layout_invalid = false; self.is_layout_invalid = false;
f() f();
} }
} }

View file

@ -1,6 +1,15 @@
//! Draw and interact with text. //! Draw and interact with text.
mod paragraph;
pub mod editor;
pub mod highlighter;
pub use editor::Editor;
pub use highlighter::Highlighter;
pub use paragraph::Paragraph;
use crate::alignment; use crate::alignment;
use crate::{Color, Pixels, Point, Rectangle, Size}; use crate::{Color, Pixels, Point, Size};
use std::borrow::Cow; use std::borrow::Cow;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -12,17 +21,14 @@ pub struct Text<'a, Font> {
pub content: &'a str, pub content: &'a str,
/// The bounds of the paragraph. /// The bounds of the paragraph.
pub bounds: Rectangle, pub bounds: Size,
/// The size of the [`Text`] in logical pixels. /// The size of the [`Text`] in logical pixels.
pub size: f32, pub size: Pixels,
/// The line height of the [`Text`]. /// The line height of the [`Text`].
pub line_height: LineHeight, pub line_height: LineHeight,
/// The color of the [`Text`].
pub color: Color,
/// The font of the [`Text`]. /// The font of the [`Text`].
pub font: Font, pub font: Font,
@ -129,10 +135,43 @@ impl Hit {
} }
} }
/// The difference detected in some text.
///
/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some
/// [`Text`].
///
/// [`compare`]: Paragraph::compare
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Difference {
/// No difference.
///
/// The text can be reused as it is!
None,
/// A bounds difference.
///
/// This normally means a relayout is necessary, but the shape of the text can
/// be reused.
Bounds,
/// A shape difference.
///
/// The contents, alignment, sizes, fonts, or any other essential attributes
/// of the shape of the text have changed. A complete reshape and relayout of
/// the text is necessary.
Shape,
}
/// A renderer capable of measuring and drawing [`Text`]. /// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer { pub trait Renderer: crate::Renderer {
/// The font type used. /// The font type used.
type Font: Copy; type Font: Copy + PartialEq;
/// The [`Paragraph`] of this [`Renderer`].
type Paragraph: Paragraph<Font = Self::Font> + 'static;
/// The [`Editor`] of this [`Renderer`].
type Editor: Editor<Font = Self::Font> + 'static;
/// The icon font of the backend. /// The icon font of the backend.
const ICON_FONT: Self::Font; const ICON_FONT: Self::Font;
@ -151,62 +190,35 @@ pub trait Renderer: crate::Renderer {
fn default_font(&self) -> Self::Font; fn default_font(&self) -> Self::Font;
/// Returns the default size of [`Text`]. /// Returns the default size of [`Text`].
fn default_size(&self) -> f32; fn default_size(&self) -> Pixels;
/// Measures the text in the given bounds and returns the minimum boundaries
/// that can fit the contents.
fn measure(
&self,
content: &str,
size: f32,
line_height: LineHeight,
font: Self::Font,
bounds: Size,
shaping: Shaping,
) -> Size;
/// Measures the width of the text as if it were laid out in a single line.
fn measure_width(
&self,
content: &str,
size: f32,
font: Self::Font,
shaping: Shaping,
) -> f32 {
let bounds = self.measure(
content,
size,
LineHeight::Absolute(Pixels(size)),
font,
Size::INFINITY,
shaping,
);
bounds.width
}
/// Tests whether the provided point is within the boundaries of text
/// laid out with the given parameters, returning information about
/// the nearest character.
///
/// If `nearest_only` is true, the hit test does not consider whether the
/// the point is interior to any glyph bounds, returning only the character
/// with the nearest centeroid.
fn hit_test(
&self,
contents: &str,
size: f32,
line_height: LineHeight,
font: Self::Font,
bounds: Size,
shaping: Shaping,
point: Point,
nearest_only: bool,
) -> Option<Hit>;
/// Loads a [`Self::Font`] from its bytes. /// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>); fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Draws the given [`Text`]. /// Draws the given [`Paragraph`] at the given position and with the given
fn fill_text(&mut self, text: Text<'_, Self::Font>); /// [`Color`].
fn fill_paragraph(
&mut self,
text: &Self::Paragraph,
position: Point,
color: Color,
);
/// Draws the given [`Editor`] at the given position and with the given
/// [`Color`].
fn fill_editor(
&mut self,
editor: &Self::Editor,
position: Point,
color: Color,
);
/// Draws the given [`Text`] at the given position and with the given
/// [`Color`].
fn fill_text(
&mut self,
text: Text<'_, Self::Font>,
position: Point,
color: Color,
);
} }

181
core/src/text/editor.rs Normal file
View file

@ -0,0 +1,181 @@
//! Edit text.
use crate::text::highlighter::{self, Highlighter};
use crate::text::LineHeight;
use crate::{Pixels, Point, Rectangle, Size};
use std::sync::Arc;
/// A component that can be used by widgets to edit multi-line text.
pub trait Editor: Sized + Default {
/// The font of the [`Editor`].
type Font: Copy + PartialEq + Default;
/// Creates a new [`Editor`] laid out with the given text.
fn with_text(text: &str) -> Self;
/// Returns the current [`Cursor`] of the [`Editor`].
fn cursor(&self) -> Cursor;
/// Returns the current cursor position of the [`Editor`].
///
/// Line and column, respectively.
fn cursor_position(&self) -> (usize, usize);
/// Returns the current selected text of the [`Editor`].
fn selection(&self) -> Option<String>;
/// Returns the text of the given line in the [`Editor`], if it exists.
fn line(&self, index: usize) -> Option<&str>;
/// Returns the amount of lines in the [`Editor`].
fn line_count(&self) -> usize;
/// Performs an [`Action`] on the [`Editor`].
fn perform(&mut self, action: Action);
/// Returns the current boundaries of the [`Editor`].
fn bounds(&self) -> Size;
/// Updates the [`Editor`] with some new attributes.
fn update(
&mut self,
new_bounds: Size,
new_font: Self::Font,
new_size: Pixels,
new_line_height: LineHeight,
new_highlighter: &mut impl Highlighter,
);
/// Runs a text [`Highlighter`] in the [`Editor`].
fn highlight<H: Highlighter>(
&mut self,
font: Self::Font,
highlighter: &mut H,
format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
);
}
/// An interaction with an [`Editor`].
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
/// Apply a [`Motion`].
Move(Motion),
/// Select text with a given [`Motion`].
Select(Motion),
/// Select the word at the current cursor.
SelectWord,
/// Select the line at the current cursor.
SelectLine,
/// Perform an [`Edit`].
Edit(Edit),
/// Click the [`Editor`] at the given [`Point`].
Click(Point),
/// Drag the mouse on the [`Editor`] to the given [`Point`].
Drag(Point),
/// Scroll the [`Editor`] a certain amount of lines.
Scroll {
/// The amount of lines to scroll.
lines: i32,
},
}
impl Action {
/// Returns whether the [`Action`] is an editing action.
pub fn is_edit(&self) -> bool {
matches!(self, Self::Edit(_))
}
}
/// An action that edits text.
#[derive(Debug, Clone, PartialEq)]
pub enum Edit {
/// Insert the given character.
Insert(char),
/// Paste the given text.
Paste(Arc<String>),
/// Break the current line.
Enter,
/// Delete the previous character.
Backspace,
/// Delete the next character.
Delete,
}
/// A cursor movement.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Motion {
/// Move left.
Left,
/// Move right.
Right,
/// Move up.
Up,
/// Move down.
Down,
/// Move to the left boundary of a word.
WordLeft,
/// Move to the right boundary of a word.
WordRight,
/// Move to the start of the line.
Home,
/// Move to the end of the line.
End,
/// Move to the start of the previous window.
PageUp,
/// Move to the start of the next window.
PageDown,
/// Move to the start of the text.
DocumentStart,
/// Move to the end of the text.
DocumentEnd,
}
impl Motion {
/// Widens the [`Motion`], if possible.
pub fn widen(self) -> Self {
match self {
Self::Left => Self::WordLeft,
Self::Right => Self::WordRight,
Self::Home => Self::DocumentStart,
Self::End => Self::DocumentEnd,
_ => self,
}
}
/// Returns the [`Direction`] of the [`Motion`].
pub fn direction(&self) -> Direction {
match self {
Self::Left
| Self::Up
| Self::WordLeft
| Self::Home
| Self::PageUp
| Self::DocumentStart => Direction::Left,
Self::Right
| Self::Down
| Self::WordRight
| Self::End
| Self::PageDown
| Self::DocumentEnd => Direction::Right,
}
}
}
/// A direction in some text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// <-
Left,
/// ->
Right,
}
/// The cursor of an [`Editor`].
#[derive(Debug, Clone)]
pub enum Cursor {
/// Cursor without a selection
Caret(Point),
/// Cursor selecting a range of text
Selection(Vec<Rectangle>),
}

View file

@ -0,0 +1,88 @@
//! Highlight text.
use crate::Color;
use std::ops::Range;
/// A type capable of highlighting text.
///
/// A [`Highlighter`] highlights lines in sequence. When a line changes,
/// it must be notified and the lines after the changed one must be fed
/// again to the [`Highlighter`].
pub trait Highlighter: 'static {
/// The settings to configure the [`Highlighter`].
type Settings: PartialEq + Clone;
/// The output of the [`Highlighter`].
type Highlight;
/// The highlight iterator type.
type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)>
where
Self: 'a;
/// Creates a new [`Highlighter`] from its [`Self::Settings`].
fn new(settings: &Self::Settings) -> Self;
/// Updates the [`Highlighter`] with some new [`Self::Settings`].
fn update(&mut self, new_settings: &Self::Settings);
/// Notifies the [`Highlighter`] that the line at the given index has changed.
fn change_line(&mut self, line: usize);
/// Highlights the given line.
///
/// If a line changed prior to this, the first line provided here will be the
/// line that changed.
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>;
/// Returns the current line of the [`Highlighter`].
///
/// If `change_line` has been called, this will normally be the least index
/// that changed.
fn current_line(&self) -> usize;
}
/// A highlighter that highlights nothing.
#[derive(Debug, Clone, Copy)]
pub struct PlainText;
impl Highlighter for PlainText {
type Settings = ();
type Highlight = ();
type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>;
fn new(_settings: &Self::Settings) -> Self {
Self
}
fn update(&mut self, _new_settings: &Self::Settings) {}
fn change_line(&mut self, _line: usize) {}
fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
std::iter::empty()
}
fn current_line(&self) -> usize {
usize::MAX
}
}
/// The format of some text.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Format<Font> {
/// The [`Color`] of the text.
pub color: Option<Color>,
/// The `Font` of the text.
pub font: Option<Font>,
}
impl<Font> Default for Format<Font> {
fn default() -> Self {
Self {
color: None,
font: None,
}
}
}

View file

@ -0,0 +1,59 @@
use crate::alignment;
use crate::text::{Difference, Hit, Text};
use crate::{Point, Size};
/// A text paragraph.
pub trait Paragraph: Sized + Default {
/// The font of this [`Paragraph`].
type Font: Copy + PartialEq;
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_text(text: Text<'_, Self::Font>) -> Self;
/// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size);
/// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
fn compare(&self, text: Text<'_, Self::Font>) -> Difference;
/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
/// Returns the vertical alignment of the [`Paragraph`].
fn vertical_alignment(&self) -> alignment::Vertical;
/// Returns the minimum boundaries that can fit the contents of the
/// [`Paragraph`].
fn min_bounds(&self) -> Size;
/// Tests whether the provided point is within the boundaries of the
/// [`Paragraph`], returning information about the nearest character.
fn hit_test(&self, point: Point) -> Option<Hit>;
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// Updates the [`Paragraph`] to match the given [`Text`], if needed.
fn update(&mut self, text: Text<'_, Self::Font>) {
match self.compare(text) {
Difference::None => {}
Difference::Bounds => {
self.resize(text.bounds);
}
Difference::Shape => {
*self = Self::with_text(text);
}
}
}
/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
}
/// Returns the minimum height that can fit the contents of the [`Paragraph`].
fn min_height(&self) -> f32 {
self.min_bounds().height
}
}

View file

@ -33,12 +33,12 @@ use crate::{Clipboard, Length, Rectangle, Shell};
/// - [`geometry`], a custom widget showcasing how to draw geometry with the /// - [`geometry`], a custom widget showcasing how to draw geometry with the
/// `Mesh2D` primitive in [`iced_wgpu`]. /// `Mesh2D` primitive in [`iced_wgpu`].
/// ///
/// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples /// [examples]: https://github.com/iced-rs/iced/tree/0.10/examples
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool /// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.10/examples/bezier_tool
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget /// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.10/examples/custom_widget
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry /// [`geometry`]: https://github.com/iced-rs/iced/tree/0.10/examples/geometry
/// [`lyon`]: https://github.com/nical/lyon /// [`lyon`]: https://github.com/nical/lyon
/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/wgpu /// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.10/wgpu
pub trait Widget<Message, Renderer> pub trait Widget<Message, Renderer>
where where
Renderer: crate::Renderer, Renderer: crate::Renderer,
@ -55,6 +55,7 @@ where
/// user interface. /// user interface.
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node; ) -> layout::Node;
@ -62,7 +63,7 @@ where
/// Draws the [`Widget`] using the associated `Renderer`. /// Draws the [`Widget`] using the associated `Renderer`.
fn draw( fn draw(
&self, &self,
state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -115,6 +116,7 @@ where
_renderer: &Renderer, _renderer: &Renderer,
_clipboard: &mut dyn Clipboard, _clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>, _shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
event::Status::Ignored event::Status::Ignored
} }

View file

@ -8,6 +8,7 @@ pub use scrollable::Scrollable;
pub use text_input::TextInput; pub use text_input::TextInput;
use crate::widget::Id; use crate::widget::Id;
use crate::{Rectangle, Vector};
use std::any::Any; use std::any::Any;
use std::fmt; use std::fmt;
@ -23,6 +24,7 @@ pub trait Operation<T> {
fn container( fn container(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
); );
@ -30,7 +32,14 @@ pub trait Operation<T> {
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
/// Operates on a widget that can be scrolled. /// Operates on a widget that can be scrolled.
fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} fn scrollable(
&mut self,
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
}
/// Operates on a widget that has text input. /// Operates on a widget that has text input.
fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
@ -92,6 +101,7 @@ where
fn container( fn container(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
) { ) {
struct MapRef<'a, A> { struct MapRef<'a, A> {
@ -102,11 +112,12 @@ where
fn container( fn container(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
) { ) {
let Self { operation, .. } = self; let Self { operation, .. } = self;
operation.container(id, &mut |operation| { operation.container(id, bounds, &mut |operation| {
operate_on_children(&mut MapRef { operation }); operate_on_children(&mut MapRef { operation });
}); });
} }
@ -115,8 +126,10 @@ where
&mut self, &mut self,
state: &mut dyn Scrollable, state: &mut dyn Scrollable,
id: Option<&Id>, id: Option<&Id>,
bounds: Rectangle,
translation: Vector,
) { ) {
self.operation.scrollable(state, id); self.operation.scrollable(state, id, bounds, translation);
} }
fn focusable( fn focusable(
@ -145,15 +158,21 @@ where
MapRef { MapRef {
operation: operation.as_mut(), operation: operation.as_mut(),
} }
.container(id, operate_on_children); .container(id, bounds, operate_on_children);
} }
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
self.operation.focusable(state, id); self.operation.focusable(state, id);
} }
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { fn scrollable(
self.operation.scrollable(state, id); &mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
} }
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@ -197,6 +216,7 @@ pub fn scope<T: 'static>(
fn container( fn container(
&mut self, &mut self,
id: Option<&Id>, id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
) { ) {
if id == Some(&self.target) { if id == Some(&self.target) {

View file

@ -1,6 +1,7 @@
//! Operate on widgets that can be focused. //! Operate on widgets that can be focused.
use crate::widget::operation::{Operation, Outcome}; use crate::widget::operation::{Operation, Outcome};
use crate::widget::Id; use crate::widget::Id;
use crate::Rectangle;
/// The internal state of a widget that can be focused. /// The internal state of a widget that can be focused.
pub trait Focusable { pub trait Focusable {
@ -45,9 +46,10 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -80,9 +82,10 @@ where
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
fn finish(&self) -> Outcome<T> { fn finish(&self) -> Outcome<T> {
@ -126,9 +129,10 @@ pub fn focus_previous<T>() -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -159,9 +163,10 @@ pub fn focus_next<T>() -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -185,9 +190,10 @@ pub fn find_focused() -> impl Operation<Id> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
fn finish(&self) -> Outcome<Id> { fn finish(&self) -> Outcome<Id> {

View file

@ -1,5 +1,6 @@
//! Operate on widgets that can be scrolled. //! Operate on widgets that can be scrolled.
use crate::widget::{Id, Operation}; use crate::widget::{Id, Operation};
use crate::{Rectangle, Vector};
/// The internal state of a widget that can be scrolled. /// The internal state of a widget that can be scrolled.
pub trait Scrollable { pub trait Scrollable {
@ -22,12 +23,19 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id { if Some(&self.target) == id {
state.snap_to(self.offset); state.snap_to(self.offset);
} }
@ -49,12 +57,19 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { fn scrollable(
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id { if Some(&self.target) == id {
state.scroll_to(self.offset); state.scroll_to(self.offset);
} }

View file

@ -1,6 +1,7 @@
//! Operate on widgets that have text input. //! Operate on widgets that have text input.
use crate::widget::operation::Operation; use crate::widget::operation::Operation;
use crate::widget::Id; use crate::widget::Id;
use crate::Rectangle;
/// The internal state of a widget that has text input. /// The internal state of a widget that has text input.
pub trait TextInput { pub trait TextInput {
@ -34,9 +35,10 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -63,9 +65,10 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -93,9 +96,10 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }
@ -121,9 +125,10 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
fn container( fn container(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) { ) {
operate_on_children(self) operate_on_children(self);
} }
} }

View file

@ -3,9 +3,9 @@ use crate::alignment;
use crate::layout; use crate::layout;
use crate::mouse; use crate::mouse;
use crate::renderer; use crate::renderer;
use crate::text; use crate::text::{self, Paragraph};
use crate::widget::Tree; use crate::widget::tree::{self, Tree};
use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Widget}; use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget};
use std::borrow::Cow; use std::borrow::Cow;
@ -19,7 +19,7 @@ where
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
{ {
content: Cow<'a, str>, content: Cow<'a, str>,
size: Option<f32>, size: Option<Pixels>,
line_height: LineHeight, line_height: LineHeight,
width: Length, width: Length,
height: Length, height: Length,
@ -53,7 +53,7 @@ where
/// Sets the size of the [`Text`]. /// Sets the size of the [`Text`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self { pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into().0); self.size = Some(size.into());
self self
} }
@ -117,11 +117,23 @@ where
} }
} }
/// The internal state of a [`Text`] widget.
#[derive(Debug, Default)]
pub struct State<P: Paragraph>(P);
impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer> impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
where where
Renderer: text::Renderer, Renderer: text::Renderer,
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
{ {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Renderer::Paragraph>>()
}
fn state(&self) -> tree::State {
tree::State::new(State(Renderer::Paragraph::default()))
}
fn width(&self) -> Length { fn width(&self) -> Length {
self.width self.width
} }
@ -132,30 +144,29 @@ where
fn layout( fn layout(
&self, &self,
tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
let limits = limits.width(self.width).height(self.height); layout(
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
let size = self.size.unwrap_or_else(|| renderer.default_size()); renderer,
limits,
let bounds = renderer.measure( self.width,
self.height,
&self.content, &self.content,
size,
self.line_height, self.line_height,
self.font.unwrap_or_else(|| renderer.default_font()), self.size,
limits.max(), self.font,
self.horizontal_alignment,
self.vertical_alignment,
self.shaping, self.shaping,
); )
let size = limits.resolve(bounds);
layout::Node::new(size)
} }
fn draw( fn draw(
&self, &self,
_state: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
theme: &Renderer::Theme, theme: &Renderer::Theme,
style: &renderer::Style, style: &renderer::Style,
@ -163,22 +174,60 @@ where
_cursor_position: mouse::Cursor, _cursor_position: mouse::Cursor,
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
draw( draw(
renderer, renderer,
style, style,
layout, layout,
&self.content, state,
self.size,
self.line_height,
self.font,
theme.appearance(self.style.clone()), theme.appearance(self.style.clone()),
self.horizontal_alignment,
self.vertical_alignment,
self.shaping,
); );
} }
} }
/// Produces the [`layout::Node`] of a [`Text`] widget.
pub fn layout<Renderer>(
state: &mut State<Renderer::Paragraph>,
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
content: &str,
line_height: LineHeight,
size: Option<Pixels>,
font: Option<Renderer::Font>,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
) -> layout::Node
where
Renderer: text::Renderer,
{
let limits = limits.width(width).height(height);
let bounds = limits.max();
let size = size.unwrap_or_else(|| renderer.default_size());
let font = font.unwrap_or_else(|| renderer.default_font());
let State(ref mut paragraph) = state;
paragraph.update(text::Text {
content,
bounds,
size,
line_height,
font,
horizontal_alignment,
vertical_alignment,
shaping,
});
let size = limits.resolve(paragraph.min_bounds());
layout::Node::new(size)
}
/// Draws text using the same logic as the [`Text`] widget. /// Draws text using the same logic as the [`Text`] widget.
/// ///
/// Specifically: /// Specifically:
@ -193,44 +242,31 @@ pub fn draw<Renderer>(
renderer: &mut Renderer, renderer: &mut Renderer,
style: &renderer::Style, style: &renderer::Style,
layout: Layout<'_>, layout: Layout<'_>,
content: &str, state: &State<Renderer::Paragraph>,
size: Option<f32>,
line_height: LineHeight,
font: Option<Renderer::Font>,
appearance: Appearance, appearance: Appearance,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
) where ) where
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
let State(ref paragraph) = state;
let bounds = layout.bounds(); let bounds = layout.bounds();
let x = match horizontal_alignment { let x = match paragraph.horizontal_alignment() {
alignment::Horizontal::Left => bounds.x, alignment::Horizontal::Left => bounds.x,
alignment::Horizontal::Center => bounds.center_x(), alignment::Horizontal::Center => bounds.center_x(),
alignment::Horizontal::Right => bounds.x + bounds.width, alignment::Horizontal::Right => bounds.x + bounds.width,
}; };
let y = match vertical_alignment { let y = match paragraph.vertical_alignment() {
alignment::Vertical::Top => bounds.y, alignment::Vertical::Top => bounds.y,
alignment::Vertical::Center => bounds.center_y(), alignment::Vertical::Center => bounds.center_y(),
alignment::Vertical::Bottom => bounds.y + bounds.height, alignment::Vertical::Bottom => bounds.y + bounds.height,
}; };
let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_paragraph(
paragraph,
renderer.fill_text(crate::Text { Point::new(x, y),
content, appearance.color.unwrap_or(style.text_color),
size, );
line_height,
bounds: Rectangle { x, y, ..bounds },
color: appearance.color.unwrap_or(style.text_color),
font: font.unwrap_or_else(|| renderer.default_font()),
horizontal_alignment,
vertical_alignment,
shaping,
});
} }
impl<'a, Message, Renderer> From<Text<'a, Renderer>> impl<'a, Message, Renderer> From<Text<'a, Renderer>>

View file

@ -61,7 +61,7 @@ impl Tree {
Renderer: crate::Renderer, Renderer: crate::Renderer,
{ {
if self.tag == new.borrow().tag() { if self.tag == new.borrow().tag() {
new.borrow().diff(self) new.borrow().diff(self);
} else { } else {
*self = Self::new(new); *self = Self::new(new);
} }
@ -78,7 +78,7 @@ impl Tree {
new_children, new_children,
|tree, widget| tree.diff(widget.borrow()), |tree, widget| tree.diff(widget.borrow()),
|widget| Self::new(widget.borrow()), |widget| Self::new(widget.borrow()),
) );
} }
/// Reconciliates the children of the tree with the provided list of widgets using custom /// Reconciliates the children of the tree with the provided list of widgets using custom
@ -107,6 +107,88 @@ impl Tree {
} }
} }
/// Reconciliates the `current_children` with the provided list of widgets using
/// custom logic both for diffing and creating new widget state.
///
/// The algorithm will try to minimize the impact of diffing by querying the
/// `maybe_changed` closure.
pub fn diff_children_custom_with_search<T>(
current_children: &mut Vec<Tree>,
new_children: &[T],
diff: impl Fn(&mut Tree, &T),
maybe_changed: impl Fn(usize) -> bool,
new_state: impl Fn(&T) -> Tree,
) {
if new_children.is_empty() {
current_children.clear();
return;
}
if current_children.is_empty() {
current_children.extend(new_children.iter().map(new_state));
return;
}
let first_maybe_changed = maybe_changed(0);
let last_maybe_changed = maybe_changed(current_children.len() - 1);
if current_children.len() > new_children.len() {
if !first_maybe_changed && last_maybe_changed {
current_children.truncate(new_children.len());
} else {
let difference_index = if first_maybe_changed {
0
} else {
(1..current_children.len())
.find(|&i| maybe_changed(i))
.unwrap_or(0)
};
let _ = current_children.splice(
difference_index
..difference_index
+ (current_children.len() - new_children.len()),
std::iter::empty(),
);
}
}
if current_children.len() < new_children.len() {
let first_maybe_changed = maybe_changed(0);
let last_maybe_changed = maybe_changed(current_children.len() - 1);
if !first_maybe_changed && last_maybe_changed {
current_children.extend(
new_children[current_children.len()..].iter().map(new_state),
);
} else {
let difference_index = if first_maybe_changed {
0
} else {
(1..current_children.len())
.find(|&i| maybe_changed(i))
.unwrap_or(0)
};
let _ = current_children.splice(
difference_index..difference_index,
new_children[difference_index
..difference_index
+ (new_children.len() - current_children.len())]
.iter()
.map(new_state),
);
}
}
// TODO: Merge loop with extend logic (?)
for (child_state, new) in
current_children.iter_mut().zip(new_children.iter())
{
diff(child_state, new);
}
}
/// The identifier of some widget state. /// The identifier of some widget state.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Tag(any::TypeId); pub struct Tag(any::TypeId);

View file

@ -1,5 +1,6 @@
//! Build window-based GUI applications. //! Build window-based GUI applications.
pub mod icon; pub mod icon;
pub mod settings;
mod event; mod event;
mod id; mod id;
@ -7,7 +8,6 @@ mod level;
mod mode; mod mode;
mod position; mod position;
mod redraw_request; mod redraw_request;
mod settings;
mod user_attention; mod user_attention;
pub use event::Event; pub use event::Event;

View file

@ -3,7 +3,7 @@ use crate::Size;
use std::mem; use std::mem;
/// Builds an [`Icon`] from its RGBA pixels in the sRGB color space. /// Builds an [`Icon`] from its RGBA pixels in the `sRGB` color space.
pub fn from_rgba( pub fn from_rgba(
rgba: Vec<u8>, rgba: Vec<u8>,
width: u32, width: u32,
@ -49,7 +49,7 @@ impl Icon {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
/// An error produced when using [`Icon::from_rgba`] with invalid arguments. /// An error produced when using [`from_rgba`] with invalid arguments.
pub enum Error { pub enum Error {
/// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
/// safely interpreted as 32bpp RGBA pixels. /// safely interpreted as 32bpp RGBA pixels.

View file

@ -13,7 +13,7 @@ pub enum RedrawRequest {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::time::{Duration, Instant}; use crate::time::{Duration, Instant};
#[test] #[test]
fn ordering() { fn ordering() {

View file

@ -1,5 +1,4 @@
use crate::window::{Icon, Level, Position}; //! Configure your windows.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[path = "settings/windows.rs"] #[path = "settings/windows.rs"]
mod platform; mod platform;
@ -8,6 +7,10 @@ mod platform;
#[path = "settings/macos.rs"] #[path = "settings/macos.rs"]
mod platform; mod platform;
#[cfg(target_os = "linux")]
#[path = "settings/linux.rs"]
mod platform;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[path = "settings/wasm.rs"] #[path = "settings/wasm.rs"]
mod platform; mod platform;
@ -15,13 +18,15 @@ mod platform;
#[cfg(not(any( #[cfg(not(any(
target_os = "windows", target_os = "windows",
target_os = "macos", target_os = "macos",
target_os = "linux",
target_arch = "wasm32" target_arch = "wasm32"
)))] )))]
#[path = "settings/other.rs"] #[path = "settings/other.rs"]
mod platform; mod platform;
pub use platform::PlatformSpecific; use crate::window::{Icon, Level, Position};
pub use platform::PlatformSpecific;
/// The window settings of an application. /// The window settings of an application.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Settings { pub struct Settings {
@ -70,8 +75,8 @@ pub struct Settings {
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Settings { fn default() -> Self {
Settings { Self {
size: (1024, 768), size: (1024, 768),
position: Position::default(), position: Position::default(),
min_size: None, min_size: None,
@ -82,8 +87,8 @@ impl Default for Settings {
transparent: false, transparent: false,
level: Level::default(), level: Level::default(),
icon: None, icon: None,
platform_specific: Default::default(),
exit_on_close_request: true, exit_on_close_request: true,
platform_specific: PlatformSpecific::default(),
} }
} }
} }

View file

@ -0,0 +1,11 @@
//! Platform specific settings for Linux.
/// The platform specific window settings of an application.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PlatformSpecific {
/// Sets the application id of the window.
///
/// As a best practice, it is suggested to select an application id that match
/// the basename of the applications .desktop file.
pub application_id: String,
}

View file

@ -13,7 +13,7 @@ PR_COMMIT_REGEX = re.compile(r"(?i)Merge pull request #(\d+).*")
def get_merged_prs_since_release(repo: str, previous_release_branch: str) -> List[Tuple[str, int, str, str]]: def get_merged_prs_since_release(repo: str, previous_release_branch: str) -> List[Tuple[str, int, str, str]]:
prs = [] prs = []
compare_url = f"https://api.github.com/repos/{repo}/compare/{previous_release_branch}...master" compare_url = f"https://api.github.com/repos/{repo}/compare/{previous_release_branch}...master?per_page=1000"
compare_response = requests.get(compare_url, headers=HEADERS) compare_response = requests.get(compare_url, headers=HEADERS)
if compare_response.status_code == 200: if compare_response.status_code == 200:
@ -23,7 +23,10 @@ def get_merged_prs_since_release(repo: str, previous_release_branch: str) -> Lis
if match: if match:
pr_number = int(match.group(1)) pr_number = int(match.group(1))
pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
print(f"Querying PR {pr_number}")
pr_response = requests.get(pr_url, headers=HEADERS) pr_response = requests.get(pr_url, headers=HEADERS)
if pr_response.status_code == 200: if pr_response.status_code == 200:
pr_data = pr_response.json() pr_data = pr_response.json()
prs.append((pr_data["title"], pr_number, pr_data["html_url"], pr_data["user"]["login"])) prs.append((pr_data["title"], pr_number, pr_data["html_url"], pr_data["user"]["login"]))

View file

@ -10,8 +10,8 @@ A simple UI tour that can run both on native platforms and the web! It showcases
The __[`main`](tour/src/main.rs)__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__. The __[`main`](tour/src/main.rs)__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__.
<div align="center"> <div align="center">
<a href="https://gfycat.com/politeadorableiberianmole"> <a href="https://iced.rs/examples/tour.mp4">
<img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif"> <img src="https://iced.rs/examples/tour.gif">
</a> </a>
</div> </div>
@ -33,8 +33,8 @@ A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input,
The example code is located in the __[`main`](todos/src/main.rs)__ file. The example code is located in the __[`main`](todos/src/main.rs)__ file.
<div align="center"> <div align="center">
<a href="https://gfycat.com/littlesanehalicore"> <a href="https://iced.rs/examples/todos.mp4">
<img src="https://thumbs.gfycat.com/LittleSaneHalicore-small.gif" height="400px"> <img src="https://iced.rs/examples/todos.gif" height="400px">
</a> </a>
</div> </div>
@ -53,9 +53,7 @@ It runs a simulation in a background thread while allowing interaction with a `C
The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file. The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file.
<div align="center"> <div align="center">
<a href="https://gfycat.com/briefaccurateaardvark"> <img src="https://iced.rs/examples/game_of_life.gif">
<img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:
@ -72,9 +70,7 @@ An example showcasing custom styling with a light and dark theme.
The example code is located in the __[`main`](styling/src/main.rs)__ file. The example code is located in the __[`main`](styling/src/main.rs)__ file.
<div align="center"> <div align="center">
<a href="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif"> <img src="https://iced.rs/examples/styling.gif">
<img src="https://user-images.githubusercontent.com/518289/71867993-acff4300-310c-11ea-85a3-d01d8f884346.gif" height="400px">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:
@ -120,9 +116,7 @@ Since [Iced was born in May 2019], it has been powering the user interfaces in
<div align="center"> <div align="center">
<a href="https://gfycat.com/gloomyweakhammerheadshark"> <img src="https://iced.rs/examples/coffee.gif">
<img src="https://thumbs.gfycat.com/GloomyWeakHammerheadshark-small.gif">
</a>
</div> </div>
[Iced was born in May 2019]: https://github.com/hecrj/coffee/pull/35 [Iced was born in May 2019]: https://github.com/hecrj/coffee/pull/35

View file

@ -6,4 +6,5 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] } iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]

View file

@ -37,7 +37,7 @@ impl Application for Arc {
( (
Arc { Arc {
start: Instant::now(), start: Instant::now(),
cache: Default::default(), cache: Cache::default(),
}, },
Command::none(), Command::none(),
) )

View file

@ -6,4 +6,5 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["canvas"] } iced.workspace = true
iced.features = ["canvas"]

View file

@ -5,9 +5,7 @@ A Paint-like tool for drawing Bézier curves using the `Canvas` widget.
The __[`main`]__ file contains all the code of the example. The __[`main`]__ file contains all the code of the example.
<div align="center"> <div align="center">
<a href="https://gfycat.com/soulfulinfiniteantbear"> <img src="https://iced.rs/examples/bezier_tool.gif">
<img src="https://thumbs.gfycat.com/SoulfulInfiniteAntbear-small.gif">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:

View file

@ -81,7 +81,7 @@ mod bezier {
} }
pub fn request_redraw(&mut self) { pub fn request_redraw(&mut self) {
self.cache.clear() self.cache.clear();
} }
} }
@ -100,12 +100,9 @@ mod bezier {
bounds: Rectangle, bounds: Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> (event::Status, Option<Curve>) { ) -> (event::Status, Option<Curve>) {
let cursor_position = let Some(cursor_position) = cursor.position_in(bounds) else {
if let Some(position) = cursor.position_in(bounds) { return (event::Status::Ignored, None);
position };
} else {
return (event::Status::Ignored, None);
};
match event { match event {
Event::Mouse(mouse_event) => { Event::Mouse(mouse_event) => {

View file

@ -6,4 +6,4 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../.." } iced.workspace = true

View file

@ -6,5 +6,7 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] } iced.workspace = true
time = { version = "0.3.5", features = ["local-offset"] } iced.features = ["canvas", "tokio", "debug"]
time = { version = "0.3", features = ["local-offset"] }

View file

@ -35,7 +35,7 @@ impl Application for Clock {
Clock { Clock {
now: time::OffsetDateTime::now_local() now: time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()), .unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
clock: Default::default(), clock: Cache::default(),
}, },
Command::none(), Command::none(),
) )
@ -141,7 +141,7 @@ impl<Message> canvas::Program<Message, Renderer> for Clock {
frame.with_save(|frame| { frame.with_save(|frame| {
frame.rotate(hand_rotation(self.now.second(), 60)); frame.rotate(hand_rotation(self.now.second(), 60));
frame.stroke(&long_hand, thin_stroke()); frame.stroke(&long_hand, thin_stroke());
}) });
}); });
vec![clock] vec![clock]

View file

@ -6,5 +6,7 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["canvas", "palette"] } iced.workspace = true
palette = "0.7.0" iced.features = ["canvas", "palette"]
palette.workspace = true

View file

@ -3,13 +3,11 @@
A color palette generator, based on a user-defined root color. A color palette generator, based on a user-defined root color.
<div align="center"> <div align="center">
<a href="https://gfycat.com/dirtylonebighornsheep"> <img src="screenshot.png">
<img src="screenshot.png">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:
``` ```
cargo run --package pure_color_palette cargo run --package color_palette
``` ```

View file

@ -3,8 +3,8 @@ use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider}; use iced::widget::{column, row, text, Slider};
use iced::{ use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox,
Size, Vector, Settings, Size, Vector,
}; };
use palette::{ use palette::{
self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue, self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
@ -168,7 +168,7 @@ impl Theme {
let mut text = canvas::Text { let mut text = canvas::Text {
horizontal_alignment: alignment::Horizontal::Center, horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Top, vertical_alignment: alignment::Vertical::Top,
size: 15.0, size: Pixels(15.0),
..canvas::Text::default() ..canvas::Text::default()
}; };

View file

@ -0,0 +1,10 @@
[package]
name = "combo_box"
version = "0.1.0"
authors = ["Joao Freitas <jhff.15@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["debug"]

View file

@ -0,0 +1,18 @@
## Combo-Box
A dropdown list of searchable and selectable options.
It displays and positions an overlay based on the window position of the widget.
The __[`main`]__ file contains all the code of the example.
<div align="center">
<img src="combobox.gif">
</div>
You can run it with `cargo run`:
```
cargo run --package combo_box
```
[`main`]: src/main.rs

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,142 @@
use iced::widget::{
column, combo_box, container, scrollable, text, vertical_space,
};
use iced::{Alignment, Element, Length, Sandbox, Settings};
pub fn main() -> iced::Result {
Example::run(Settings::default())
}
struct Example {
languages: combo_box::State<Language>,
selected_language: Option<Language>,
text: String,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Selected(Language),
OptionHovered(Language),
Closed,
}
impl Sandbox for Example {
type Message = Message;
fn new() -> Self {
Self {
languages: combo_box::State::new(Language::ALL.to_vec()),
selected_language: None,
text: String::new(),
}
}
fn title(&self) -> String {
String::from("Combo box - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::Selected(language) => {
self.selected_language = Some(language);
self.text = language.hello().to_string();
}
Message::OptionHovered(language) => {
self.text = language.hello().to_string();
}
Message::Closed => {
self.text = self
.selected_language
.map(|language| language.hello().to_string())
.unwrap_or_default();
}
}
}
fn view(&self) -> Element<Message> {
let combo_box = combo_box(
&self.languages,
"Type a language...",
self.selected_language.as_ref(),
Message::Selected,
)
.on_option_hovered(Message::OptionHovered)
.on_close(Message::Closed)
.width(250);
let content = column![
text(&self.text),
"What is your language?",
combo_box,
vertical_space(150),
]
.width(Length::Fill)
.align_items(Alignment::Center)
.spacing(10);
container(scrollable(content))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Language {
Danish,
#[default]
English,
French,
German,
Italian,
Portuguese,
Spanish,
Other,
}
impl Language {
const ALL: [Language; 8] = [
Language::Danish,
Language::English,
Language::French,
Language::German,
Language::Italian,
Language::Portuguese,
Language::Spanish,
Language::Other,
];
fn hello(&self) -> &str {
match self {
Language::Danish => "Halloy!",
Language::English => "Hello!",
Language::French => "Salut!",
Language::German => "Hallo!",
Language::Italian => "Ciao!",
Language::Portuguese => "Olá!",
Language::Spanish => "¡Hola!",
Language::Other => "... hello?",
}
}
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Language::Danish => "Danish",
Language::English => "English",
Language::French => "French",
Language::German => "German",
Language::Italian => "Italian",
Language::Portuguese => "Portuguese",
Language::Spanish => "Spanish",
Language::Other => "Some other language",
}
)
}
}

View file

@ -6,4 +6,5 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["debug", "lazy"] } iced.workspace = true
iced.features = ["debug", "lazy"]

View file

@ -6,4 +6,8 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../.." } iced.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
iced.workspace = true
iced.features = ["webgl"]

View file

@ -5,9 +5,7 @@ The classic counter example explained in the [`README`](../../README.md).
The __[`main`]__ file contains all the code of the example. The __[`main`]__ file contains all the code of the example.
<div align="center"> <div align="center">
<a href="https://gfycat.com/fairdeadcatbird"> <img src="https://iced.rs/examples/counter.gif">
<img src="https://thumbs.gfycat.com/FairDeadCatbird-small.gif">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:
@ -15,4 +13,12 @@ You can run it with `cargo run`:
cargo run --package counter cargo run --package counter
``` ```
The web version can be run with [`trunk`]:
```
cd examples/counter
trunk serve
```
[`main`]: src/main.rs [`main`]: src/main.rs
[`trunk`]: https://trunkrs.dev/

View file

@ -6,4 +6,5 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["advanced"] } iced.workspace = true
iced.features = ["advanced"]

View file

@ -36,6 +36,7 @@ mod quad {
fn layout( fn layout(
&self, &self,
_tree: &mut widget::Tree,
_renderer: &Renderer, _renderer: &Renderer,
_limits: &layout::Limits, _limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -0,0 +1,17 @@
[package]
name = "custom_shader"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
[dependencies]
iced.workspace = true
iced.features = ["debug", "advanced"]
image.workspace = true
bytemuck.workspace = true
glam.workspace = true
glam.features = ["bytemuck"]
rand = "0.8.5"

View file

@ -0,0 +1,163 @@
mod scene;
use scene::Scene;
use iced::executor;
use iced::time::Instant;
use iced::widget::shader::wgpu;
use iced::widget::{checkbox, column, container, row, shader, slider, text};
use iced::window;
use iced::{
Alignment, Application, Color, Command, Element, Length, Renderer,
Subscription, Theme,
};
fn main() -> iced::Result {
IcedCubes::run(iced::Settings::default())
}
struct IcedCubes {
start: Instant,
scene: Scene,
}
#[derive(Debug, Clone)]
enum Message {
CubeAmountChanged(u32),
CubeSizeChanged(f32),
Tick(Instant),
ShowDepthBuffer(bool),
LightColorChanged(Color),
}
impl Application for IcedCubes {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
(
Self {
start: Instant::now(),
scene: Scene::new(),
},
Command::none(),
)
}
fn title(&self) -> String {
"Iced Cubes".to_string()
}
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::CubeAmountChanged(amount) => {
self.scene.change_amount(amount);
}
Message::CubeSizeChanged(size) => {
self.scene.size = size;
}
Message::Tick(time) => {
self.scene.update(time - self.start);
}
Message::ShowDepthBuffer(show) => {
self.scene.show_depth_buffer = show;
}
Message::LightColorChanged(color) => {
self.scene.light_color = color;
}
}
Command::none()
}
fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
let top_controls = row![
control(
"Amount",
slider(
1..=scene::MAX,
self.scene.cubes.len() as u32,
Message::CubeAmountChanged
)
.width(100)
),
control(
"Size",
slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged)
.step(0.01)
.width(100),
),
checkbox(
"Show Depth Buffer",
self.scene.show_depth_buffer,
Message::ShowDepthBuffer
),
]
.spacing(40);
let bottom_controls = row![
control(
"R",
slider(0.0..=1.0, self.scene.light_color.r, move |r| {
Message::LightColorChanged(Color {
r,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
),
control(
"G",
slider(0.0..=1.0, self.scene.light_color.g, move |g| {
Message::LightColorChanged(Color {
g,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
),
control(
"B",
slider(0.0..=1.0, self.scene.light_color.b, move |b| {
Message::LightColorChanged(Color {
b,
..self.scene.light_color
})
})
.step(0.01)
.width(100)
)
]
.spacing(40);
let controls = column![top_controls, bottom_controls,]
.spacing(10)
.padding(20)
.align_items(Alignment::Center);
let shader =
shader(&self.scene).width(Length::Fill).height(Length::Fill);
container(column![shader, controls].align_items(Alignment::Center))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
fn subscription(&self) -> Subscription<Self::Message> {
window::frames().map(Message::Tick)
}
}
fn control<'a>(
label: &'static str,
control: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
row![text(label), control.into()].spacing(10).into()
}

View file

@ -0,0 +1,186 @@
mod camera;
mod pipeline;
use camera::Camera;
use pipeline::Pipeline;
use crate::wgpu;
use pipeline::cube::{self, Cube};
use iced::mouse;
use iced::time::Duration;
use iced::widget::shader;
use iced::{Color, Rectangle, Size};
use glam::Vec3;
use rand::Rng;
use std::cmp::Ordering;
use std::iter;
pub const MAX: u32 = 500;
#[derive(Clone)]
pub struct Scene {
pub size: f32,
pub cubes: Vec<Cube>,
pub camera: Camera,
pub show_depth_buffer: bool,
pub light_color: Color,
}
impl Scene {
pub fn new() -> Self {
let mut scene = Self {
size: 0.2,
cubes: vec![],
camera: Camera::default(),
show_depth_buffer: false,
light_color: Color::WHITE,
};
scene.change_amount(MAX);
scene
}
pub fn update(&mut self, time: Duration) {
for cube in self.cubes.iter_mut() {
cube.update(self.size, time.as_secs_f32());
}
}
pub fn change_amount(&mut self, amount: u32) {
let curr_cubes = self.cubes.len() as u32;
match amount.cmp(&curr_cubes) {
Ordering::Greater => {
// spawn
let cubes_2_spawn = (amount - curr_cubes) as usize;
let mut cubes = 0;
self.cubes.extend(iter::from_fn(|| {
if cubes < cubes_2_spawn {
cubes += 1;
Some(Cube::new(self.size, rnd_origin()))
} else {
None
}
}));
}
Ordering::Less => {
// chop
let cubes_2_cut = curr_cubes - amount;
let new_len = self.cubes.len() - cubes_2_cut as usize;
self.cubes.truncate(new_len);
}
Ordering::Equal => {}
}
}
}
impl<Message> shader::Program<Message> for Scene {
type State = ();
type Primitive = Primitive;
fn draw(
&self,
_state: &Self::State,
_cursor: mouse::Cursor,
bounds: Rectangle,
) -> Self::Primitive {
Primitive::new(
&self.cubes,
&self.camera,
bounds,
self.show_depth_buffer,
self.light_color,
)
}
}
/// A collection of `Cube`s that can be rendered.
#[derive(Debug)]
pub struct Primitive {
cubes: Vec<cube::Raw>,
uniforms: pipeline::Uniforms,
show_depth_buffer: bool,
}
impl Primitive {
pub fn new(
cubes: &[Cube],
camera: &Camera,
bounds: Rectangle,
show_depth_buffer: bool,
light_color: Color,
) -> Self {
let uniforms = pipeline::Uniforms::new(camera, bounds, light_color);
Self {
cubes: cubes
.iter()
.map(cube::Raw::from_cube)
.collect::<Vec<cube::Raw>>(),
uniforms,
show_depth_buffer,
}
}
}
impl shader::Primitive for Primitive {
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
_bounds: Rectangle,
target_size: Size<u32>,
_scale_factor: f32,
storage: &mut shader::Storage,
) {
if !storage.has::<Pipeline>() {
storage.store(Pipeline::new(device, queue, format, target_size));
}
let pipeline = storage.get_mut::<Pipeline>().unwrap();
//upload data to GPU
pipeline.update(
device,
queue,
target_size,
&self.uniforms,
self.cubes.len(),
&self.cubes,
);
}
fn render(
&self,
storage: &shader::Storage,
target: &wgpu::TextureView,
_target_size: Size<u32>,
viewport: Rectangle<u32>,
encoder: &mut wgpu::CommandEncoder,
) {
//at this point our pipeline should always be initialized
let pipeline = storage.get::<Pipeline>().unwrap();
//render primitive
pipeline.render(
target,
encoder,
viewport,
self.cubes.len() as u32,
self.show_depth_buffer,
);
}
}
fn rnd_origin() -> Vec3 {
Vec3::new(
rand::thread_rng().gen_range(-4.0..4.0),
rand::thread_rng().gen_range(-4.0..4.0),
rand::thread_rng().gen_range(-4.0..2.0),
)
}

View file

@ -0,0 +1,53 @@
use glam::{mat4, vec3, vec4};
use iced::Rectangle;
#[derive(Copy, Clone)]
pub struct Camera {
eye: glam::Vec3,
target: glam::Vec3,
up: glam::Vec3,
fov_y: f32,
near: f32,
far: f32,
}
impl Default for Camera {
fn default() -> Self {
Self {
eye: vec3(0.0, 2.0, 3.0),
target: glam::Vec3::ZERO,
up: glam::Vec3::Y,
fov_y: 45.0,
near: 0.1,
far: 100.0,
}
}
}
pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4(
vec4(1.0, 0.0, 0.0, 0.0),
vec4(0.0, 1.0, 0.0, 0.0),
vec4(0.0, 0.0, 0.5, 0.0),
vec4(0.0, 0.0, 0.5, 1.0),
);
impl Camera {
pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 {
//TODO looks distorted without padding; base on surface texture size instead?
let aspect_ratio = bounds.width / (bounds.height + 150.0);
let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up);
let proj = glam::Mat4::perspective_rh(
self.fov_y,
aspect_ratio,
self.near,
self.far,
);
OPENGL_TO_WGPU_MATRIX * proj * view
}
pub fn position(&self) -> glam::Vec4 {
glam::Vec4::from((self.eye, 0.0))
}
}

View file

@ -0,0 +1,619 @@
pub mod cube;
mod buffer;
mod uniforms;
mod vertex;
pub use uniforms::Uniforms;
use buffer::Buffer;
use vertex::Vertex;
use crate::wgpu;
use crate::wgpu::util::DeviceExt;
use iced::{Rectangle, Size};
const SKY_TEXTURE_SIZE: u32 = 128;
pub struct Pipeline {
pipeline: wgpu::RenderPipeline,
vertices: wgpu::Buffer,
cubes: Buffer,
uniforms: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
depth_texture_size: Size<u32>,
depth_view: wgpu::TextureView,
depth_pipeline: DepthPipeline,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
target_size: Size<u32>,
) -> Self {
//vertices of one cube
let vertices =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cubes vertex buffer"),
contents: bytemuck::cast_slice(&cube::Raw::vertices()),
usage: wgpu::BufferUsages::VERTEX,
});
//cube instance data
let cubes_buffer = Buffer::new(
device,
"cubes instance buffer",
std::mem::size_of::<cube::Raw>() as u64,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
//uniforms for all cubes
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("cubes uniform buffer"),
size: std::mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
//depth buffer
let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("cubes depth texture"),
size: wgpu::Extent3d {
width: target_size.width,
height: target_size.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth_view =
depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
let normal_map_data = load_normal_map_data();
//normal map
let normal_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("cubes normal map texture"),
size: wgpu::Extent3d {
width: 1024,
height: 1024,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
&normal_map_data,
);
let normal_view =
normal_texture.create_view(&wgpu::TextureViewDescriptor::default());
//skybox texture for reflection/refraction
let skybox_data = load_skybox_data();
let skybox_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("cubes skybox texture"),
size: wgpu::Extent3d {
width: SKY_TEXTURE_SIZE,
height: SKY_TEXTURE_SIZE,
depth_or_array_layers: 6, //one for each face of the cube
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
&skybox_data,
);
let sky_view =
skybox_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("cubes skybox texture view"),
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
});
let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("cubes skybox sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cubes uniform bind group layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::Filtering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let uniform_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes uniform bind group"),
layout: &uniform_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniforms.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&sky_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sky_sampler),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(
&normal_view,
),
},
],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes pipeline layout"),
bind_group_layouts: &[&uniform_bind_group_layout],
push_constant_ranges: &[],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cubes shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shaders/cubes.wgsl"),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cubes pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[Vertex::desc(), cube::Raw::desc()],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Max,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
});
let depth_pipeline = DepthPipeline::new(
device,
format,
depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
);
Self {
pipeline,
cubes: cubes_buffer,
uniforms,
uniform_bind_group,
vertices,
depth_texture_size: target_size,
depth_view,
depth_pipeline,
}
}
fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
if self.depth_texture_size.height != size.height
|| self.depth_texture_size.width != size.width
{
let text = device.create_texture(&wgpu::TextureDescriptor {
label: Some("cubes depth texture"),
size: wgpu::Extent3d {
width: size.width,
height: size.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
self.depth_view =
text.create_view(&wgpu::TextureViewDescriptor::default());
self.depth_texture_size = size;
self.depth_pipeline.update(device, &text);
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
uniforms: &Uniforms,
num_cubes: usize,
cubes: &[cube::Raw],
) {
//recreate depth texture if surface texture size has changed
self.update_depth_texture(device, target_size);
// update uniforms
queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
//resize cubes vertex buffer if cubes amount changed
let new_size = num_cubes * std::mem::size_of::<cube::Raw>();
self.cubes.resize(device, new_size as u64);
//always write new cube data since they are constantly rotating
queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes));
}
pub fn render(
&self,
target: &wgpu::TextureView,
encoder: &mut wgpu::CommandEncoder,
viewport: Rectangle<u32>,
num_cubes: u32,
show_depth: bool,
) {
{
let mut pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("cubes.pipeline.pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_scissor_rect(
viewport.x,
viewport.y,
viewport.width,
viewport.height,
);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_vertex_buffer(0, self.vertices.slice(..));
pass.set_vertex_buffer(1, self.cubes.raw.slice(..));
pass.draw(0..36, 0..num_cubes);
}
if show_depth {
self.depth_pipeline.render(encoder, target, viewport);
}
}
}
struct DepthPipeline {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
bind_group: wgpu::BindGroup,
sampler: wgpu::Sampler,
depth_view: wgpu::TextureView,
}
impl DepthPipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
depth_texture: wgpu::TextureView,
) -> Self {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("cubes.depth_pipeline.sampler"),
..Default::default()
});
let bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("cubes.depth_pipeline.bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: false,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes.depth_pipeline.bind_group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(
&depth_texture,
),
},
],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cubes.depth_pipeline.layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cubes.depth_pipeline.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shaders/depth.wgsl"),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cubes.depth_pipeline.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
});
Self {
pipeline,
bind_group_layout,
bind_group,
sampler,
depth_view: depth_texture,
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
depth_texture: &wgpu::Texture,
) {
self.depth_view =
depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
self.bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("cubes.depth_pipeline.bind_group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(
&self.depth_view,
),
},
],
});
}
pub fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
viewport: Rectangle<u32>,
) {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("cubes.pipeline.depth_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(
wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: None,
stencil_ops: None,
},
),
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_scissor_rect(
viewport.x,
viewport.y,
viewport.width,
viewport.height,
);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.draw(0..6, 0..1);
}
}
fn load_skybox_data() -> Vec<u8> {
let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg");
let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg");
let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg");
let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg");
let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg");
let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg");
let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z];
data.iter().fold(vec![], |mut acc, bytes| {
let i = image::load_from_memory_with_format(
bytes,
image::ImageFormat::Jpeg,
)
.unwrap()
.to_rgba8()
.into_raw();
acc.extend(i);
acc
})
}
fn load_normal_map_data() -> Vec<u8> {
let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png");
image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
.unwrap()
.to_rgba8()
.into_raw()
}

View file

@ -0,0 +1,41 @@
use crate::wgpu;
// A custom buffer container for dynamic resizing.
pub struct Buffer {
pub raw: wgpu::Buffer,
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
}
impl Buffer {
pub fn new(
device: &wgpu::Device,
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
) -> Self {
Self {
raw: device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
}),
label,
size,
usage,
}
}
pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) {
if new_size > self.size {
self.raw = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(self.label),
size: new_size,
usage: self.usage,
mapped_at_creation: false,
});
}
}
}

View file

@ -0,0 +1,326 @@
use crate::scene::pipeline::Vertex;
use crate::wgpu;
use glam::{vec2, vec3, Vec3};
use rand::{thread_rng, Rng};
/// A single instance of a cube.
#[derive(Debug, Clone)]
pub struct Cube {
pub rotation: glam::Quat,
pub position: Vec3,
pub size: f32,
rotation_dir: f32,
rotation_axis: glam::Vec3,
}
impl Default for Cube {
fn default() -> Self {
Self {
rotation: glam::Quat::IDENTITY,
position: glam::Vec3::ZERO,
size: 0.1,
rotation_dir: 1.0,
rotation_axis: glam::Vec3::Y,
}
}
}
impl Cube {
pub fn new(size: f32, origin: Vec3) -> Self {
let rnd = thread_rng().gen_range(0.0..=1.0f32);
Self {
rotation: glam::Quat::IDENTITY,
position: origin + Vec3::new(0.1, 0.1, 0.1),
size,
rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 },
rotation_axis: if rnd <= 0.33 {
glam::Vec3::Y
} else if rnd <= 0.66 {
glam::Vec3::X
} else {
glam::Vec3::Z
},
}
}
pub fn update(&mut self, size: f32, time: f32) {
self.rotation = glam::Quat::from_axis_angle(
self.rotation_axis,
time / 2.0 * self.rotation_dir,
);
self.size = size;
}
}
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
#[repr(C)]
pub struct Raw {
transformation: glam::Mat4,
normal: glam::Mat3,
_padding: [f32; 3],
}
impl Raw {
const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
//cube transformation matrix
4 => Float32x4,
5 => Float32x4,
6 => Float32x4,
7 => Float32x4,
//normal rotation matrix
8 => Float32x3,
9 => Float32x3,
10 => Float32x3,
];
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
impl Raw {
pub fn from_cube(cube: &Cube) -> Raw {
Raw {
transformation: glam::Mat4::from_scale_rotation_translation(
glam::vec3(cube.size, cube.size, cube.size),
cube.rotation,
cube.position,
),
normal: glam::Mat3::from_quat(cube.rotation),
_padding: [0.0; 3],
}
}
pub fn vertices() -> [Vertex; 36] {
[
//face 1
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, 0.0, -1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 2
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, 0.0, 1.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 3
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(-1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
//face 4
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(1.0, 0.0, 0.0),
tangent: vec3(0.0, 0.0, -1.0),
uv: vec2(0.0, 1.0),
},
//face 5
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, 0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, -0.5, -0.5),
normal: vec3(0.0, -1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
//face 6
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 1.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(1.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, 0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 0.0),
},
Vertex {
pos: vec3(-0.5, 0.5, -0.5),
normal: vec3(0.0, 1.0, 0.0),
tangent: vec3(1.0, 0.0, 0.0),
uv: vec2(0.0, 1.0),
},
]
}
}

View file

@ -0,0 +1,23 @@
use crate::scene::Camera;
use iced::{Color, Rectangle};
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Uniforms {
camera_proj: glam::Mat4,
camera_pos: glam::Vec4,
light_color: glam::Vec4,
}
impl Uniforms {
pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self {
let camera_proj = camera.build_view_proj_matrix(bounds);
Self {
camera_proj,
camera_pos: camera.position(),
light_color: glam::Vec4::from(light_color.into_linear()),
}
}
}

View file

@ -0,0 +1,31 @@
use crate::wgpu;
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Vertex {
pub pos: glam::Vec3,
pub normal: glam::Vec3,
pub tangent: glam::Vec3,
pub uv: glam::Vec2,
}
impl Vertex {
const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
//position
0 => Float32x3,
//normal
1 => Float32x3,
//tangent
2 => Float32x3,
//uv
3 => Float32x2,
];
pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}

View file

@ -0,0 +1,123 @@
struct Uniforms {
projection: mat4x4<f32>,
camera_pos: vec4<f32>,
light_color: vec4<f32>,
}
const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0);
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var sky_texture: texture_cube<f32>;
@group(0) @binding(2) var tex_sampler: sampler;
@group(0) @binding(3) var normal_texture: texture_2d<f32>;
struct Vertex {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) tangent: vec3<f32>,
@location(3) uv: vec2<f32>,
}
struct Cube {
@location(4) matrix_0: vec4<f32>,
@location(5) matrix_1: vec4<f32>,
@location(6) matrix_2: vec4<f32>,
@location(7) matrix_3: vec4<f32>,
@location(8) normal_matrix_0: vec3<f32>,
@location(9) normal_matrix_1: vec3<f32>,
@location(10) normal_matrix_2: vec3<f32>,
}
struct Output {
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) tangent_pos: vec3<f32>,
@location(2) tangent_camera_pos: vec3<f32>,
@location(3) tangent_light_pos: vec3<f32>,
}
@vertex
fn vs_main(vertex: Vertex, cube: Cube) -> Output {
let cube_matrix = mat4x4<f32>(
cube.matrix_0,
cube.matrix_1,
cube.matrix_2,
cube.matrix_3,
);
let normal_matrix = mat3x3<f32>(
cube.normal_matrix_0,
cube.normal_matrix_1,
cube.normal_matrix_2,
);
//convert to tangent space to calculate lighting in same coordinate space as normal map sample
let tangent = normalize(normal_matrix * vertex.tangent);
let normal = normalize(normal_matrix * vertex.normal);
let bitangent = cross(tangent, normal);
//shift everything into tangent space
let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal));
let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0);
var out: Output;
out.clip_pos = uniforms.projection * world_pos;
out.uv = vertex.uv;
out.tangent_pos = tbn * world_pos.xyz;
out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz;
out.tangent_light_pos = tbn * LIGHT_POS;
return out;
}
//cube properties
const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6);
const SHINE_DAMPER: f32 = 1.0;
const REFLECTIVITY: f32 = 0.8;
const REFRACTION_INDEX: f32 = 1.31;
//fog, for the ~* cinematic effect *~
const FOG_DENSITY: f32 = 0.15;
const FOG_GRADIENT: f32 = 8.0;
const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);
@fragment
fn fs_main(in: Output) -> @location(0) vec4<f32> {
let to_camera = in.tangent_camera_pos - in.tangent_pos;
//normal sample from texture
var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz;
normal = normal * 2.0 - 1.0;
//diffuse
let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos);
let brightness = max(dot(normal, dir_to_light), 0.0);
let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz;
//specular
let dir_to_camera = normalize(to_camera);
let light_dir = -dir_to_light;
let reflected_light_dir = reflect(light_dir, normal);
let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0);
let damped_factor = pow(specular_factor, SHINE_DAMPER);
let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY;
//fog
let distance = length(to_camera);
let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0);
//reflection
let reflection_dir = reflect(dir_to_camera, normal);
let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir);
let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX);
let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir);
let final_reflect_color = mix(reflection_color, refraction_color, 0.5);
//mix it all together!
var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w);
color = mix(color, final_reflect_color, 0.8);
color = mix(FOG_COLOR, color, visibility);
return color;
}

View file

@ -0,0 +1,48 @@
var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(1.0, -1.0)
);
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var depth_sampler: sampler;
@group(0) @binding(1) var depth_texture: texture_2d<f32>;
struct Output {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) v_index: u32) -> Output {
var out: Output;
out.position = vec4<f32>(positions[v_index], 0.0, 1.0);
out.uv = uvs[v_index];
return out;
}
@fragment
fn fs_main(input: Output) -> @location(0) vec4<f32> {
let depth = textureSample(depth_texture, depth_sampler, input.uv).r;
if (depth > .9999) {
discard;
}
let c = 1.0 - depth;
return vec4<f32>(c, c, c, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -6,4 +6,5 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["advanced"] } iced.workspace = true
iced.features = ["advanced"]

View file

@ -5,9 +5,7 @@ A demonstration of how to build a custom widget that draws a circle.
The __[`main`]__ file contains all the code of the example. The __[`main`]__ file contains all the code of the example.
<div align="center"> <div align="center">
<a href="https://gfycat.com/jealouscornyhomalocephale"> <img src="https://iced.rs/examples/custom_widget.gif">
<img src="https://thumbs.gfycat.com/JealousCornyHomalocephale-small.gif">
</a>
</div> </div>
You can run it with `cargo run`: You can run it with `cargo run`:

View file

@ -43,6 +43,7 @@ mod circle {
fn layout( fn layout(
&self, &self,
_tree: &mut widget::Tree,
_renderer: &Renderer, _renderer: &Renderer,
_limits: &layout::Limits, _limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {

View file

@ -6,7 +6,8 @@ edition = "2021"
publish = false publish = false
[dependencies] [dependencies]
iced = { path = "../..", features = ["tokio"] } iced.workspace = true
iced.features = ["tokio"]
[dependencies.reqwest] [dependencies.reqwest]
version = "0.11" version = "0.11"

View file

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

View file

@ -123,7 +123,7 @@ impl Download {
| State::Errored { .. } => { | State::Errored { .. } => {
self.state = State::Downloading { progress: 0.0 }; self.state = State::Downloading { progress: 0.0 };
} }
_ => {} State::Downloading { .. } => {}
} }
} }

View file

@ -0,0 +1,15 @@
[package]
name = "editor"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["highlighter", "tokio", "debug"]
tokio.workspace = true
tokio.features = ["fs"]
rfd = "0.12"

Binary file not shown.

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